OpenStreetMap で作成した要素を調べてみたいと思いたって Overpass API (Overpass turbo) を使ってみましたので、その際のメモです。
Overpass API って?
OpenStreetMap のデータを抽出してくれる便利なサービスとのことです。XMLまたは Overpass QL という形式のクエリを投げて、結果を取得するそうです。詳しくは、下記URLを参照してください。
https://wiki.openstreetmap.org/wiki/JA:Overpass_API
実際に使えるサービスを提供してくれてるのがこちら。
これを Web から使えるようにしてくれてるサービスが Overpass turbo になります。詳しい説明は下記を参照してください。
https://wiki.openstreetmap.org/wiki/JA:Overpass_turbo
Overpass turbo を触ってみる
まずは地図上で結果を確認できる Overpass turbo をさっそく触ってみました。
Overpass turbo のサイトにアクセスすると、下記のような入力画面と地図が表示されます(最初、地図の中心はイタリアかどこかですが、下記は伊勢を中心に表示するように移動後のものになります)。
で、このまま、画面上部の『実行』ボタンを押すと、
のように右画面の地図で表示している範囲内の水飲み場を示してくれます。
これは便利そうです。 ということで、左画面に書く Overpass QL について調べてみることにします。
Overpass QL クエリの書き方
中途半端な説明をするよりも、言語ガイド と Overpass QL のページを見てもらうほうがいいのですが、 それだと、あまりに内容があれなんで、ちょっと戸惑った点とかとも合わせて、自分なりの理解の元、基本的な感じを書いておきます。間違ってる可能性も大いにありますので、変なところ見つけた方ぜひご指摘ください。
最初のサンプルとして、下記のものを考えます。
node (34.1316, 136.3006,34.6774, 136.9621) (user:"junichim"); out;
抽出結果はこちら。
伊勢市を中心に、私(ユーザー名 junichim)が編集したノードを表示しています。
クエリ(フィルタ)ステートメント
取得する要素を表す種類指定子(node, way ,relation (rel) , area) と、それらに続くフィルタ条件から成ります。
node で始まる行が、OpenStreetMapのノード(緯度経度を持つ地点)を取得するステートメントです。 続く (34.1316, 136.3006,34.6774, 136.9621) が対象となる領域を指定するフィルタ条件になります。ここでは、(南の緯度, 西の経度, 北の緯度, 東の経度)という形で指定します。 最後の (user:"junichim") がそのノードを作成したユーザーを指定するフィルタになります。
これで1つの命令(ステートメント)になり、最後はセミコロンになります。 表記は、1行で書いても、空白なしでもありでもいけるようです。
フィルタの種類はいろいろありますが、tag のキーバリューを指定する
node (34.1316, 136.3006,34.6774, 136.9621) ["name" ~ "商店$"]; out;
などもよく使うと思います。この場合は name キー が正規表現 ( ~ で指定) で『xx商店』で終わる ($) ものを指定しています。
アクション
上記の例の最後の out がアクションになります。 out アクションにより、クエリ結果を出力することになるようなので、ほぼ書くことになると思います。
out に引き続いて、出力の詳細さなどを記述できます。
out; // out body と同じ out body; // デフォルト out ids; // id のみ out skel; // body より簡潔 out meta; // 一番詳しい
結果の結合
複数のフィルタ結果を統合(和集合)を取りたいときに使います。
( node (34.1316, 136.3006,34.6774, 136.9621) ["name" ~ "商店$"]; node (34.1316, 136.3006,34.6774, 136.9621) ["name" ~ "駅$"]; ); out;
()内に書かれた個々のフィルタを or でつなげたものになります。 この例だと、『xx商店』のノードと『〇〇駅』を取得します。
結果の絞り込み
実は一番動作がよくわからなかったのが、この結果の絞り込みです。
例えば、フィルタステートメントを連続して書くと、『伊勢』で始まり、『郵便局』で終わるものを指定できるような印象がありますが、
node (34.1316, 136.3006,34.6774, 136.9621) ["name" ~ "^伊勢"]; node (34.1316, 136.3006,34.6774, 136.9621) ["name" ~ "郵便局$"]; out;
のように書くと、
あれ?167か所も選ばれています。明らかに伊勢とは関係なさそうなところにもプロットがあります。
この例だと、一つのステートメントで書けば、両方の条件を and で指定できます。
node (34.1316, 136.3006,34.6774, 136.9621) ["name" ~ "^伊勢"] ["name" ~ "郵便局$"]; out;
結果も
となり、16か所、伊勢市近辺の郵便局だけになりました。
いやいや、言語ガイドにある例だと、指定したnameの近くにある別のnameを持つもの なんてのが堂々と載ってます。
node["name"="Bonn"]; node (around:1000) ["name"="Gielgen"]; out body;
(引用元はこちら)
これって、さっきだめだった奴と同じ書き方なのに and 条件になってんじゃないの?
・・・これ調べてみると、実に曲者でして、リファレンスをよく読むと、フィルタステートメントのいくつかは、前のステートメントの結果を受け取らない、とあります。
でも、aroundフィルタでは受け取るんですね。なので、上記の言語ガイドの例は間違いなく and 条件として動いています。
他に、結果セットを明示的に示すフィルタ(入力集合によるフィルタ) も受け取ることができます。
なので、この場合は、
node (34.1316, 136.3006,34.6774, 136.9621) ["name" ~ "^伊勢"]; node._ (34.1316, 136.3006,34.6774, 136.9621) ["name" ~ "郵便局$"]; out;
のようにデフォルトの結果セット (_) を指定してやれば、問題なく and 条件を作れます。
これは、はまりそうです。
なお、結果セットについては、 Overpass QL のページの概要と集合の部分が詳しいです。
Overpass turbo 使用上の注意
Overpass turbo はクエリを書けば、すぐに地図上で結果を確認できるので非常に便利なのですが、取得したデータ量が過大だった場合、下記のような警告が表示されます。
あまりにデータ量が多い時は条件を考え直したほうがいいかもしれませんね。
Overpass API として使ってみる
最後に、Overpass turbo 経由ではなく、スクリプト等で Overpass API を使いたい場合も試しました。 この場合は、下記のエンドポイントに対して data= パラメータで Overpass QL を URL エンコードした文字列を渡せばOKです。
https://overpass-api.de/api/interpreter
POSTでも投げられるそうです。詳しくは下記のページをご覧ください。
http://www.overpass-api.de/command_line.html
ただ、取得する結果のフォーマットをjsonとかにしたいときは、
[out:json]; node (34.1316, 136.3006, 34.6774, 136.9621); out meta;
のように、先頭に出力形式を指定する必要があります(デフォルトはXML)。
試しに、指定範囲内で過去に編集したユーザー一覧を作ってみようと思いやってみました。
指定範囲内のノードをすべて取得する Overpass QL を作成します。
[out:json]; node (34.1316, 136.3006, 34.6774, 136.9621); out meta;
この時、出力は meta を指定して、作成ユーザーの情報が載るようにします。
これを先ほどのエンドポイントに向かって投げます。
curl https://overpass-api.de/api/interpreter?data=%5bout%3ajson%5d%3b%0d%0anode%0d%0a%20%20%2834%2e1316%2c%20136%2e3006%2c%2034%2e6774%2c%20136%2e9621%29%3b%0d%0aout%20meta%3b > test.json
こんな感じで、Overpass API でデータ(約180Mあったのでご注意を)を取得後、
jq ' .elements[] | select(.timestamp >= "2018-01-01T00:00:00Z") | .user ' test.json | sort | uniq
のようにして、 jqで処理すれば簡単に取れますね。
こっちはこっちでいろいろと使えそうです。