筑波大学自販機Mapを支える技術
この記事はOpenStreetMap Advent Calendar 2024 22日目の記事です。
数年前になりますが、大学内にある自動販売機を地図上に並べた「筑波大学自販機Map」というものを作りました。
これはOpenStreetMap(OSM)から自動販売機のデータを取得して地図上に乗せてみただけの単純なサイトです。
なぜ作ろうとしたのか、きっかけをはっきりとは覚えていないのですが、「大学のキャンパスが大きすぎて普段は使わないエリアに行くと自動販売機がどこにあるのかがわからない」という話を友人がしていたのがきっかけだった記憶があります[1]。友人の話を聞いて、Google Mapは当時自動販売機のデータがなかったのに対して、OSMには自動販売機を記述するタグがあり、タグを使って絞り込みができるAPIがあることを思い出し、OSMを利用すれば自動販売機の地図が作れるのではないか、と思い、実装を始めました。
機能
基本的には筑波大学のキャンパス内にある自動販売機を地図上にマッピングしたサイトです。
popupをクリックすると、対応している決済手段と販売しているものを表示してくれます。決済手段も表示できるようにしたのは「飲み物が欲しいが財布はないし取りに行くのは面倒だけどスマホならある」ことが多々あった自分の経験から付けました。
どのようにできているのか
ここからはある程度の知識を持った人向けに実装など技術的な話をしていこうかなと思います。
具体的には以下の知識がある人を想定しています:
- OSMに関しての基礎知識(way、nodeなどの地物の種類、タグの概念など)
- Webフロントエンド開発について知っている
使用しているライブラリ、サービスは以下のようになっています:
- 地図の表示: MapLibre(以前はLeafletを使っていました)
- フロントエンド: Svelte(SvelteKit)
- CSS: tailwindcss(以前はbulmaを使っていました)
全体の構造は以下の画像のようになっています:
Overpass API
Overpass APIとは
Overpass APIとはOSMに登録されているデータを条件を満たしたもののみにフィルタしてデータを出力できるサービス、ソフトウェアです。フィルタ条件は一般にOverpassQLと呼ばれる独自言語を用いて指定します。
Overpass APIからキャンパス内の自動販売機のみを取り出す
先に今回使っているOverpassQLのクエリを示します。
やっていることを説明してみます。
1行目はOverpassQLでいうところの設定という部分になります。左から順に出力フォーマットの指定とtimeout時間の指定です。Overpass APIはXMLかJSONで出力できます。今回はJavaScriptで取扱いが簡単なJSONで出力させることとしました。
2行目、OSMでidが183555030であるwayを取得しています。これは筑波大学の春日エリア(旧図書館情報大学のエリア)に該当します[2]。
3行目では、先程2行目で取得したwayをareaという形に変換しています。あまり詳しいことはわかっていないのですが、閉じたway内にあるものだけを検索できるよう(OSM wikiでは「エリア内エリアクエリ」と呼称されているもの?)にするためにareaという形に変換する必要があるようです。
4、5行目は対象を変更し、2、3行目同様のことを行っています。対象は筑波大学(OSMのIDでは183555029)として、wayを取得し、areaに変換しています。
6~9行目は今まで定義してきたareaの中にある自動販売機、OSMのタグでいうとamenity=vending_machine
タグが含まれているnode(point)を抽出しています。
プログラムからOverpass APIを利用する方法
プログラムからOverpass APIを利用するにはHTTP POSTリクエストを送ることになります。
具体的には、POSTメソッドでリクエストボディの中でdata
属性を作り、そこにクエリを書き込んで送信することになります。
しかし、GETメソッドでURLパラメータdata
にクエリを入れることでもデータの取得ができたので、今回はGETメソッドを使う方法で取得しています。
sveltekit
このサービスではsveltekitをサーバー、フロントエンド双方に使っています。一時期はOGPがうまく表示されなかったことから、Astro上でsvelteを動かしていた時期もありました。しかし、開発効率が悪かったのでsveltekit単体に戻しました。
サーバー
このサービスではvercelを使っています。
vercelのサーバ上で、自動販売機データをブラウザ向けに加工して送信します。具体的には、上記の方法でOverpass APIと通信してデータを取得しGeoJSONへ変換してブラウザに送る、ということをしています。
クライアントとのデータのやり取りはsveltekitのserver load関数を使っています。load関数とはページをレンダリングする際に必要なデータを先に作成したいときに使われる機能になります。load関数にはuniversalとserverの2つの種類があります。universalはserverまたはclientのいずれかの実行環境でデータを作成するもので、serverは実行環境をserverに限定してデータを作成するものになります。なぜuniversalではなくserverを選んだのかというと、1日というスパンで自動販売機が増減することはそこまで考えられにくいことから、キャッシュを効かせても問題がないだろうと思ったためです。
フロントエンド
サーバから送られてきたGeoJSON形式の自動販売機の地点データは、データが届き次第すべてPopupとして作成しています。ブラウザ上でわざわざ自分でJSONをパースしPopupを作成しています。これは単に当時の自分がMapLibreのSourceやLayerという機能を知らなかったためです。(このあたりはそろそろ修正しておきたいところ……。)
Popupをタップした際に表示されるHTMLを作成する際に、自動販売機の地物に付いている、OSMのタグであるvending=*
、payment:*=yes
、indoor=yes
のデータを利用しています。
vending
というタグはその自動販売機が何を販売しているのかを示すタグになっています。例えば、飲料全般であればdrinks
やアイスクリームであればice_cream
などが入ります。
payment:*
というタグは、その地物がどの決済手段に対応しているのかを示すタグになっています。例えば現金であればpayment:cash=yes
、PayPayであればpayment:paypay=yes
のように日本国内でしか使われていない決済手段までwikiに記載があります。
indoor=yes
はその地物が屋内にあることを示します。さらに、level=*
というタグで地物のある階まで表現できることを知ったので、タグがあった場合には階数を表示するようにしました。
こぼれ話ですが、地図の描画には当初Leafletを使っていました。Leafletしか当時は知らなかったためです。しかし、DOM操作の関係でSvelteとの相性が悪いことから、MapLibreに途中で乗せ替えました。
おわりに
ざっとでしたが、自動販売機Mapの実装について説明させてもらいました。
現状では、どこに自動販売機があるのかがわかるだけの、かなり簡易的なサービスになっていますが、「PayPayが使える自動販売機は?」や「パンが売っている自動販売機は?」といった絞り込み検索など、実装されていないことがまだあるので、今後機能の充実もやっていきたいところです。
-
キャンパスの面積は東京ドームで換算すると52個分、バチカン市国やモナコを超える広さのようです。 https://natsu-san.hatenadiary.org/entry/20100531/1275319900 ↩︎
-
OSMでは線のことをwayと呼びます。 ↩︎
Discussion