本教程假设您已阅读 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.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 元素
另一个扩展方法是 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 元素。简而言之
L.Map
容器具有“地图窗格”,它们是<div>
。L.Layer
是地图窗格内的 HTML 元素- 地图将所有
LatLng
转换为地图 CRS 中的坐标,然后转换为绝对“像素坐标”(CRS 的原点与像素坐标的原点相同) - 当
L.Map
准备好(具有中心LatLng
和缩放级别)时,左上角的绝对像素坐标将成为“像素原点” - 每个
L.Layer
根据像素原点和图层LatLng
的绝对像素坐标从其地图窗格偏移 - 像素原点在
L.Map
上的每次zoomend
或viewreset
事件后都会重置,每个L.Layer
都必须重新计算其位置(如果需要) - 在平移地图时,像素原点不会重置;相反,整个窗格将重新定位。
这可能有点难以理解,因此请考虑以下说明性地图
查看此独立示例。 |
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>
绘制几何图形。
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);
}
});