Leaflet

一個開源的 JavaScript 函式庫
用於行動裝置友善的互動式地圖

← 教學


本教學假設您已閱讀Leaflet 類別繼承的理論

在 Leaflet 中,「圖層」是指任何在地圖移動時會跟著移動的東西。在了解如何從頭開始建立圖層之前,先解釋如何進行簡單的擴展會比較容易。

「擴展方法」

一些 Leaflet 類別具有所謂的「擴展方法」:用於為子類別編寫程式碼的進入點。

其中一個是 L.TileLayer.getTileUrl()。每當需要知道要載入哪個圖片時,L.TileLayer 都會在內部呼叫此方法。藉由建立 L.TileLayer 的子類別並改寫其 getTileUrl() 函式,我們可以建立自訂行為。

讓我們用一個自訂的 L.TileLayer 來示範,它將顯示來自 PlaceKitten 的隨機小貓圖片

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 元素

另一個擴展方法是 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() );

如果元素必須進行一些非同步初始化,則使用第二個函式參數 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。像素原點(紅色)始終從左上角開始。當地圖平移時,像素原點會移動(地圖窗格相對於地圖的容器重新定位),並且在縮放時保持在螢幕上的相同位置(地圖窗格不會重新定位,但圖層可能會重新繪製自己)。當縮放時,到像素原點的絕對像素坐標會更新,但在平移時不會更新。請注意,每次地圖放大時,絕對像素坐標(到綠色括號的距離)都會加倍。

若要定位任何東西(例如,藍色的 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);
	}
});