Leaflet

オープンソースのJavaScriptライブラリ
モバイルフレンドリーなインタラクティブマップ用

← チュートリアル


このチュートリアルでは、Leafletのクラス継承の理論を読んでいることを前提としています。

Leafletでは、「レイヤー」とは、マップの移動時に一緒に移動するものを指します。ゼロから作成する方法を見る前に、簡単な拡張方法を説明する方が簡単です。

「拡張メソッド」

いくつかのLeafletクラスには、「拡張メソッド」と呼ばれるものがあります。これは、サブクラスのコードを記述するためのエントリポイントです。

その1つがL.TileLayer.getTileUrl()です。このメソッドは、新しいタイルがどの画像を読み込む必要があるかを知る必要があるたびにL.TileLayerによって内部的に呼び出されます。L.TileLayerのサブクラスを作成し、そのgetTileUrl()関数を書き換えることで、カスタムの動作を作成できます。

PlaceKittenからランダムな子猫の画像を表示するカスタムL.TileLayerを例に説明しましょう。

L.TileLayer.Kitten = L.TileLayer.extend({
    getTileUrl: function(coords) {
        var i = Math.ceil( Math.random() * 4 );
        return "https://placekitten.com/256/256?image=" + i;
    },
    getAttribution: function() {
        return "<a href='https://placekitten.com/attribution.html'>PlaceKitten</a>"
    }
});

L.tileLayer.kitten = function() {
    return new L.TileLayer.Kitten();
}

L.tileLayer.kitten().addTo(map);
この例をスタンドアロンで参照してください。

通常、getTileUrl()はタイル座標(coords.xcoords.ycoords.zとして)を受け取り、それらからタイルURLを生成します。この例では、それらを無視し、ランダムな数値を使用して毎回異なる子猫を取得します。

プラグインコードの分離

前の例では、L.TileLayer.Kittenは使用されている場所と同じ場所で定義されています。プラグインの場合、プラグインコードを独自のファイルに分割し、使用時にそのファイルを含める方が良いです。

KittenLayerの場合、L.KittenLayer.jsのようなファイルを作成する必要があります。

L.TileLayer.Kitten = L.TileLayer.extend({
    getTileUrl: function(coords) {
        var i = Math.ceil( Math.random() * 4 );
        return "https://placekitten.com/256/256?image=" + i;
    },
    getAttribution: function() {
        return "<a href='https://placekitten.com/attribution.html'>PlaceKitten</a>"
    }
});

そして、マップを表示する際にそのファイルを含めます。

<html>
…
<script src='leaflet.js'>
<script src='L.KittenLayer.js'>
<script>
	var map = L.map('map-div-id');
	L.tileLayer.kitten().addTo(map);
</script>
…

L.GridLayerとDOM要素

もう1つの拡張メソッドはL.GridLayer.createTile()です。L.TileLayerが画像(<img>要素)のグリッドがあると仮定するのに対し、L.GridLayerはそれを仮定しません。あらゆる種類のHTML要素のグリッドを作成できます。

L.GridLayer<img>のグリッドを作成できますが、<div><canvas><picture>(またはその他何でも)のグリッドも可能です。createTile()は、タイル座標が与えられたHTMLElementのインスタンスを返すだけです。DOMで要素を操作する方法を知ることはここで重要です。LeafletはHTMLElementのインスタンスを期待するため、jQueryなどのライブラリで作成された要素は問題になります。

カスタムGridLayerの例として、タイル座標を<div>に表示するものがあります。これは、Leafletの内部をデバッグする場合や、タイル座標の仕組みを理解する際に特に役立ちます。

L.GridLayer.DebugCoords = L.GridLayer.extend({
	createTile: function (coords) {
		var tile = document.createElement('div');
		tile.innerHTML = [coords.x, coords.y, coords.z].join(', ');
		tile.style.outline = '1px solid red';
		return tile;
	}
});

L.gridLayer.debugCoords = function(opts) {
	return new L.GridLayer.DebugCoords(opts);
};

map.addLayer( L.gridLayer.debugCoords() );

要素が非同期初期化を行う必要がある場合は、2番目の関数パラメータdoneを使用し、タイルの準備ができたとき(たとえば、画像が完全に読み込まれたとき)、またはエラーが発生したときにコールバックします。ここでは、タイルを人為的に遅延させます。

createTile: function (coords, done) {
	var tile = document.createElement('div');
	tile.innerHTML = [coords.x, coords.y, coords.z].join(', ');
	tile.style.outline = '1px solid red';

	setTimeout(function () {
		done(null, tile);	// Syntax is 'done(error, tile)'
	}, 500 + Math.random() * 1500);

	return tile;
}
この例をスタンドアロンで参照してください。

これらのカスタムGridLayerを使用すると、プラグインはグリッドを構成するHTML要素を完全に制御できます。いくつかのプラグインはすでにこのように<canvas>を使用して高度なレンダリングを行っています。

非常に基本的な<canvas> GridLayerは次のようになります。

L.GridLayer.CanvasCircles = L.GridLayer.extend({
	createTile: function (coords) {
		var tile = document.createElement('canvas');

		var tileSize = this.getTileSize();
		tile.setAttribute('width', tileSize.x);
		tile.setAttribute('height', tileSize.y);

		var ctx = tile.getContext('2d');

		// Draw whatever is needed in the canvas context
		// For example, circles which get bigger as we zoom in
		ctx.beginPath();
		ctx.arc(tileSize.x/2, tileSize.x/2, 4 + coords.z*4, 0, 2*Math.PI, false);
		ctx.fill();

		return tile;
	}
});
この例をスタンドアロンで参照してください。

ピクセル原点

カスタムL.Layerの作成は可能ですが、LeafletがHTML要素を配置する方法に関するより深い知識が必要です。要約すると、

これは少し分かりにくいので、次の説明図を参照してください。

この例をスタンドアロンで参照してください。

CRS原点(緑)は同じLatLngに留まります。ピクセル原点(赤)は常に左上に始まります。マップをパンニングするとピクセル原点は移動します(マップペインはマップのコンテナに対して位置が変更されます)、ズームしても画面上では同じ場所に留まります(マップペインの位置は変更されませんが、レイヤーは自身を再描画することがあります)。ピクセル原点に対する絶対ピクセル座標はズーム時に更新されますが、パンニング時には更新されません。マップをズームインするたびに絶対ピクセル座標(緑色の括弧までの距離)が2倍になることに注意してください。

何か(たとえば、青いL.Marker)を配置するには、そのLatLngをマップのL.CRS内の絶対ピクセル座標に変換します。次に、ピクセル原点の絶対ピクセル座標をその絶対ピクセル座標から減算して、ピクセル原点に対するオフセット(水色)を取得します。ピクセル原点はすべてのマップペインの左上隅であるため、このオフセットをマーカーのアイコンのHTML要素に適用できます。マーカーのiconAnchor(濃い青色の線)は、負のCSSマージンによって実現されます。

L.Map.project()L.Map.unproject()メソッドはこれらの絶対ピクセル座標で動作します。同様に、L.Map.latLngToLayerPoint()L.Map.layerPointToLatLng()はピクセル原点に対するオフセットで動作します。

さまざまなレイヤーがこれらの計算をさまざまな方法で適用します。L.Markerは単にアイコンの位置を変更します。L.GridLayerはマップの範囲(絶対ピクセル座標で)を計算し、要求するタイル座標のリストを計算します。ベクトルレイヤー(ポリライン、ポリゴン、サークルマーカーなど)は各LatLngをピクセルに変換し、SVGまたは<canvas>を使用してジオメトリを描画します。

onAddonRemove

基本的に、すべてのL.Layerはマップペイン内のHTML要素であり、その位置と内容はレイヤーのコードによって定義されます。ただし、レイヤーがインスタンス化されるときにHTML要素を作成することはできません。これは、レイヤーがマップに追加されたときに実行されます。レイヤーは、それまでマップ(またはdocumentでさえ)について知りません。

言い換えれば、マップはレイヤーのonAdd()メソッドを呼び出し、レイヤーはそのHTML要素(通常は「コンテナ」要素と呼ばれる)を作成してマップペインに追加します。逆に、レイヤーがマップから削除されると、そのonRemove()メソッドが呼び出されます。レイヤーはマップに追加されるときにコンテンツを更新し、マップビューが更新されたときに位置を再配置する必要があります。レイヤースケルトンは次のようになります。

L.CustomLayer = L.Layer.extend({
	onAdd: function(map) {
		var pane = map.getPane(this.options.pane);
		this._container = L.DomUtil.create(…);

		pane.appendChild(this._container);

		// Calculate initial position of container with `L.Map.latLngToLayerPoint()`, `getPixelOrigin()` and/or `getPixelBounds()`

		L.DomUtil.setPosition(this._container, point);

		// Add and position children elements if needed

		map.on('zoomend viewreset', this._update, this);
	},

	onRemove: function(map) {
		this._container.remove();
		map.off('zoomend viewreset', this._update, this);
	},

	_update: function() {
		// Recalculate position of container

		L.DomUtil.setPosition(this._container, point);        

		// Add/remove/reposition children elements if needed
	}
});

レイヤーのHTML要素を正確に配置する方法は、レイヤーの仕様によって異なりますが、この紹介はLeafletのレイヤーコードを読み、新しいレイヤーを作成するのに役立ちます。

親のonAddの使用

いくつかのユースケースでは、onAddコード全体を再作成する必要はなく、代わりに親のコードを再利用し、その初期化の前または後(必要に応じて)にいくつかの詳細を追加できます。

例として、オプションを無視して常に赤になるL.Polylineのサブクラスを作成できます。

L.Polyline.Red = L.Polyline.extend({
	onAdd: function(map) {
		this.options.color = 'red';
		L.Polyline.prototype.onAdd.call(this, map);
	}
});