静的地図サービスを作ってみた


二行まとめ

独自の静的地図サービス を作りました。
副産物として、独自のタイルサーバー とか、ベクタータイル版 ができました。

はじめに

静的地図サービスとは

静的地図サービスとは、URLのパラメーターを利用したカスタマイズ可能な地図を画像として提供するサービスです。

Google が提供する Maps Static API がとても有名です。

https://maps.googleapis.com/maps/api/staticmap?center=Brooklyn+Bridge,New+York,NY&zoom=13&size=600x300&maptype=roadmap&markers=color:blue%7Clabel:S%7C40.702147,-74.015794&markers=color:green%7Clabel:G%7C40.711614,-74.012318&markers=color:red%7Clabel:C%7C40.718217,-73.998284&key=YOUR_API_KEY

最近日本にやってきた mapbox も、Static Images API を提供しています。

https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/-122.4241,37.78,14.25,0,60/600x600?access_token=YOUR_ACCESS_TOKEN

今回作ったもの

独自の静的地図サービス です。

https://static.maps.rest/osm?size=256x256&zoom=14&scale=1&center=43.063678,141.342663&path=43.066489,141.336021|43.067210,141.340441|43.066959,141.344175|43.062211,141.345109|43.061300,141.337416|43.066489,141.336021

動機

昔、とある位置情報を利用したゲームをしていた時期に、とある SNS 上で、ゲームに関するカスタマイズした地図を画像として表示するということを行っていました。

便利がエスカレートした結果として、月間のリクエストが18万強、4万円弱ほどの意図しない出費をしてしまいました。(※某利用者のご好意により、なんとかなりました)

ということで、自分で作ればタダ 理論に則って、色々勉強をしながら車輪の再発明をしました。

やったこと

  • データベースの設計(1)
  • 地図データの取り込み
  • データベースの設計(2)
  • 地図データの変換
  • 地図の描画
  • マーカーやポリゴンの描画
  • Webサービスとして動かす

以前に、既存の地図タイルを利用したそれっぽいサービス を作ったことがあったのですが、地図タイルの利用条件等を考慮した結果、地図も自分で描いた方が良さそうだな。という結論に達しました。

ということで、OpenStreetMap のデータ を利用することにしました。

データベースの設計(1)

使い慣れた PostGIS を使用しました。docker 上で動いています。

OSM のデータ形式に沿って、基底テーブルを作りました。

  • element
    • id (primary key)
    • type (0 = node, 1 = way, 2 = relation)
    • version
    • timestamp
    • changeset

element の派生として、各要素に対応する以下のテーブルを作成しました

  • node (inherits element)
    • lat
    • lon
  • way (inherits element)
  • relation (inherits element)

way -> node と relation -> way/node の関係性を表すテーブルも作成しました

  • element_x_element
    • parent_id
    • parent_type
    • seq
    • child_id
    • child_type
    • role

最後に、タグ用のテーブルを作成しました

  • tag
    • id (primary key)
    • k
    • v
  • element_x_tag
    • element_id
    • element_type
    • tag_id

各テーブルの必要そうなところに適当に INDEX を追加しています。

地図データの取り込み

Geofabrik にある XML 形式のデータを、データベースに取り込むための簡単なプログラムを作りました。

データベースの設計(2)

地図を書くために、指定した領域に存在する描画対象(道路や建物)を高速に検索する必要があるため、上記のデータを、PostGIS 的な形式に変換して保持します。

ということで、以下のようなテーブルを作りました。

  • point
    • id
    • element_id
    • element_type
    • key
    • value
    • tags
    • layer
    • geom
  • line
    • id
    • element_id
    • element_type
    • key
    • value
    • tags
    • layer
    • geom
    • m
  • area
    • id
    • element_id
    • element_type
    • key
    • value
    • tags
    • layer
    • geom
    • m2

id は独自のもの、element_id, element_typeelement.id, element.type です。
key, value は、変換時に変換対象として指定したデータで、例えば、key=highway, value=primary のような値が入ります。
tags には、key, value 以外で描画時に利用するタグを JSONB 形式で保持していて、例えば、{"structure": "tunnel"} のような値が入ります。
layer は、タグの layer を切り出したものです。(tags 自体に取り込んでもいい気がしている)
geom は GEOMETRY(Point), GEOMETRY(LineString), GEOMETRY(Polygon) 形式の値が入っています。
line.m は、線の距離を、 area.m2 は面の面積を保持しています。

地図データの変換

node, way, relation を、point, line, area に変換するためのツールを作りました。

以下のような変換の定義で変換を行っています。

convert.rule
way place island area
node highway traffic_signals point
way highway  motorway line {"bridge.k": "structure", "tunnel.k": "structure"}

地図の描画

指定したエリアの地図を描画するライブラリを Qt で作りました。

ST_Intersects で、指定したエリアのデータを検索し、 QImageQPainter で描画します。

描画対象の設定と、描画の仕方についてはスタイルごとに QML で定義を書いて管理をしています。

Map.qml
import MapTile 1.0

Map {
    id: map
    color: '#A9D3DF'
    Pen {
        id: borderPen
        color: 'black'
        width: 0.1
    }
    Area {
        filter: 'place=island'
        pen: borderPen
        brush: Brush { color: '#F2EFE9' }
        lower: true
    }
}

マーカーやポリゴンの描画

上記のライブラリで生成した QImage 上に、QPainter を利用して、カスタムの要素を描画しています。

Webサービスとして動かす

だいたい Qt で作っているので、QtHttpServer を利用しました。

描画結果の QImage を QImageWriter クラスで PNG 形式の QByteArray に変換して、"image/png" で返しています。

苦労した点

OSM データの取り込み

最初に bounds 内の node, way をデータベースから検索してメモリ上にキャッシュし、既存のものは再登録をしないようにした。

PostGIS 形式への変換

謎のデータが少なからずあり、整合性のチェックの仕組みの実装に結構苦労した。

結局、GEOS という PostGIS も利用している専用のライブラリを利用することにしたけれど、日本語の情報がほとんどないし、英語のドキュメンテーションも不親切だし、開発環境の Mac と、サーバー側(Ubuntu)でバージョンが違うとかで色々色々苦労した。

way は line にするものと area にするものの判別が難しかった。

データベースの設計(2)

対象のデータを高速に検索するとこでとても苦労した。

最初は素直に汎用的に作っていたが全然速度が出なかった。

  • 検索対象のタグを、tag -> element_x_tag -> point/line/area から探すのではなく、あらかじめ key,value として保持しておくことにした。
  • line.m や area.m2 などを用いて、描画対象にならない(=1px に満たない)対象物を簡単に除外できるようにした。
  • line(特にhighway)と area(特にbuilding)を、key と m/m2 でパーティション分けをし、なるべく狭い範囲のテーブルから検索を行うようにした。

あたりの工夫で最初に比べて10倍くらい検索が速くなりました。

地図の描画

線は、データとしては太さがないけれど、描画時には太さを持つので、描画範囲のすぐ外にある線は検索対象にしないといけないという知見を得た。

線を描く際に、道路の縁取りや、道路の接続が自然な見た目になるようにとても苦労をした。

レイヤー順に正しく描画しつつパフォーマンスが落ちないような工夫をした。

Zoom レベルに応じて、何を描くかが見た目もパフォーマンスに影響が大きく、調整にとても苦労した。

https://static.maps.rest/osm?size=400x400&center=35.835933,139.859422&zoom=16&scale=2

まとめ

OpenStreetMap のデータを利用した静的地図サービスができました。
これで無料で静的地図が使い放題!!!?

開発に際してサーバーを さくらのクラウド 様からお借りすることができました。本当にありがとうございます。

ちなみに、自分に都合のいいように API の追加を色々していて、こんな画像も生成できるようになっています。(そのうち別途記事を書きます)

ついでにできたもの

GeoJSON 形式のデータを静的地図にしたいというリクエストがあったので、簡単なインターフェース を作ってみました。砂時計とか出ないので、我慢強くお待ちください。

いわゆるタイルサーバー的なものもできました。

Leaflet を使った デモ

時代はベクタータイルらしいので、ベクター版も作ってみました。

mapboxgl を利用した デモ

TODO

  • 地図の描画内容を充実させる
    • (歩道)橋、トンネル
    • マイナーな要素
  • 地図に文字を描けるようにする
    • 元々想定していた利用方法的には必要ない
    • 実装が多少めんどくさそう…
  • 日本以外の地図も対応する
  • 美しいスタイルを編み出す
  • サービスとしてスケールするような仕組みにする
    • 目指せ月18万アクセス…
    • 今は裏で5プロセスが頑張っているだけ
  • 最低限のアクセス制限(?)の仕組みを作る

FAQ (適宜追加予定)

  • shp2pgsql なぜ使わなかったの?
    • 自分で作って勉強したかったからです
    • boost がよくわからないとかではありません。
  • mapnik なぜ使わなかったの?
    • 自分で作って勉強したかったからです
    • cairo (ry