ここのことはなかったことにするかもしれない

仕事がらみの記事を主として扱いますが、あくまで個人ブログです。2013年以前の記事は https://yellow-73.hatenablog.com/ にあります。

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 はそれぞれ東西方向、南北方向の番号です。

ラベルの問題

ラベルは、ポイントデータをアンカーにして、指定した属性の値から文字列を描くものです。アンカーは中心に置いたり左上隅においたりいろいろできますが、面倒くさいのでここでは中心とします。

そのまま素直に地図タイル画像を並べてみると、次のようなことになります。

f:id:boiledorange73:20191106140436p:plain
タイルにすると地名が途中で切れる例

日本橋大伝馬町日本橋小伝馬町のあたりが途中で切れていると思います。

ラベルは、ポイントデータでポイントされる位置に文字列を描いています。このポイントが右タイルに属する場合は左タイルには属さないので、左タイルには影響を与えません。逆もまたしかり。

これが問題で、ポイントが右タイルに属していても、ラベル描画範囲が左タイルにまではみ出すことはよくありますが、これを描いてくれません。「影響を与えない」でなく「影響を与えてくれない」と言ったほうが良いでしょう。

ポイント位置に赤丸を載せてみましょう。

左タイルは次の通りです。

f:id:boiledorange73:20191106140611p:plain
左側のタイル

次は右タイル。

f:id:boiledorange73:20191106140650p:plain
右タイル

日本橋小伝馬町」あたりが分かりやすいかと思います。右タイルに赤丸が載っているので右タイルには「日本橋小伝馬町」の一部が描画されています。左タイルには赤丸が載ってきていなくて、全く描画されません。

解決方法

マージンをとって大きな地図画像を生成し、その中心を切り出せば解決します。

上の例でいくと、左タイルの右方向に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が無い場合です。

f:id:boiledorange73:20191106140817p:plain
tile_map_edge_bufferが無い場合

続いて、tile_map_edge_bufferに256を指定した場合です。

f:id:boiledorange73:20191106140853p:plain
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を設定しなかった場合は次のようになります。

f:id:boiledorange73:20191106140954p:plain
tile_map_edge_bufferを設定しなかった場合

tile_map_edge_bufferに256を設定した場合は次のようになります。

f:id:boiledorange73:20191106141016p:plain
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を指定することでラベルの問題を避けることは可能です。

ただし、次の問題があります。

  • レイヤごとにMapServerにとって空間参照系が分かるようにしないといけない。
  • tile_map_edge_bufferはmode=tileと指定したときのみ。
  • tile_map_edge_bufferは上下左右同じマージンを取るので、地名等横長が予定されている場合に上下のマージンを詰めるといったことができない。地名の場合、縦に256ピクセルはまずありえない。
  • tile_map_edge_bufferを指定するとsvgが出ない (tile_map_bufferを指定しない場合は出る)。

最後に

ここまで書いといてなんですけど、自作のPHPスクリプトを今後も使う予定です。ズコー。