いきなり日曜大工

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

Leaflet 拡張基礎編 #4 L.Control でお好み UI

L.Control の継承

L.Handler でつけたい機能は今のところ前回の URL ハンドラだけだったので、次は L.Control のドキュメントを読んで画面に地図とやりとりするための UI をつけていきます。

雛型

公式サイトの L.Control のチュートリアル から extend() を使った “the simplest” なサンプルを引用しました。

L.Control.Watermark = L.Control.extend({
    onAdd: function(map) {
        var img = L.DomUtil.create('img');

        img.src = '../../docs/images/logo.png';
        img.style.width = '200px';

        return img;
    },

    onRemove: function(map) {
        // Nothing to do here
    }
});

L.control.watermark = function(opts) {
    return new L.Control.Watermark(opts);
}

L.control.watermark({ position: 'bottomleft' }).addTo(map);

サブクラスの作り方は L.Handler のときとほぼ同じで、extend() を使って L.Control のサブクラスを作り、メソッド onAdd と onRemove を実装します。

L.Control の仕組み

チュートリアル と API reference の L.Control から。

L.Control は地図領域の上に重ねられたコントロール表示領域(地図と一緒に動かない)に自前の DOM 要素を表示させることができるクラスで、表示させる DOM 要素と表示させる位置(地図の四隅のどれか)の情報を持ちます。

表示させる位置の選択

自前の DOM 要素を表示させる位置のベースとして、 topleft, topright, bottomleft, bottomright の地図領域の四隅のどれかを position に指定します。

上のサンプルでは bottomleft を指定しています(デフォルトは topright)。

L.control.watermark({ position: 'bottomleft' }).addTo(map);

下は Leaflet が生成する HTML の構造です。マップコンテナの <div>(1行目)が地図を描画する <div>(2~4行目)とその上に重ねられたコントロール(L.Control)表示領域の <div>(5~10行目)を内包しています。

<div id="map" class="leaflet-container ...">
  <div class="leaflet-pane leaflet-map-pane">
    (地図タイル・地物等のレイヤー)
  </div>
  <div class="leaflet-control-container">
    <div class="leaflet-top leaflet-left"></div>
    <div class="leaflet-top leaflet-right"></div>
    <div class="leaflet-bottom leaflet-left"></div>
    <div class="leaflet-bottom leaflet-right"></div>
  </div>
</div>

6~9行目の <div> はそれぞれクラス名に表される領域の四隅が基準になるようにスタイルシートで固定されていて、自前の DOM 要素は position で指定した位置の <div> の子要素になることでその位置に準じて表示されるという仕組みです。
細かい表示位置の調整はスタイルシートで行います。

onAdd

メソッド onAdd で自前の DOM 要素を構築し、それを戻り値にします。
このメソッドは L.Control を継承するクラスをインスタンス生成してそれを addTo(map) したときに呼び出され、戻り値の DOM 要素(HTMLElement)は position で指定した前述の <div> の子要素として追加されます。

サンプルでは、画像の HTMLImageElement を作ってそれを返しています。

    onAdd: function(map) {
        var img = L.DomUtil.create('img');  // HTMLImageElement

        img.src = '../../docs/images/logo.png';
        img.style.width = '200px';

        return img;
    },

生成する DOM 要素にイベントを紐づけたい場合は、ここで一緒にイベントリスナーの登録も行います。

onRemove

addTo(map) で画面に追加された DOM 要素は メソッド remove() で削除することができます。このとき自分で実装するメソッド onRemove が呼ばれ、DOM 要素の削除以外に必要な削除時の処理をすることになります。具体的には onAdd での DOM 要素生成時にイベントリスナーを登録していた場合にそのリスナーを削除したり、他に何かクリーンアップすべきことがあればここでさっぱり片づけます。

試作

インタラクティブなサンプル

チュートリアルで紹介してるサンプルは画像を表示するだけのものだったので、インタラクティブかつシンプルなものを試作。

地図上のマーカーを動かすと情報パネルのマーカーの緯度経度情報が書き換えられます。

L.CustomControl = L.Control.extend({
    onAdd: function(map) {
        this._div = L.DomUtil.create('div', 'custom-panel leaflet-bar');
        return this._div;
    },

    onRemove: function(map) {
    },

    setContent: function(latlng) {
        latlng = latlng.wrap()
        this._div.innerHTML = '<pre class="coords">'
                            + 'lat: ' + this._format(latlng.lat) + "\n"
                            + 'lng: ' + this._format(latlng.lng)
                            + '</pre>';
        return this;
    },

    _format: function(num) {
        return (('    ') + num.toFixed(7)).slice(-12);
    }
});
L.customControl = function(opts) {
    return new L.CustomControl(opts);
}

const nintendo = L.latLng(34.9697112, 135.7562166);
const map = L.map('map').setView(nintendo, 13);

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

const panel = L.customControl({ position: 'topright' })
    .addTo(map)
    .setContent(nintendo);

L.marker(nintendo, {
    draggable: true
}).on('drag', function(e) {
    panel.setContent(e.latlng);
}).addTo(map);

Leaflet Demo – L.Control #1

実際に作りたいものはイベントがごちゃごちゃになりそうな予感がしますが、こんな感じのものをベースにこれからいっぱいお世話になりそうな L.Control です。