読者です 読者をやめる 読者になる 読者になる

smellman's Broken Diary

クソみたいなもんです

地図タイルについて

どうも、生きてます。

今回はFOSS4G Advent Calendar 2015 (1個目)の最後となる記事です。

今回書く内容はタイトルどおり、いまさらだけど地図タイルについていろいろ解説して実際に実装をしてみようというものになっています。
地図エンジニアで無い人でも興味を持っていただき、沼感を感じていただけたらと思います。

なお、先に書いておくと精度とかそういう話はほっておいてるので非常に雑です。

地図タイルって何?

まずは基本となる地図タイルについてご紹介します。

そもそも地図を使ったクライアントソフトウェアとしては古くからGIS(Geographic Information System)という種類のソフトが多く開発されていました。
私も大学生の時に留年して半年間休学していた間だけとある地図会社でアルバイトで実際にVisual BasicGISの開発をしていました。
2002年頃の話なので、まだGoogle Mapsも無いような時代です。

当時は地図データをレンタリングする有償のライブラリを組み込んで地図の表示などはその上で行い、そこにAccessのデータベースに座標と情報を乗っけることでGISを作成していました。
ここでポイントとなるのは地図データと対応するライブラリはセットになっていたので、そのライブラリで対応していないファイルは変換して取り込めるようにするなど、地図データ自体の調整を別途やっていないといけないという状態です。
僕のアルバイト時代の最後の仕事はこの地図データを変換するプログラムをPythonで書いてpy2exeで動かせるようにしたものにVisual BasicGUIを作って使いやすく提供するというものでした(なお、最終日に仕上がった後に丁度便所掃除の順番が回ってきていたので本当の最後の仕事は便所掃除でした。徹底的に綺麗にしたぞ!)

そんな地図ですが、これをWeb上で表示するとしたらどうしたらいいでしょうか?
まず、古く(Netscape Navigator 2.x)からイメージマップという機能がありました。
そのため、静的な地図イメージを作って、その画像のどの部分をクリックすると移動をするというシステムは昔からよくあり、現在でも路線図などで使われていたりします。
(こういう動作をする地図は最近ではd3.jsを使ったモダンなものが多くなっています。)

しかしながら、先ほど書いたようにライブラリをクライアントに組み込むような技術はWeb上には当時は無かったため、主に2つの方法が開発されていきます。
一つはMapserverGeoserverに代表される地図配信サーバを使う方法で、もう一つは地図タイルを使うという方法です。
なお、地図配信サーバは地図タイルを使うためのバックエンドとして動かすことも多くあります。
地図配信サーバは実際にデータを配信する技術で、地図タイルはクライアント側に近い技術となります。

地図配信サーバとデータソース

地図配信サーバについてもうちょっと解説をします。
先ほど「データを配信する技術」と書きましたが、簡単に図にすると以下の様なサーバを指します。

f:id:smellman:20151225091336p:plain

データソースとなるものは地図座標が入っているデータとなります。
ESRI ShapefileやGeoJSONなどはデータそのものに座標がついたベクタファイルです。
現在一般的にWebで使われているベクタファイルのSVGはコンピュータ上の座標(左上を0,0としたもの)に基づいたものですが、地図で使われるベクタファイルの座標は測地系に基づいた座標となります。
ざっくりと言うと緯度経度(latitude, longitude)で表現されるもので、コンピュータの座標(x, y)とは違う概念に基づきます。

ベクタファイル以外にもラスタファイルも使われており、一番有名なファイルはGeoTiffファイルです。
GeoTiffはその名前の通りTIFFファイルをベースとしたものですが、こちらはピクセルに対して測地系に基づいた座標が割り当てられているファイルとなります。
また、GeoTiffファイルはTIFFファイルの特性を使って、バンドごとにデータを格納することができます。
ピクセルごとにデータを格納するのでよくメッシュデータという表現をしたりします。

データソースはPostgreSQLのようなデータベースを指しています。
単純に緯度経度とデータだけが基づいたようなデータから、Geography型に基づくデータを格納するものもあります。
例えばPostgreSQLのextensionとして提供されているPostGISではGeometryとGeographyが型として追加され、それらを操作する関数が多く提供されています。
Geometryはコンピュータなどの座標を指し、Geographyは測地系に基づく座標のデータを指します。
Geometryでは「x: 35, y: 50の点から半径20ピクセル以内のデータを取ってくる」というようなクエリを出しますが、Geographyでは「緯度: 35.613056, 経度: 140.113889 (JR千葉駅)の点から半径200メートル以内のデータを取ってくる」というようなクエリになります。
なお、Geographyでは測地系が考慮されていて、今の座標は世界測地系(WGS84)に基づいたものとしています。

今まで扱ったデータソースはいずれも測地系に基づいたものであり、測地系に基づいたデータを「空間データ」と言い、また空間データだけでは検索が難しいので同時に「空間インデックス(spatial index)」というものが作られます。
余談ですが、Geometry型はコンピュータなどの座標と同じとなりますが同じく空間インデックスを持つように作る事もでき、またライブラリでサポートするときにシンプルな座標系(測地系と言うと変)として扱うことがあります(あとで実例がでてきます)。

さて、地図配信サーバはこれら座標が入ったものを取り込み、クライアントの要求に合わせて処理を行ってファイルを出力します。
例えばPNG画像で取得する場合は「左上を35.613056,140.113889右下を35.522778,140.310806までの地図データを取ってきてデザインを適応して縦横400ピクセルのPNG画像にして出力してください」という要求を出します。
この場合は千葉駅から大網駅までの範囲のがPNG画像をもらうことになります。

なお、正方形の画像だと縮尺がおかしくなるじゃないかと思われると思いますが、縮尺という概念はあくまで自分が慣れてる地図のイメージでしか無いです。
例えば町内会の掲示板に貼りだされてる手書きの地図なんかは縮尺は適当だけどちゃんと地図としては成立しています。
また、メルカトル図法を元にしたOpenStreetMapGoogle Mapsで南極を見てみればこんなに横に平べったくなるはずはないというのがわかります。
なので地図配信サーバへの要求次第ではこのようなズレは当然発生します。
そもそも地球は楕円球(ellipsoid)ですし。

だいたいこれで地図配信サーバというものが何者かというのがわかったと思いますが、ここでちょっと蛇足で測地系の話をしたいと思います。

ややこしい測地系と座標変換

先ほどから測地系とかWGS84とか出てきているのですが、測地系自体は概念としては難しくない代わりに無視すると悲惨な目に合うものなのでちゃんと説明しておこうと思います。

測地系は簡単に言うと地球上(あー、別に月とかにもあるんだけどな)での位置を緯度経度(と標高)を座標で表すための系の示すものです。
系っていうのはシステムっていうのが良いらしいのだけど、個人的にはルールって言ったほうがしっくりきてる。
細かい所はややこしいのでwikipedia測地系を参考にして欲しい。

測地系には代表されるものがいくつかあるけど、一般的にWGS84世界測地系日本測地系(旧日本測地系)の3つがわかっていれば普通は大丈夫です。

WGS84はアメリカで制定された測地系で、GPSで使われていることで有名です。
海外の地図アプリなどではだいたいこれが基準になっていることが多い。

世界測地系は日本で現在測量法などで制定されている日本測地系2000(JGD2000)というものを指しています。
あれ、世界なのに日本だ。
というのはあまり気にしなくてよいらしく、雑に言うと「国内ではこれを使っておけばOK」っていうものです。
雑すぎると偉い人が怒りそうだけど、WGS84と比べても誤差は少ないし、そもそも日本島国だし基本的には問題ない。
実際のところWebアプリが現在Geo Location APIGPSの情報を使っている、つまりWGS84でデータが降ってくるんだけど、現実にそこで差がわからん程度なので細かいことは気にするなっていう話です。
まぁ、測量する人は絶対世界測地系使わないといけないということは覚えておいてください。
法律だもん。

で、一番やっかいな存在が日本測地系という存在です。
日本測地系というか旧日本測地系(Tokyo Datum)っていうのが良いのだろうけど、日本測地系2000のことを世界測地系って言ってしまってるのでだいたい話してるレベルや書類レベルでも日本測地系っていうと旧日本測地系を指すという暗黙の了解みたいなのがあったりします。
で、何が厄介かというと2002年より前は実際に測量で使われていたり、古いシステムが日本測地系だけであったりして、座標データを渡されてしばらく立ってから「あーそのデータ日本測地系ですわー、それしか作ってないので」みたいな会社が未だにあります。
事前に言えよクソが、おっと。
しかもややこしいことに「地図系じゃないでかい会社」とかでこういうケースあるので気をつけたほうがいい。
ちなみに誤差はかなり出て、試しに日本測地系の千葉駅の座標をそのまま変換しないものと、世界測地系への変換後のデータを作ってみたところ使えないぐらい誤差が出てるのがわかります
とりあえず日本測地系でデータが来たら世界測地系WGS84に変換をしましょう。

さて、ここで変換を行う方法なんですが、そのためのライブラリがPROJ.4というものです。
C言語で書かれているライブラリですが、非常に多くの言語のバインディングがある上、Proj4jsという名前でJavascriptに移植もされています。
Javascriptでの日本測地系から世界測地系へ変換は以下のコードで実現できます。

// Tokyo
proj4.defs("EPSG:4301","+proj=longlat +ellps=bessel +towgs84=-146.414,507.337,680.507,0,0,0,0 +no_defs");
// JGD2000
proj4.defs("EPSG:4612","+proj=longlat +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +no_defs");
var longitude = 140.11714968743658;
var latitude = 35.609799649121534;
var ret = proj4("EPSG:4301", "EPSG:4612", [longitude, latitude]);
// ret[0]: 140.1138890177319
// ret[1]: 35.613055982213794

実際に座標をいれられるサンプルはこちらにあります: JS Bin

なお、上記のコードではproj4.defsで変換式を登録していますが、ここではEPSGコードというものを指定しています。
EPSGはEuropean Petroleum Survey Groupの略です。
どうもこの団体が測地系ごとに一意のコードが割り当て、変換式なども管理しているっぽいです(雑)。
で、EPSGコードが分かればそのデータベースからproj4や他のアプリケーションで使うパラメータを取得することができるっていうのが大雑把な仕組みです。
データベースとしては現在はepsg.ioがよく使われていて、例えば旧日本測地系(Tokyo Datum)はEPSG:4301というコードが割り当てられているのでepsg.ioで4301と調べれば雑にTokyoって書かれたページにたどり着くことができ、Exportの項目から各ツールで取り込めるプログラムの断片を取得することができます。
上記のコードのproj4.defsは単にこのサイトから引っ張ってきただけのものというのがわかります。
ちなみにEPSGコードは管理してる団体を指していますが、コード自体はSRID(Spatial Reference System Identifier)というものになります。

で、地図タイルってなんなの

そろそろ本題に戻ります。

先ほどの地図配信サーバではあくまで「こういう条件の地図を画像データとしてくれ」というものでした。
しかしながら地図配信サーバ側でキャッシュをしていても、範囲が異なれば画像は再度作り直しとなってしまいます。
そのために生まれたのが地図タイルという概念です。

地図タイルとはその名の通り地図をタイル状に並べて配置するという技術です。
実は考え方というか実装自体は1995〜1996年ぐらいには日本ではすでにありました。
ただし、現在とは違いJavascriptでどうこうみたいなものではなかったと思われます。
そして、その実装ができた翌年にとある日本の会社が地図タイル自体に特許を取ってしまったため、日本の多くの企業で実装ができなくなってしまいます。
ちなみにGoogle Mapsなんかも出てきた当時から当然その特許に抵触してるんですが、特にそれで揉め事は起きていません。
とてもWebっぽい話なんですが、特許を取った会社は現在はW3Cのメンバーになっていて、そのおかげで「技術特許による訴えを禁じられてる」というイカした事案が発生しています。
なお、最初に実装してた人たちは「この会社のせいで日本のWeb地図は10年は遅れた!」とビール呑みながら語っていたりしてなおさら地図業界の闇深さを感じるものです。
これから具体的な話を書いていきますが、ぜひ地図タイルの進化を感じながら闇深さを思い出しさわやかな気分になっていただきたいです。

さて、先ほど地図をタイル状にならべるというざっくりとした概念しか言っていませんが、実際のところはこれにズームという概念が発生します。
実際にOpenStreetMapを使って説明をしてみます。

  • 地図画像はタイル状(正方形)に分割されています。
    • 多くの実装では256pxの正方形が使われています(ちなみにRetinaディスプレイ向けに512pxで出すっていう事案もあります)。
  • ズームレベル0を世界がすべて収まるようにしています(つまり1枚の画像)
  • それぞれのズームレベルではそれぞれの次元で2のズームレベルの数字-乗のタイルを使います(雑な表現なので下の方の解説見てください)。
  • OpenStreetMapではWeb Mecator Projection (EPSG:3857 古い定義だと EPSG:900913) を使います。
  • opensteeetmap.org においては各画像タイルのリクエストはREST API (GET) を使い、URLは .../{zoom}/{x coordinate}/{y coordinate}.png という形式になります。
    • この仕組みをzxy tileと呼びます。ただし、後述しますが単にリクエストが z/x/y となっているからというわけではないです。

まず、ズームレベル0の時のタイル画像を見てみましょう。URLは https://a.tile.openstreetmap.org/0/0/0.png となります。

https://a.tile.openstreetmap.org/0/0/0.png

次に、それぞれのズームレベルのタイルの数の定義ですが、それぞれの次元(x, y)で2のズームレベルの数字-乗とありますが、計算式を見ればわかりやすいと思います。

  • zoom 0: 20 * 20 = 1 * 1 = 1 tile
  • zoom 1: 21 * 21 = 2 * 2 = 4 tiles
  • zoom 2: 22 * 22 = 4 * 4 = 16 tiles
  • zoom 3: 23 * 23 = 8 * 8 = 64 tiles
  • zoom n: 2n * 2n
  • zoom 18: 218 * 218 = 262,144 * 262,144 = 68,719,476,736 tiles

f:id:smellman:20151225174059p:plain

ちなみにたしかズームレベル22ぐらいまでいくと、日本では(ディスプレイの解像度次第ですが)ほぼ原寸大になるらしいです。
あと、勘の良い人はわかると思いますが、こんなにファイル数があるとinode数で死ぬっていうやつが迫ってきますので、それなりに工夫は必要ですし、その問題を解決したものもあとで解説をします間にあわんかった。
まぁ、個人でタイルを作るときは非常に範囲が狭かったりしますので、あんまり問題になったりしないっていう現実があります。

さて、次はWeb Mecator Projection (EPSG:3857) についてです。
これはGoogle Maps由来のメルカトル図法を定義しているものです。
先ほど 0/0/0.png を見せましたが、このファイルを良く見てください。
ぱっと見てわかりづらいのですが、「南極点と北極点あたりが無い」んです。
地図関係は精度がどうとかいう話にこだわりを持つケースが多いのですが、さすがWeb系企業っていうことでなんと南北の 85.051129 から先をぶった切るということをやってしまっています。
元々メルカトル図法自体赤道から離れていくとドンドン面積が広くなってしまう性質があるのですが、このようにぶった切る事で「どうせいらねーだろ」っていうところを大胆に省いてしまったわけです。
完全に実用主義というわけです。
ところで、こちらでもEPSGコードが出てきています。
EPSGコードでは座標以外にも定義が可能で、epsg.ioのUnitという項目を見るとどういうものを指してるかがわかります。
例えばEPSG:4612はdegree (supplier to define representation)というUnitで、EPSG:3857はmetre(=meter:メートル)というUnitを指しています。
なお、このメートルがなんなのかあんまよくわかってないのですが、gdaltransform(GDALで提供されてる座標変換ツール。たぶんGDAL->OGR->Proj4っていう流れでやってると思う。proj4jsとかでコード書くのが面倒なのでこれでさくっと座標変換するのはおすすめ)で調べるとメートルっぽい距離が出てくるので、どこかを原点にした距離がでてくるっぽい。

$ gdaltransform -s_srs EPSG:4612 -t_srs EPSG:3857
140.113889 35.613056
15597406.7765453 4247508.26884084 -3.5465694963932e-05

まぁ、今回はそんなに問題にならないので割愛します(おい
で、もう一つ古い定義でEPSG:900913というのがありました。
それ以外にもOpenLayers:900913とかOSGEO:900913とかいろいろあってややこしいのですが、900913という文字を良く見ればわかります。
これってGoogleをもじった数字です。
で、みんなで900913っていう文字をつかってやっていたんですが、EPSGコードしてちゃんと管理されるようになり、最初はEPSG:3785となったんですが名称とか変わったりして最終的にEPSG:3857に落ち着いたっていう感じです。
たまにこういうお遊びっぽいことも出てきたりします。
いや、コレ以外しらねーけど。

最後に各画像ファイルへのリクエストについて説明します。
地図タイルでは取得すべき地図画像のURLが4つの要素で決まります。

  1. ズームレベル
  2. X座標
  3. Y座標
  4. 画像の形式

URLの形式にすると {zoom}/{x}/{y}.{format} となるのが一般的です。
実際に http://a.tile.openstreetmap.org/3/2/4.png にアクセスすると以下の図のところが取れるっていう仕組みです。

f:id:smellman:20151225174204p:plain

これでJavascriptでぐりぐり動かせる地図ができるようになったかわかったと思います。
画面に表示するべきタイルだけを取得して表示をして、マウスでドラッグをしたら表示範囲が変わるので必要なタイルだけを取得して、マウスやコントローラでズームレベルを変えればアクセスするファイルのトップレベルと探すファイルのX座標Y座標もズームアップするなら倍の位置になったりと、非常にブラウザで処理しやすいURLがわかるのです。
これが現在良く使われている地図タイルを使ったサイト(OpenStreetMap, 地理院地図など)の仕組みです。
(あ、Google Mapsはベクトルタイルになってからどうやってるか知らんです)

で、この時に気をつけて欲しいのが X軸Y軸の原点が左上から計算されていることです。
コンピュータ使っていたら一般的かもしれませんが、そんな話が地図に通用するはずはありません。
なぜでしょう。

皆さんが使っている緯度経度は (-180, -90) 〜 (180, 90) の範囲です。
つまりコンピュータの座標とは逆なんです。
厄介ですね!

その厄介さはタイルの仕様にも現れています。
まず、タイルの仕様には2つ「有名なもの」があります。
それがTMS(Tile Map Service)とWMTS(Web Map Tile Service)です。
両者の決定的な違いは座標の扱い方です。
具体的には次の図を見てください。

f:id:smellman:20151225174254p:plain

TMSは原点を左下にもっていきます。
つまり数学っぽい座標です。
一方WMTSはコンピュータ座標のように定義されています。
なるほど、では OpenStreetMap はWMTSを採用してるんだ!!!!ってなことにはなっていません。
WMTSは別の意味で厄介なのは、別にタイルのURLや細かいパラメータを定義したXMLファイル(!)を置いて、それから参照しましょうという仕様になっています。
その上、WMTSではHTTP KVP(いわゆるhoge.cgi?a=bみたいなの)とか、なんとSOAP(ぎゃー)で配信するものとかもあったりして、使用自体が恐ろしくカオス。
なのでWMTSでっていうといろいろ厄介な概念を打ち込まれてしまいます。
一方TMSはこちらもXMLファイル(!)を置くっていう仕様があるもののタイル自体のアクセスはRESTful。
でも座標が...

ということで考えだされたのがいろいろ無視して、TMSのRESTfulなところだけ採用してかつY座標を逆にするという発想です。
そしてこの発想でやっていったところほぼ問題が起きなくなり、最終的に現在のzxy tile(ちょっと前までxyz tileっていう人が多かった)が主流になってしまったという感じです。
簡単にまとめますと、以下のとおりです。

  1. Web Mecator Projection を使う
  2. {z}/{x}/{y}.{format} というURLでRESTfulでアクセス可能
  3. TMSとY座標が逆

ちなみに、コレ自体まだ「仕様」としては存在してない暗黙の了解みたいなもんですが、国土地理院地理院タイルもコレになってるので安心しましょう!

というわけで今回僕が話すものはコレです。
実はmbtilesやCesiumの quantized-mesh-1.0 terrain format がTMSを使っていますが、面倒なので割愛します。

ちなみに、いままでさんざんWeb Mecator Projectionを使うって言ってきてるんだけど、実際のところ内部的に測地系WGS84を使っているというものなので、そうでないものも作れます。
よくあるのがSRID:0で定義したもの、つまりGeographyではなくGeometryだけで表現した地図タイルがあり、特にこのケースでは用途が「ゲーム」である場合が多いのでゲームタイルっていう事があります。
例えばドラクエ2とか3とかの地図をタイル化するようなものといえばわかりやすいと思います。
なお、僕は仕事でzxyタイルもゲームタイルも作っていて、例えばヴァル研究所さんの『駅すぱあと』の新しい路線図がゲームタイルに相当します。
まぁ、ゲームタイルまでいくと完全に沼なので僕がFOSS4G Advent Calendar 2014で書いたゲームタイル的な何かについてと突発的に書いた東京の鉄道路線図SVGをさっそくタイル化してみたを参考にしてください。
ちなみに、『駅すぱあと』の新しい路線図ではSVG画像をPhantomJSでタイル化しています。

では、続いては地図タイルを使うライブラリの話をします。

地図タイルを使うライブラリ

今回の目的は地図タイルを作る所まで行きたいので、当然それを使うライブラリについて知らないといけません。
ここからはそのライブラリについてお話します。
なお、みんなが大好きGoogle Maps APIとかでもたぶん使えると思いますが、僕が全く興味ないので丸ごと割愛します。
というかOSSのライブラリだけしか扱いません(キリッ

地図タイルは今やWebにかぎらずスマートフォンのネイティブアプリケーションでも使うことができます。
主にJavascriptライブラリとiOS/Android向けライブラリについてざっくり解説します。

OpenLayersは古くからあるJavascriptライブラリでかつ、非常に巨大なライブラリでzxyタイルはもちろん、他にも多くの地図APIを標準でサポートするほか、Google Maps APIを叩くことでOpenLayersの使い方でGoogle Mapsが使えるという変な実装まで含まれています。
いわゆるフルスタックライブラリという位置づけですが、APIがとにかく多いので今は初心者にはあまりおすすめできないかも(あくまで個人の感想です)。
なお、長い間OpenLayers 2系が使われていましたが、設計の古かったため全面的に書き直しが行われ、現在はOpenLayers 3がメインになっていて、WebGLが使えたりとかわりとモダンな実装になってきています。
また、GeoDjangoで採用されていたライブラリとしても有名です。

Leaflet.jsは現在最も主流と思われるJavascriptライブラリです。
実装がとてもシンプルでかつ動作が速いため急速に普及しました。
基本的な機能を実装している変わりに、サポートしてない機能はプラグインを使う必要があります。
プラグインは結構バージョンごとの依存性が強いため、比較的注意が必要ですが、基本的なWeb地図であればLeaflet.jsだけで十分な機能があるので一番最初に触ってみることをおすすめします。

Mapbox.jsはLeaflet.jsを拡張してMapbox社のサービスを使いやすくしたライブラリです。
Leaflet.jsの機能がそのまま使えるため平たく言うとMapboxというサービスに依存して無ければ特に必要ないですが、現在地図配信の有料サービスとしては最大手ぐらいの位置づけにいるので注意が必要です。
ただ、そろそろ次に話すMapbox GL JS APIの方に移行していくだろうという雰囲気があります。

Mapbox GL JS APIは現在Mapbox社が進めているMapbox Vector Tileに対応したJavascriptライブラリです。
Mapbox Vector Tileについては今回は(もう眠いので)割愛しますが、通常のzxyタイルももちろん使えます(というか、Mapbox Vector Tileもzxyタイルなのでややこしいのですが...)。
また、他のJavascriptライブラリと決定的に違う点は読み込むタイルの設定をStyleとして定義します。
このStyleについては後述しますが、平たく言うとWeb Mecator Projection及びWGS84(EPSG:4326)以外まともに使えないと思われます。
そのため、要件によっては採用ができない可能性があります。
ただし、StyleファイルはMapbox iOS SDK及びMapbox Android SDKと共通なので非常に大きいメリットもあります。

Cesiumは上記までとはまったく違い、いわゆるGoogle EarthのようなものをWebGLで実装したJavascriptライブラリです。
なので、そもそもの表現方法が違います。
ただし、zxyタイルも使うことができるので、作りたいサービスによっては重要な選択肢の一つとなります。

Javascriptライブラリはこのように選び方はちょっと工夫が必要になります。
個人的には以下のように選びます。

  1. 昔からある地図仕様とかのサポートが必要 -> OpenLayers / PluginがあればLeaflet.js
  2. 3DのUIが欲しい -> Cesium
  3. WGS84以外まったく使ってない上にスマートフォンのネイティブアプリと読み込むタイルの設定を共通化したい -> Mapbox GL JS API
  4. Mapboxのタイルを使いたい -> Mapbox GL JS API
  5. Mapboxのタイルを使うのに他の仕組みも取りれないといけない -> Mapbox.js + Leaflet.jsのプラグイン
  6. ゲームタイル -> Leaflet.js / OpenLayers
  7. その他 -> とりあえず Leaflet.js から始めてみる

まぁ、特殊な事案が無い限りLeaflet.jsがオススメということです。

さて、ここで一旦それぞれの実装方法を確認してみましょう。
とここで長々とソースコードを書くのは面倒なので、Leaflet.jsとOpenLayersとCesiumについては国土地理院が公開してる地理院地図の地理院タイルを用いたサイト構築サンプル集を見ましょう。
ソースコードgithubにおいてあるという超親切対応です。
平たく言うとcloneすればあなただけの地図サイトが作れちゃうんですよ!!!!!!!!!

では、今回の例になかった Mapbox GL JS API のサンプルを作ってみましょう。
例のごとくサンプルはJS Binにアップしています。
なお、このサンプルは地理院タイルを使わず、OpenStreetMapのタイルを使っています。
何故かと言うと地理院タイルは「まだ」HTTPSでの配信をしていないため、JS Binで実行すると混在したコンテンツとして扱われてしまうので、HTTPSをサポートしてるOpenStreetMapのタイルサーバを使いました。

サンプルのコアな部分を解説します。

<body>
  <div id='map'></div>
  <script>
    var map = new mapboxgl.Map({
      container: 'map', // container id
      style: 'https://gist.githubusercontent.com/smellman/d3cbc19d134d5283df73/raw/24aa928ac76056ab0e887d6a65484c1806765260/osm_mapbox_gl_example.json', //stylesheet location
      center: [140.1138890177319, 35.613055982213794], // starting position
      zoom: 16 // starting zoom
    });
    map.addControl(new mapboxgl.Navigation({position: 'top-left'})); // position is optional
  </script>
</body>

ざっくりと解説すると、まずはMapbox GL JS APIのExampleをまるごとパクって、今回はMapbox社のサービスにアクセスしないので mapboxgl.accessToken の設定を削除します。
次にstyleを設定しますが、このjson自体はgistにあります。


Mapbox GL Style example using OpenStreetMap tile s ...

jsonの中身をざっくり説明します。
sourceというプロパティで取り込むレイヤーを設定しているのですが、ここでは"osm"というidを作って、typeをrasterに、tilesの中にzxyタイルのエントリポイントを書いています。
{z}/{x}/{y}の部分がそれぞれアクセスするタイルのzoom, x座標, y座標に変換されます。
そしてlayersのところに使うレイヤーを指定します。
レイヤーは複数書くこともでき、アプリケーション側のコードからどのレイヤーを出すかなどがコントロールできるという仕組みです。

Javascriptに戻って、centerとzoomで千葉駅が程よく見えるぐらいにしています。
最後にNavigationのコントロールを追加しています。

本来ならAttributionを入れないといけないのですが、そもそもまだMapbox GL Styleの仕様にAttributionが無いという超ウケる状況です。
みなさん、プルリクチャンスですよ!!!!

というわけで、Javascriptライブラリ側をざっくり説明したのですが、次にスマートフォンの状況をお話します。

まずiOSですが、状況は地獄です。
そもそも、iPhoneでは初期の頃からRoute-Meというライブラリがあった(というか僕も使ってた)のですが、見ての通りまず開発が止まってしまい、代わりにALPSTEIN Tourismusのfork(Alpstein fork)が比較的動いていたところにMapbox社がそれをforkしてMapbox iOS SDKとして開発をしました。
そのため、Alpsteinは開発終了と自分たちのforkではなくMapbox iOS SDKを代わりに使ってくれとアナウンスを出します。
ところが、今度はMapbox社の方が全く新しいmapbox-gl-nativeを作り、現在はこちらがMapbox iOS SDKとしてリリースされてしまったため、古いMapbox iOS SDKの開発がほぼ息絶えました。
いちおうMapbox iOS SDK LegacyはまだCocoaPodsにあるのでいちおうギリギリ使えるかもしれませんが、すでにdeprecatedだし、そもそもCocoaPodsにあるのとgithubにあるpodspecでsourceのURL違うしいろんな意味で地獄っぽさがあります。
なので、iOSでは現在ではmapbox-gl-nativeベースのMapbox iOS SDKを使った方が無難です。

一方、AndroidはOSMDroidという長年使われているらしいライブラリがあります。
らしいというのはOSMDroidは触ったことがないのでよくわからないのです。
たぶん、使い方のドキュメントが4日前にアップデートされてるので使えるはずです。
また、iOSと同様にMapbox Android SDKというmapbox-gl-nativeによるライブラリが提供されていますが、こっちもMapbox Android SDK Legacyという実装があってややこしいことになっています。
ただ、現行でメンテナンスされている実装が二系統あるだけiOSよりはマシかと思います。

さて、こちらもMapbox iOS SDKとMapbox Android SDKで通常のzxyタイルを使う方法を示したいのですが、Mapbox iOS SDKについては数ヶ月前に書いていたりしますのでそちらを参考にしてください。
Mapbox Android SDKについてはLegacyの方しか書いてなかったのでこちらに改めて書きます。

大まかなやり方は以下のとおりです。

  1. 適当にプロジェクトを作成。
  2. Mapbox Android SDKgradle の解説にそって app/build.gradle を編集。
  3. AndroidManifest.xml にネットワークアクセスなどの許可を書く。
  4. activity_main.xml に MapView を追加する。このときmapbox:accesstokenを"pk."という文字を入れておく。
  5. プログラムを書く。

やっかいなのはactivityにあるmapbox:accesstokenです。
Mapbox Android SDK Legacyの方はaccesstoken自体は抜いても問題なかったのですが、新しいMapbox Android SDKでは起動時にチェックをしてくるのです。
ただし、チェックの仕方が先頭の文字が"pk."か"sk."かって違いだけなので、"pk."という文字を入れておけばOKです。
というか、これなぜかMapbox Android SDKのみにチェックが入ってるっぽくて、iOSの実装とかこういうチェックが無いという謎な感じになってる。

というわけで、activity_main.xmlとMainActivity.ktを掲載しておきます(例のごとくKotlinで実装です。かわいいは正義...)。
ちなみにStyleのJSONファイルはiOSのサンプルと同じものを使っています。


Mapbox Android SDK (mapbox-gl-native) activity_mai ...


Mapbox Android SDK (mapbox-gl-native) MainActivity ...

以上で、地図タイルのライブラリについての解説は終わりです。
続いては地図タイルの作り方を見ていきましょう。

地図タイルを作る

地図タイルの作り方はいろいろありますが、静的なファイルを作成するか、またはサーバとして動作させるのかによっても方法が違います。
ただし、OSSベースの地図タイルの作成には共通点があります。
なお、こっから先は僕がMapServer及びGeoServerについて知らないのでそれらに関する話は全く無いのに注意してください。

まず最初に押さえておきたいプログラムがあります。
それはmapnikです。

mapnikはさまざまなデータソースから綺麗な地図画像を作るためのプログラムです。
さまざまなデータソースとは主にGDAL/OGRで読み込めるものを指します。
GDAL/OGRは多くの地図データをサポートしたライブラリです。
GDALはGeoTIFFに代表されるラスタファイルを、OGRはShapefileやGeoJSONに代表されるベクタファイルを処理するプログラムで、GDAL/OGRは一つのパッケージで提供されていて、かつGDALを使ってラスタファイルを処理するさいにも座標の変換部分はOGRの関数を使ったりと相互補完の関係にあったりします。
ついでに言うとOGRはProj4を使っていて、これらのライブラリはすべて相互補完の関係にあるような状態です。
まぁ、FOSS4Gです。

mapnikの基本的にはMapnik configuration XMLを用いてレンタリングを行います。
このファイルにはレイヤーやデータソース、描画するためのスタイルなどレンタリングに必要な情報をすべて含みます。
描画をするためのスタイルはSymbolizerというもので、例えばLineSymbolizerなら地図データ上のLineをどのような太さや色で表現するかというを決定し、PointSymbolizerなら地図データ上のPointに対して画像を貼り付けたりとかします(郵便局のマークを追加するとか)。

ただし、mapnikのXMLをすべて手動で編集するのは非常に困難です。
OpenStreetMapでは以前はXML複数のファイルに分割して管理をしていましたが、最近ではCartoCSSという仕組みを使うようになりました。

では、CartoCSSとは何者なのでしょうか?
その名の通り、スタイル情報をCSSのような形で書くことができる仕組みで、Mapbox社によって開発された概念であり、またTilemillというソフトで使われています。
CSSと違う点は、セレクタのネストが可能であったりスタイルを重ねるためのsymbolizerという仕組みがあること、変数が利用可能であること、さらに地図タイルに特化した仕様のためセレクタにズームレベルが使えるという点が大きく違います。

例えばビルを描くための定義は次のような感じになります。

@building-fill: #d6d1c8;
@building-line: darken(@building-fill, 10%);
@building-low-zoom: darken(@building-fill, 4%);

#buildings {
  [zoom >= 15] {
    polygon-clip: false;
    line-color: @building-line;
    polygon-fill: @building-fill;
    line-width: .75;
    line-clip: false;
  }
}

@から始まる変数で描画時の色を使い回したり、ズームレベルによるセレクタなどによってズームレベル15以上でなければ描画しないという表現がされています。
また、上記の例では#buildingsはレイヤーのIDを指しています。
レイヤーはproject.mmlというTilemillのプロジェクトファイルに書かれていてます。
このファイル自体はJSON形式で書かれています。
例えばbuildingsの設定は次のように書かれています。

    {
      "name": "buildings",
      "srs-name": "900913",
      "geometry": "polygon",
      "class": "",
      "srs": "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over",
      "Datasource": {
        "geometry_field": "way",
        "dbname": "gis",
        "host": "localhost",
        "user": "osm",
        "extent": "-20037508,-20037508,20037508,20037508",
        "table": "(SELECT\n    way, building\n  FROM planet_osm_polygon\n  WHERE (building IS NOT NULL)\n    AND building != 'no'\n  ORDER BY z_order, way_area DESC\n) AS buildings",
        "password": "osm",
        "type": "postgis",
        "key_field": ""
      },
      "extent": [
        -180,
        -85.05112877980659,
        180,
        85.05112877980659
      ],
      "id": "buildings",
      "advanced": {}
    },

簡単に解説するとbuildingsはpolygonの形状で、Datasourceでpostgisからtableに書かれたSELECT分で取得できた地図データを使うということになります。
Datasourceには他にもGeoJSONやShapefile、GeoTIFFファイルなどを使うことができます。

そして先ほどみていたCartoCSSファイルのうちセレクタに相当するものうち、 zoom によるセレクタがmapnik XMLの MaxScaleDenominator などの表示可能なズームを示すパラメータになります。
CartoCSSには以下のように地図データそのものに対する条件によるセレクタもあります。

  [feature = 'amenity_police'][zoom >= 16] {
    point-file: url('symbols/police.p.16.png');
    point-placement: interior;
  }

この場合はfeatureの検索条件がmapnik xmlのFilterというものになります。
実際に変換後のファイルを見てみましょう。

  <Rule>
    <MaxScaleDenominator>12500</MaxScaleDenominator>
    <Filter>([feature] = 'amenity_police')</Filter>
    <PointSymbolizer file="symbols/police.p.16.png" placement="interior" />
  </Rule>

このように複雑なmapnik XMLを手軽に編集できるのがCartoCSSの強みです。

では、実際にTilemillを使って地図のデザインをしてみましょう。
今回はとても簡単なものを扱います。

まずはデータが必要なので、簡単に扱える地球地図を使いましょう。
地球地図とはInternational Steering Committee for Global Mapping(ISCGM)によって作成されている地図データで、世界各国における地理空間を管理する人たち(日本で言うところの国土地理院)によって運営されています。
なお、国土地理院はこの委員会の事務局を担当しています。

今回使うのは地球地図日本のデータです。
2015年公開のデータの全レイヤ(gm-jpn-all_u_2_1)をダウンロードして展開します。
次にTilemillを起動して、新しいプロジェクトを作成します。
プロジェクトを作成すると最初にレイヤーが一つあるのですが、邪魔なので左下のレイヤーボタンを押してレイヤー一覧から削除して、さらにスタイルも一旦消してしまします。
ここで保存するとまっさらな地図になります。

この段階でアイコンセットを入手しておきましょう。
MapboxはMakiというアイコンセットを作成しているので、このサイトからMaki.zipのリンクをクリックして、ダウンロードしたzipファイルを展開します。
次に、TilemillのFiles & directoriesの記述を参考にTilemillのprojectディレクトリを探します。
その中に先ほど作ったプロジェクトのフォルダが入って中にはいって images というディレクトリを作成します。
次に、Maki.zipのなかのsrcフォルダにあるファイルを全て images の中にコピーします。
これで url(images/airport-24.svg); のようなCSSのURL指定で参照することができます。

次にレイヤー一覧にあるAdd Layerを押してレイヤーを追加します。
DatasourceでBrowseボタンを押して最初なのでcoastl_jpn.shpというファイルを追加します。
また、SRSの指定でWGS84を選択します。
これでSaveを押すと日本の海岸線が描画されます。

この調子で面倒ですがshapeファイルをインポートして、最後にサンプルのmssファイルに中身を置き換えてからセーブをします。

すると、次のような画面になります。

f:id:smellman:20151226052832p:plain

細かいデザインの内容については割愛します。
最後にプロジェクトの範囲を指定します。
右上の設定アイコンを押すと範囲を指定する画面になります。
まず日本の位置でズームレベル4ぐらいまで拡大したあとにShiftを押しながらドラッグをして範囲を設定します。
次に今設定した範囲内をクリックしてCenterの位置を範囲の中に収まるようにします。
最後に設定項目の真ん中ぐらいにあるzoomのスライドを動かして0〜12ぐらいにします。

f:id:smellman:20151226052934p:plain

この設定は後ほど作成するmapnik XMLに反映されるので必ず行ってください。
これで地図のデザインが完了しました。

次に実際のタイルを作成する方法ですが、いくつか方法があります。

  1. TilemillのExport機能をつかってmbtilesを出力してそれをmb-utilsを使って各画像を取り出す
  2. Tilemillのproject.mmlファイルをmapnik XMLに変換して別のプログラムから出力する

実際はTilemillも内部ではmapnikを使っている、というか気づいた方はいると思いますが、プロジェクトがセーブされるたびにmapnikを使って地図をレンタリングしていたりするので、今回は後者の方法でついでにタイルのファイル自体を作成するのではなくタイルサーバを作成しましょう。

では、さっそくmapnik XMLを作成しましょう。
Tilemillのproject.mmlからmapnik XMLを作成するにはcartoというプログラムを使います。
まずコンソールから以下のようにしてインストールをします。

npm install -g carto

さらっとnpmとか書いてますが、これはnode.jsのパッケージマネージャーです。
詳しくはnpm自体を調べてください。
とにかくこれでcartoコマンドが使えるようになります。

次にmillstoneというプログラムをインストールをします。
これは比較的新しいmapnikがDatasourceにtypeという属性が追加されてたのですが、Tilemillは開発版でないとこのオプションをサポートしていないためmapnikがエラーを吐いてしまいます。
millstoneはそういう差を吸収してくれるプログラムなのですが、これがやっかいなことにnode.jsが新しいとnode-srsのインストールでコケます(いちおうプルリクは出しておきました)。
もし、

npm install -g millstone

このコマンドでインストールできない場合は、以下の手順を取ってください。

git clone -b update_srs_version https://github.com/smellman/millstone.git
cd millstone
npm install -g

次にproject.mmlをmapnik XMLに変換します。
変換方法は以下のようにします。

carto -l project.mml > project.xml

lオプションはmillstoneを使うというものです。
これでmapnik XMLができあがるので興味がある人はproject.mml及びstyle.mssとの内容を比較してみるとよいでしょう。

あとはサーバを作るだけなのですが、今回は簡単にすますためtesseraを使います。
これはtileliveのモジュールを追加するだけでお手軽にタイルサーバが立ち上げられるプログラムです。
今回はmapnik XMLを使うので、以下のようにインストールをします。

npm install -g tessera
npm install -g tilelive-mapnik

あとはサーバを起動します。

tessera mapnik://./project.xml

デフォルトでは8080で立ち上がるので、試しにアクセスして試してみましょう。
http://localhost:8080/8/227/100.png

これで以下の画像が出てくればOKです。

f:id:smellman:20151226053033p:plain

以上で実際に地図タイルのサーバを作るところまでやりました。
最後はあっけなかったですが、これぐらい簡単にサーバが作れるのは感動ものです。

というわけで今回はここまでです。
Mapbox Vector Tileを作るところまでやりたかったんだけど、さすがに眠いので諦めます。
また、後日〜