いきなり日曜大工

好きなことを好きなだけやる

Leaflet で地図以外の画像を扱う #2 L.TileLayer

地図タイルの試作

地図タイルの仕組み

L.TileLayer で地図でない画像を扱う練習をするにあたり、できれば本物の地図でないタイルのサンプルセットが欲しかったんですが、そういうものをすぐ拾える心当たりがなかったので自分でタイルを作ってみました。

Leaflet は Google Maps モデルの地図タイル をそのまま使えるので 256×256 ピクセルのタイルをズームレベル0からズームレベルが1つあがるたびに縦横が2倍に拡大されるようにデータを作成します(実際には大きい画像から縮小しますけどね)。今回は 1024×1024 ピクセルの画像を用意してズームレベル0~2のタイルを作成しました。

こうして作ったタイルを L.TileLayer で読み込むには最初の引数で指定する URL がタイルのファイル配置と一致している必要があります。

L.tileLayer('tiles/{z}/{x}/{y}.png',

z はズームレベル、x, y はタイルの座標です。タイルの座標とは

各タイルの上に書いてある (x, y) で、横方向が x, 縦方向が y になります。
タイル作成者としては、例えばズームレベル2 の (1, 3) のタイルは

tiles/2/1/3.png

として配置することになります。ちなみに絶対にこのディレクトリ構成でということはなく、この程度のタイルファイル数なら z_x_y.png として1つのディレクトリに全部のタイルを保存して L.TileLayer 側では

L.tileLayer('tiles/{z}_{x}_{y}.png', 

と指定するのもありです。辻褄があっていれば {z} {x} {y} で指定したファイルパスに応じて必要なファイルを Leaflet が読み込んでくれます。

L.TileLayer

L.CRS.Simple + L.TileLayer

それでは L.CRS.Simple で読み込んでみます。L.ImageOverlay のときと同様に座標情報を詳しく教えてくれるドラッグ可能なマーカーもつけました。

Leaflet Demo – L.TileLayer #1
var map = L.map('map', {
    crs: L.CRS.Simple,
    maxZoom: 2
});
map.setView(L.latLng(-256/2, 256/2), 0);

L.tileLayer('tiles/kumamon/{z}/{x}/{y}.png', {
    attribution: '<a href="http://kumamon-official.jp/">©2010熊本県くまモン</a>'
}).addTo(map);

マーカーを動かして座標を確認してみると、前回の L.ImageOverlay で画像上のピクセル座標が左上 [0, 0] から右下 [width, height] になるように画像を配置したときと同じです。前回のは別にこれを狙ったわけではなかったんですけど、やっぱりこの方がいいってことなんじゃないの。

L.ImageOverlay との緯度経度の違いですが、あちらは画像を描画する領域をそもそも L.LatLngBounds で指定するので [lat, lng] はその指定の通りでしたが、L.TileLayer の場合は画像全体が収まるズームレベル0 のタイルが基準になるのでこのタイルのピクセルサイズ、デフォルトのままであれば 256×256 が基準になり [lat, lng] はズームレベル0のときのタイルの左上が [0, 0]、右下が [-256, 256] になります。

画像の領域と中心のとり方

前回の L.ImageOverlay のときは画像を描画する領域が L.LatLngBounds なので、画像の領域情報はその各メソッドを使えば簡単に、例えば中心の緯度経度座標は bounds.getCenter() でとれていました。

では L.TileLayer で描画した画像の中心はというと、[lat, lng] = [-256/2, 256/2] と言いたいところですがタイルと実際の画像領域との違いを考慮する必要があります。先に自作したタイルはタイルサイズと画像(をズームレベル0のタイルに収まるように縮小した)サイズがぴったり合っているので [-256/2, 256/2] が中心になりましたが、例えばズームレベル0のタイルがこんなタイルだったらタイル全体の中心と画像領域の中心は同じではなくなってしまいます。

このタイルの元画像は 1024×800 ピクセルの横に長い温泉くまモン(下の方の黒塗り以外の部分)で、それをそのまま左上から 256×256 ピクセルのタイルに切り分けると 256×256 ピクセルに足りない部分は無理やり 256×256 に表示されて縦方向に伸びて表示されてしまうため、正方形になるように下の方に黒背景をパディングして 1024×1024 ピクセルに仕立ててからタイルサイズにカットしています。

L.ImageOverlay の方は最初から指定領域すなわち画像領域ですが、L.TileLayer はあくまで正方形タイルのサイズが基準なので現実はこうなる(タイルに合わせるために何らかの補正処理を行う)ケースの方が多いんじゃないでしょうか。そもそもこのタイルの仕組みを考えた Google こそ、メルカトルな世界地図を正方形のタイルに収めるために地図の上下を 南北の緯度85度ぐらいで切っちゃった という経緯(地図だけに…)があります。

さてこれで [lat, lng] = [-256/2, 256/2] を map の中心に指定すると、タイルの中心が指定されているため温泉くまモンは地図の中央よりも上にずれています。
(わかりやすさのためパディング部分と地図背景をあえて違う色にしています)

Leaflet Demo – L.TileLayer #2
var map = L.map('map', {
    crs: L.CRS.Simple,
    maxZoom: 2
});
var bottomright = L.latLng(-256, 256);         // タイルの右下
var center = L.latLng(-256/2, 256/2);          // タイルの中央
map.setView(center, 0);

L.tileLayer('tiles/kumamon2/{z}/{x}/{y}.png', {
    attribution: '<a href="http://kumamon-official.jp/">©2010熊本県くまモン</a>'
}).addTo(map);

var pointer = L.marker(bottomright, {
    draggable: true
}).addTo(map).bindPopup();

ここで L.Map の unproject メソッド を使い、ピクセル座標から緯度経度座標を参照。この温泉くまモンは元のサイズが 1024×800 ピクセルで、ズームレベル2 のときに元のサイズと同じになるので

var width = 1024, height = 800;
var bottomright = map.unproject([width, height], 2);

これでズームレベル2 のときに画像のピクセルが [width, height] であるポイント、つまり画像領域の右下隅のピクセル座標に対応する緯度経度座標 L.LatLng が得られます(画像とタイルの左上の位置が合っていることが前提です)。これを使って画像領域の L.LatLngBounds を作り、L.ImageOverlay のときと同じように L.LatLngBounds のメソッド を使って画像領域の情報を扱えるようにします。

var imageBounds = L.latLngBounds(
    [0, 0],                                  // 画像領域の左上(北西)
    map.unproject([width, height], 2)        // 画像領域の右下(南東)
);

作った領域から imageBounds.getCenter() で中心の緯度経度座標をとって地図の中央に設定すると温泉くまモンを中央に表示させることができました。パディングした色と地図の背景色も合わせるといい感じに。領域を作っておくと maxBounds も簡単に設定できます。

Leaflet Demo – L.TileLayer #3
var width = 1024, height = 800;
var map = L.map('map', {
    crs: L.CRS.Simple,
    maxZoom: 2
});
var imageBounds = L.latLngBounds(
    [0, 0],                                            // 画像領域の左上(北西)
    map.unproject([width, height], 2)                  // 画像領域の右下(南東)
);
map.setView(imageBounds.getCenter(), 0);               // 中央
map.setMaxBounds(imageBounds.pad(0.5));

L.tileLayer('tiles/kumamon2/{z}/{x}/{y}.png', {
    attribution: '<a href="http://kumamon-official.jp/">©2010熊本県くまモン</a>'
}).addTo(map);

var pointer = L.marker(imageBounds.getSouthEast(), {   // 右下(南東)
    draggable: true
}).addTo(map).bindPopup();

あとは前回やった L.ImageOverlay のピクセル座標を左上→右下に設定したときと同じやり方でピクセル指定の描画ができると思います。

タイル画像の拡大縮小表示

ここまで画像から作成したタイルはズームレベル0~2のものだったので、0を最小(指定してないけどデフォルト)、2を最大ズームレベルに指定してきましたが、それよりもズームを上げ下げする方法もあるのでれんしゅう。

Leaflet Demo – L.TileLayer #4
var map = L.map('map', {
    crs: L.CRS.Simple
});
map.setView(map.unproject([1024/2, 1024/2], 2), 0);

L.tileLayer('tiles/kumamon/{z}/{x}/{y}.png', {
    minNativeZoom: 0,
    minZoom: -1,
    maxNativeZoom: 2,
    maxZoom: 4,
    attribution: '<a href="http://kumamon-official.jp/">©2010熊本県くまモン</a>'
}).addTo(map);

デフォルトでは対応するタイルが存在しないズームの分は表示されなくなるんですが、L.TileLayer のオプションにそれぞれ maxNativeZoom, minNativeZoom を指定しておくことであとは設定した maxZoom, minZoom まで Leaflet が拡大縮小表示をしてくれます。これは引き延ばしたり縮めて表示してくれるだけで画質がどうにかなるわけではありません。

大雑把ではありますがとりあえずで考えていたレベルで Leaflet で画像を扱えるようになったかなと思います。本番はこの L.TileLayer と地図タイルで行く予定なので、次回から本番用の地図タイル制作にとりかかる予定です。

ところでくまモンですが、事前に確認したところなんと個人のブログで 利用OK でした。今回は 「(5)個人で楽しむ事を目的に自分で作ったくまモンのマスコットタイル等を、自分のブログなどに掲載する場合」、ということで。