MapServerはタイル画像を生成し、かつラベルの問題を避けることができる
Advent Calendarに今年も参加
これは、FOSS4G Advent Calendar 2013 ( http://atnd.org/events/45511 ) のための記事です。
MapServerが好きで、地図タイル画像に興味がある人には受けるかと思いますが、いったいどれだけの需要があるのか正直分かりません。
はじめに
MapServer 6.2 ( http://mapserver.org ) だけで地図タイル画像を作ります。
地図タイル画像作成で最も困るのが、地名等のラベルが途中で途切れることです。この問題に対しては、上下左右にマージンをもたせて描画することで解決します。
これをMapServer単体でできるのか、という話です。
地図タイル画像についておさらい
地図画像を、あらかじめ決めてある位置、サイズで格子状に切り分けたものです。タイルごとの画像サイズは、256*256ピクセルがよく使われます。
サーバ側では本当にタイル画像だけを置いておくだけで、クライアント側で表示範囲の調整等を行います。
私が地図タイル画像を初めて見たのは Google Maps ですが、実際に考え付いた人は存じません。
地図タイル画像は Z, X, Y で特定されます。Zはズーム番号です。X, Y はそれぞれ東西方向、南北方向の番号です。
ラベルの問題
ラベルは、ポイントデータをアンカーにして、指定した属性の値から文字列を描くものです。アンカーは中心に置いたり左上隅においたりいろいろできますが、面倒くさいのでここでは中心とします。
そのまま素直に地図タイル画像を並べてみると、次のようなことになります。
日本橋大伝馬町、日本橋小伝馬町のあたりが途中で切れていると思います。
ラベルは、ポイントデータでポイントされる位置に文字列を描いています。このポイントが右タイルに属する場合は左タイルには属さないので、左タイルには影響を与えません。逆もまたしかり。
これが問題で、ポイントが右タイルに属していても、ラベル描画範囲が左タイルにまではみ出すことはよくありますが、これを描いてくれません。「影響を与えない」でなく「影響を与えてくれない」と言ったほうが良いでしょう。
ポイント位置に赤丸を載せてみましょう。
左タイルは次の通りです。
次は右タイル。
「日本橋小伝馬町」あたりが分かりやすいかと思います。右タイルに赤丸が載っているので右タイルには「日本橋小伝馬町」の一部が描画されています。左タイルには赤丸が載ってきていなくて、全く描画されません。
解決方法
マージンをとって大きな地図画像を生成し、その中心を切り出せば解決します。
上の例でいくと、左タイルの右方向に256ピクセルのマージンを設定した場合、右タイルに属するポイントも描画対象となり、日本橋小伝馬町あたりも描画されます。そのうえで左タイルに属する領域のみを切り出せば、小伝馬町も描画された左タイルができあがります。
これについては、id:yellow_73:20111122 あたりに記述しています。また、「日本一の地図システムの作り方」を参照すると、このトピック以外の話もあり、網羅的でいいかも知れません。
某所の地図タイル画像サービスでは、MapServを別プロセスで実行させ、マージン込みの地図画像描画を行い、GDでマージンを外して中身をくりぬく、という処理を行うPHPスクリプトを書いて使用しています。
MapServer単体でやる利点?
私が仮定した利点は、ベクタグラフィックのくりぬきができるPHPエクステンションが発見できていなくて、MapServerには多分あるだろうからそちらにまかせてしまおう、というところ。
PHPスクリプトを作ってしまってたので、それ以外はあまり魅力を感じません。そして結果から言うと、仮定した利点はありませんでした。
ただ、PHPスクリプトを作っていない場合には、面倒な地理的範囲をピクセルに変換する計算等が不要になるので、そのぶんだけでも助かるのではないかと思います。
MapServerのタイルモード
とりあえず本家を読むと良いと思う。
http://mapserver.org/output/tile_mode.html
必須条件
- PROJ.4は必須
- レイヤごとにPROJECTIONブロックを入れろ
となっています。
たぶんWebメルカトルへの投影変換が行われるからだろうと思います。今回はPostGISで、ジオメトリカラムに空間参照系を与えているので、なくても問題なしでした。が、空間参照系が分からないデータ形式では、PROJECTIOHN必須になると思われます。
URL
URLのパラメータで次のように指定すると、タイルモードになります。
mode=tile&tilemode=gmap&tile={X}+{Y}+{Z}
- mode=tile でタイルモードであることを示します。
- tilemode=gmap でタイルを特定するパラメータの書式が決まります。
- Virtual Earthモードもあります(タイルの命名規則が違います)が、今回は無視。
- tile={X}+{Y}+{Z} でタイルインデックスを指定します。Zはズームの略です。また、"+"となっていますが、空白が"+"に置き換えられています。http://tools.ietf.org/html/rfc1866#section-8.2.1 参照。
なお、gmapモードでのタイルの座標系は、左上隅を原点に取る左手系の座標系です。対してgdal2tiles.py等のデフォルトは左下隅を原点にする右手系です。XはそのままですがYについては(2^Z - Y - 1)で相互に変換しなければなりません。
設定ファイルを作成
マップファイルは次の通りです。なお次の前提があります。前提が異なる場合は適宜マップファイルを変更して下さい。
- PostGISのデータベース ksj 内に g_aza というテーブルがある
- g_aza のうち、ジオメトリカラムが the_geom で、aname という字・町丁目名カラムがある。
- g_aza.the_geom の空間参照系は EPSG:4612
なお、ここでの結果は、国交省国土政策局発行の字・町丁目レベル位置参照情報をもとにしています。
MAP # デバッグ用設定 CONFIG "MS_ERRORFILE" "./tmp/ms_error.txt" DEBUG 5 WEB METADATA "tile_map_edge_buffer" "256" END END IMAGETYPE png PROJECTION "init=epsg:4326" # JGD2000 END FONTSET ./fonts.txt #-------- レイヤー -------- LAYER NAME "AzaName" STATUS ON TYPE ANNOTATION # フィーチャー自体は表示しない CONNECTIONTYPE postgis # 接続文字列は状況にあわせて変更する CONNECTION "dbname=ksj host=127.0.0.1" DATA "the_geom from g_aza" # データセットのうち aname という名前のカラムを表示する LABELITEM "aname" CLASS LABEL TYPE TRUETYPE # TRUETYPE または BITMAP COLOR 51 51 102 # 336 FONT gothic ENCODING UTF-8 POSITION CC PARTIALS TRUE SIZE 10 END END MAXSCALEDENOM 50000 # デバッグをオンにします(ログに書き込みます) DEBUG ON END END
fonts.txtファイルは次の通りです。
gothic /usr/local/share/font-sazanami/sazanami-gothic.ttf
ただし、sazanamiを入れている場合です。より良いフォントがあれば、そちらを指定してください。
解説
…ま、解説というほどでもないのですが。
tile_map_edge_buffer で、マージンを付けられます。ただし、上下左右同じピクセル数になります。
WEB METADATA "tile_map_edge_buffer" "256" END END
ためしに実行してみる
MapServerはCGIとして使われますが、QUERY_STRING="..." を指定してコマンドラインから実行する手があります。デバッグに有効です。
CGIとして実行する場合はhttpdプロセスのユーザがファイルを作成しようとするので、上記のようにログを取りたいときに特定のディレクトリのパーミッションを開けたりする必要があったり、データベース接続文字列に user が必要となったり、いろいろ面倒です。コマンドラインから実行すると自ユーザの権限で実行するようになります。
次のようなかんじでコマンドラインからタイル画像を作成できます。
mapserv QUERY_STRING="map=tile-pgsql.map&mode=tile&layer=AzaName&tilemode=gmap&tile=29106 12902 15" | tail +3 > out.png
なお、CGI用のプログラムを作った方ならお分かりかと思いますが、MapServer は、ヘッダを書き出しています。"tail"は ヘッダ+区切りの空行 を消すためです。
結果
gmapモードで Z=15,(X,Y)=(29106,12902)のタイルを見てみましょう。左下原点の場合はZ=15,(X,Y)=(29106,19865)です。
まず、tile_map_edge_bufferが無い場合です。
続いて、tile_map_edge_bufferに256を指定した場合です。
tile_map_edge_bufferを設定した方は「日本橋小伝馬町」のうち「日本橋小」まで描かれていることが分かります。左タイルに属していないポイントなのに描画されていますね。
描画されるラベルにポイントも表示してみる
上に出した「赤丸付き」のやつもついでに出力してみましょう。
MAP # デバッグ用設定 CONFIG "MS_ERRORFILE" "./tmp/ms_error.txt" DEBUG 5 WEB METADATA "tile_map_edge_buffer" "256" END END IMAGETYPE png PROJECTION "init=epsg:4326" # JGD2000 END FONTSET ./fonts.txt #-------- レイヤー -------- LAYER NAME "AzaName" STATUS ON TYPE POINT # フィーチャー自身を表示する(ここ変更してます) CONNECTIONTYPE postgis # 接続文字列は状況にあわせて変更する CONNECTION "dbname=ksj host=127.0.0.1" DATA "the_geom from g_aza" # データセットのうち aname という名前のカラムを表示する LABELITEM "aname" CLASS LABEL TYPE TRUETYPE # TRUETYPE または BITMAP COLOR 51 51 102 # 336 FONT gothic ENCODING UTF-8 POSITION CC PARTIALS TRUE SIZE 10 END # 小さい円を描く STYLE SYMBOL "circle" # 後続のSYMBOLブロック参照 COLOR 255 0 0 SIZE 8 END END MAXSCALEDENOM 50000 # デバッグをオンにします(ログに書き込みます) DEBUG ON END # 直径1の円 SYMBOL NAME "circle" TYPE ellipse POINTS 1 1 END FILLED true ANCHORPOINT 0.5 0.5 END END
tile_map_edge_bufferを設定しなかった場合は次のようになります。
tile_map_edge_bufferに256を設定した場合は次のようになります。
tile_map_edge_bufferを設定しなかった場合には、赤丸のある箇所のみ地名が描かれ、tile_map_edge_bufferを設定した場合には、赤丸がない箇所の地名も描かれていることが分かるかと思います。
なお、赤丸があっても地名が描かれていない箇所がありますが、これは MapServer がラベルが重なる場合には描画しないようにしているためです。LABELブロック内に "FORCE true" を入れておくと、強制描画してくれます。
もうちょっと踏み込む
tile_map_edge_bufferが効くのはmode=tileのときのみ
MapServerは、いくつかの実行モードがあります。
タイルモード(mode=tile)の場合はtile_map_edge_bufferは効きましたが、他はどうでしょう?
ソースを見ると、どうもmode=tileのときのみ有効なようです。
tile_map_edge_bufferを指定するとsvg出力はできない
タイル自体はsvgで出るのですが、エッジバッファをつけると、ビットマップのみ対応となります。
たぶん、クロッピングの実装でひっかかったんだろうと思います。Cairoは描画はできるけど読み取りはできないですから。
まとめ
MapServerでタイル画像を生成でき、tile_map_edge_bufferを指定することでラベルの問題を避けることは可能です。
ただし、次の問題があります。