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);
	}
});