このチュートリアルでは、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.x
、coords.y
、coords.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要素を配置する方法に関するより深い知識が必要です。要約すると、
L.Map
コンテナには「マップペイン」があり、これは<div>
です。L.Layer
はマップペイン内のHTML要素です。- マップはすべての
LatLng
をマップのCRS内の座標に変換し、そこから絶対「ピクセル座標」に変換します(CRSの原点はピクセル座標の原点と同じです)。 L.Map
の準備が整うと(中心LatLng
とズームレベルがある)、左上の絶対ピクセル座標が「ピクセル原点」になります。- 各
L.Layer
は、ピクセル原点とレイヤーのLatLng
の絶対ピクセル座標に従って、マップペインからオフセットされます。 L.Map
のzoomend
またはviewreset
イベントの後、ピクセル原点はリセットされ、すべてのL.Layer
は(必要に応じて)その位置を再計算する必要があります。- マップをパンニングしてもピクセル原点はリセットされません。代わりに、ペイン全体の位置が変更されます。
これは少し分かりにくいので、次の説明図を参照してください。
この例をスタンドアロンで参照してください。 |
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>
を使用してジオメトリを描画します。
onAdd
とonRemove
基本的に、すべての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);
}
});