いきなり日曜大工

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

Leaflet で地図以外の画像を扱う #1 L.ImageOverlay

Leaflet で画像を扱う方法

まずは Leaflet で地図ではない画像を扱う方法を学びます。

基礎知識

画像をベースにした地図

ドキュメント類にざっと目を通した感じだと、ゲームマップのような画像を Leaflet で地図のように扱うには、L.ImageOverlay を使って画像1枚を拡大縮小させる方法と、画像から Google Maps モデルの地図タイル を生成して L.TileLayer でズーム表示させる方法があるようです。

Leaflet にはこういう用途のときのための CRS(座標参照系)として L.CRS.Simple が用意されていて、緯度経度をシンプルな x, y におきかえてくれるとのこと。これで画像をシンプルな直交座標上にあるものとして扱えるようになります。

L.ImageOverlay

L.CRS.Simple + L.ImageOverlay

実際に見てみます。画像を扱っている チュートリアル を参考に L.CRS.Simple と L.ImageOverlay を使って画像を表示させ、ドラッグできるマーカーをつけてポップアップに緯度経度 (L.LatLng) を表示させてみました。

東海道五十三次 – 箱根 (歌川広重)

Leaflet Demo – L.ImageOverlay #1
var image = {
    url:    'images/hiroshige_hakone.jpg',
    width:  4307,
    height: 2820
};

var imageBounds = L.latLngBounds(
    [0, 0],
    [image.height/16, image.width/16]
);

var map = L.map('map', {
    crs: L.CRS.Simple,
    maxBounds: imageBounds.pad(0.5)
});
map.fitBounds(imageBounds);

L.imageOverlay(image.url, imageBounds).addTo(map);

L.ImageOverlay は第1引数が貼り込む画像のURL、第2引数が画像を描画する領域 L.LatLngBounds で、画像の上でマーカーを動かしてみると実際に画像の左下が [lat, lng] = [0, 0] 、右上が [2820/16, 4307/16] (≒ [176, 269]) であるのが確認できます。

これだけでも地図っぽく使えそうですが、上記チュートリアルや L.ImageOverlay の API Reference を読んだだけではまだよくわからないところがあるのでもうちょっと詳しく観察することに。

ピクセル座標の参照

今度はマーカーを動かしたときに L.Map の project メソッドを使って緯度経度に対応するピクセル座標を参照したデータもつけ、デバッグコンソールにはズームレベルを変えた時の画像の表示ピクセルサイズ(小数点以下切り上げ)を出力させてみました。

[x, y] はそのズームレベルのときのピクセル座標、[X, Y] はズームレベルに関係なく元画像サイズを基準にしたピクセル座標です。さっきは [lat, lng] でしたが、[lng, lat] になっているのにご注意。

Leaflet Demo – L.ImageOverlay #2

デバッグコンソールの出力はこんな感じ。

zoom level: -1, image size:  135 x   89        // minZoom
zoom level:  0, image size:  270 x  177        // bounds に指定したサイズ
zoom level:  1, image size:  539 x  353
zoom level:  2, image size: 1077 x  705
zoom level:  3, image size: 2154 x 1410
zoom level:  4, image size: 4307 x 2820        // 画像のサイズ
zoom level:  5, image size: 8614 x 5640        // maxZoom

ズームレベル0 のとき、bounds に指定した領域のサイズと画像の表示ピクセルサイズは同じです。ズームレベル 4 のときに画像のサイズと同じになるのは、bounds が画像サイズを 2^4 で割ったものだから。それを超えると、画像が元のサイズより拡大されます。

これがゲーム地図だったとして、ここに地物をのせていくときのことを考えると画像処理ソフトで画像を扱うときと同じように画像の左上のピクセル座標が [0, 0] で右下が [width, height] だったらいろいろ便利そうです。例えば画像処理ソフトの方で地物のアイコンなどを作ってベースの画像上に試し置きしたらそのピクセル座標がそのまま使えたりとか。

左上が [0,0]、右下が [width, height] のピクセル座標になるように配置してみる

というわけで試しに元画像のピクセル座標 [X, Y] が左上 [0, 0] で右下が [width, height] になるバージョンも作成。

Leaflet Demo – L.ImageOverlay #3
var image = {
    url:    'images/hiroshige_hakone.jpg',
    width:  4307,
    height: 2820
};
var minZoom = -1, maxNativeZoom = 4, maxZoom = 5;

var map = L.map('map', {
    crs: L.CRS.Simple,
    minZoom: minZoom,
    maxZoom: maxZoom
});

var imageBounds = L.latLngBounds([
    map.unproject([0, image.height], maxNativeZoom),
    map.unproject([image.width, 0], maxNativeZoom)
]);

map.fitBounds(imageBounds);
map.setMaxBounds(imageBounds.pad(0.5));

L.imageOverlay(image.url, imageBounds).addTo(map);

これで元画像のサイズを基準に、今度は L.Map の unproject メソッドを使って逆にピクセル座標から緯度経度座標への変換をすることで画像処理ソフト上のような座標感覚で描画位置をピクセルで指定できるようになりましたが、ピクセルで指定するたびに

map.unproject([300,  200], maxNativeZoom)

とかしなければいけないのがちょっと面倒なので、フィルタ関数を用意して

L.polyline(point2latLng(
    [[300, 200], [600, 700], [1000, 300], [1800, 1500], [2700, 900], [4250, 2400]]
), {
    color: '#ff00db',
    weight: 5
}).addTo(map);

L.circle(point2latLng([1300, 2100]), {
    radius: 300/Math.pow(2, maxNativeZoom),    // 2^maxNativeZoom で割る
    color: '#00ffdd',
    weight: 5
}).addTo(map);

とでもすると見やすい感じに。

チュートリアル で作者が意図した座標の扱いとはちょっと違うものになっちゃいましたが、私の考える用途だとこの方がよさそうなんですよね。

地図上に画像を overlay する場合

本来の使い方?の地図の上に画像を overlay するやり方も一応演習としてやっておきます。

アメリカの政府系機関である U.S. Geological Survey 様から New York City は Manhattan Island 付近の 1:50,000 地質図をダウンロードさせて頂きました。

ダウンロードした地質図 の四隅に緯度経度が書かれているので、北西(左上)と南東(右下)の緯度経度を60進数から10進数に変換してそのまま bounds の定義に使っただけです。地質図部分は自分でカットしました。

Leaflet Demo – L.ImageOverlay #4

地図に貼った場合は bounds 領域にマッピングされた画像が地図のズームに合わせて一緒に拡大縮小されています。

var image_url = 'images/newyork_geologic.jpg';
var bounds = L.latLngBounds(
    [40.875, -74.04166666],
    [40.66666666, -73.875]
);

var map = L.map('map', {
    zoom: 10,
    center: bounds.getCenter()
});

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    opacity: 0.5,
    attribution: '<a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

L.imageOverlay(image_url, bounds, {
    attribution: '<a href="https://www.usgs.gov/">USGS</a>'
}).addTo(map);

2019/8/27 サンプルのコードを一部改訂しました(内容に変更はありません)
2018/11/1 一部サンプルにバグがあったので修正しました