相关文章推荐
活泼的柚子  ·  windows ...·  1 年前    · 
纯真的松球  ·  ef 某些字段更新 ...·  1 年前    · 
首发于 码海拾贝

Leaflet 浅窥

创建地图实例

基于 Leaflet 绘制地图时,首先需要使用 L.map(el, options) 方法创建地图实例。

const map = L.map('map', {
  center: [51.505, -0.09],
  zoom: 13,
  crs: L.CRS.EPSG3857
});

与大多数图表库相同,L.map 方法中的首参可以是 dom 节点的 id,也可以是 dom 节点本身。Leaflet 会把这个 dom 节点视为 container 容器节点。在 container 容器节点下,Leaflet 会创建各式 layer 图层。这些 layer 图层会按类型分组挂在不同的 pane 节点下,这样它们在地图上就可以不同的显示顺序(zIndex 不同)。

容器节点

在 pane 节点中,mapPane 节点会构成 container 容器的直系子节点,其内部又依次会创建 tilePane、overlayPane、shadowPane、markerPane、tooltipPane、popupPane 节点。这些节点分别承载了什么功能呢?

这些节点都作为容器节点。以 Leaflet 官方给出的 快速开始 为例,它们的功能分别为:

  • tilePane:作为瓦片图层分组的容器节点。瓦片图层由 GridLayer、TileLayer 等创建,它们构成了地图的底图。容器节点的意义在于,它会挂载 GridLayer、TileLayer 实例创建的图层节点,下同
  • overlayPane:作为矢量图层分组的容器节点。矢量图层由 Polyline、Polygon、ImageOverlay、VedioOverlay 等创建,其典型如在地图上圈选的矩形区域。上图中的圆和三角形即属于矢量图层分组,它们在 tilePane 上方,shadowPane 下方
  • shadowPane、markerPane:作为图标图层及其阴影分组的容器节点,阴影图层会在图标图层的下方。图标图层由 Marker 等创建。典型的图标图层如地图上的定位点
  • tooltipPane:作为提示图层分组的容器节点。提示图层由 Tooltip 等创建
  • popupPane:作为弹层分组的容器节点。提示图层由 Popup 等创建,其典型如点击定位点后的展示弹层

Layer 基类

这些图层(GridLayer、TileLayer、Polyline、Polygon、ImageOverlay、VedioOverlay、Marker、Tooltip、Popup 等)都是 Layer 的子类。作为基类,Layer 协调着图层创建销毁逻辑。通过 map.addLayer(layer)、layer.addTo(map) 可以添加图层,layer.addTo 底层会调用 map.addLayer;通过 map.removeLayer(layer)、layer.remove() 可以销毁图层,layer.remove 底层会调用 map.removeLayer。Layer 和 Map 实例会相互持有各自的引用,比如 map._layers 以唯一 id 为键存储着 Layer 实例;layer._map 即 Map 实例。

先来看一下 map.addLayer 的处理流程(这一方法会在 Map 实例的 load 事件中调用):

  1. 它首先会为 layer 生成一个唯一 id,并将其存入 _layers 属性中
  2. 其次会调用 layer.beforeAdd 生命周期函数。比如瓦片图层会限制 Map 实例的缩放比例;矢量图层用于从 Map 实例中获取绘制矢量的渲染器
  3. 随后会调用 layer._layerAdd 生命周期函数。第一使 Layer 实例持有 Map 实例的引用;第二为 Map 实例绑定 Layer 实例待注册的事件;第三调用 layer.onAdd 生命周期函数;第四触发 Layer 实例的 add 事件以及 Map 实例的 layeradd 事件

何为通过 Layer 实例注册到 Map 实例上的事件呢?Layer 子类可以实现 getEvents 方法,其返回值就会注册为 Map 实例的事件。注册事件典型的作用是,地图缩放时重新加载瓦片;矢量图层等根据地图一起缩放。

相应的,map.removeLayer 的处理流程包含:调用 layer.onRemove 生命周期函数;触发 Layer 实例的 remove 事件以及 Map 实例的 layerremove 事件;销毁相互持有的引用。

所以,作为 Layer 的子类,通常需要实现以下方法:

  • beforeAdd:可选,用于 Map 和 Layer 实例相互回填实例属性
  • getEvents:注册 Map 实例的 viewpreset、viewreset、zoom、moveend 事件(地图缩放平移时触发),以便地图缩放等时联动更新瓦片或矢量图层等
  • onAdd:初始化图层节点并将其渲染到对应的容器节点上(所对应的容器节点取决于 Layer 实例的 options.pane),根据地图缩放级别渲染内容
  • onRemove:销毁渲染内容并从容器节点移除

LayerGroup

Layer 子类中,较为特殊的是 LayerGroup 组合图层以及继承 LayerGroup 的 FeatureGroup。虽然 Map 可以通过 eachLayer 对地图上的所有图层进行操作,Map 也接收 options.layers 配置项用于设置多个底图,但是图层的添加和操作过程都是逐个的。通过 LayerGroup、FeatureGroup 可以实现批量操作及添加。这两个类会在 onAdd 方法的实现中再逐个添加图层。这里的图层不限于瓦片图层,也包含矢量图层、图标图层、弹层图层等。示例如官方的 组合图层及图层切换

const littleton = L.marker([39.61, -105.02]).bindPopup('This is Littleton, CO.');
const denver = L.marker([39.74, -104.99]).bindPopup('This is Denver, CO.');
const aurora = L.marker([39.73, -104.8]).bindPopup('This is Aurora, CO.');
const golden = L.marker([39.77, -105.23]).bindPopup('This is Golden, CO.');
const cities = L.layerGroup([littleton, denver, aurora, golden]);
cities.bindPopup('Hello world!')
  .on('click', function() { alert('Clicked on a member of the group!'); })
  .addTo(map);

LayerGroup 实现了 eachLayer、invoke 等方法,以操纵图层或调用图层的方法。FeatureGroup 在 LayerGroup 基础上实现了 addLayer、removeLayer、bringToFront、bringToBack 等方法。

事件通信

Map 和 Layer 都继承了 Evented 事件模块,两者通过事件机制加以通信 ,比如 Map 告知 Layer 地图缩放了,Layer 告知 Map 瓦片加载完了。这在下文介绍缩放平移等操作时会有更详细的体现,这里只做简单介绍。

map.setView 方法可以调整地图的缩放级别以及中心点,在这个过程中,它会触发 viewpreset 预重置、zoomstart 缩放前、movestart 移动前、zoom 缩放、move 移动、zoomend 缩放后、moveend 移动后、viewreset 重置、load 加载(当地图初始化时触发)。通过 Layer 子类实现 getEvents 方法,返回诸如 { zoom, move } 对象并注册为 Map 实例的事件,Layer 实例就可以监听到 Map 的异动,相应触发自身的动作。比如,GridLayer 实现的 getEvents 就会向 Map 实例注册 viewprerest、viewreset、zoom、moveend、move、zoomanim 事件。

以上事件可以通过主动调用 map.setView 触发。这里简要地说明下用户交互行为触发的事件是怎样实现的?leaflet 首先抽象了 Handler 类,该类用于操控事件的绑定和解绑流程。子类会通过实现 addHooks、removeHooks 向 dom 元素绑定或解绑事件。同时,子类中也会实现各种事件绑定函数,比如 Map.Drap.js 就实现了拖拽事件的绑定。

创建瓦片图层

L.tileLayer('https://somedomain.com/blabla/{z}/{x}/{y}{r}.png?{foo}', {
  foo: 'bar', 
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

创建瓦片图层的方式如上。L.tileLayer 方法的首参为请求地图瓦片的模板,x、y、z、r、foo 等值都会实际发送请求时被替换掉。其中,x、y 分别代表坐标系中的横纵轴位置(像素单位);z 代表地图的缩放级别;r 用于在 url 中添加 '@2x',以适配 retina 分辨率;foo 会被替换为次参选项中的内容。

TileLayer 继承自 GridLayer,它会基于创建 img 节点加载地图服务提供商的瓦片图资源。比如官方 快速开始 示例就会请求这些资源:

这里简要地介绍下地图瓦片的由来。为了在浏览器中使用地图,作为解决方案的 WMS(Web Map Service)在 1999 年应运而生了。它的主要设想是在服务器端把地图渲染成图片,然后由浏览器进行渲染。每次基于浏览器视窗大小由后端服务绘制单一的一张图片,这也构成了 wms 的主要缺点:计算能力、网络传输。随之而来的就是,WMS-C(cached)通过缓存地图瓦片提升效率。瓦片(Tile)的大小通常为 256*256 像素。根据金字塔规则,当 zoom = 0 时,瓦片只有 1 张;zoom = 1 时,瓦片有 4 张;zoom = 2 时,瓦片有 16 张;以此类推。对于瓦片编号,互联网厂商会有不同的设计,比如谷歌和高德都采用左上角作为 x、y 轴的起始点,zoom 从 0 开始;百度采用经纬度为 0 的位置作为原点,zoom 从 1 开始( 瓦片地图原理 中有较详细介绍)。下图即基于谷歌 XYZ,瓦片编号为 Z/X/Y:

瓦片加载

Leaflet 是怎么实现地图瓦片的加载的呢?因为实际展示的地图只会加载整张瓦片地图的一部分,所以地图瓦片的加载包含两个子问题:加载哪些地图瓦片?怎么加载这些地图瓦片?

加载哪些地图瓦片的主要处理逻辑可以参考 Leaflet 中 GridLayer#_update 方法的实现:

  1. map.getCenter 方法获取中心点的经纬度坐标。
    1. 通过 map.getSize 方法就可以获取 container 容器的像素尺寸
    2. 容器尺寸折半就是中心点距离容器角点的像素坐标(Leaflet 将其称为 containerPoint)
    3. 通过 containerPointToLayerPoint 方法将 containerPoint 转化为 layerPoint。当 container 容器中的 mapPane 节点发生平移时,中心点距离当前展示瓦片角点的像素坐标(Leaflet 将其称为 layerPoint)需要 containerPoint 扣除平移产生的距离(或者是 transform 转换的值,或者是 position 定位的值)
    4. 通过 map.layerPointToLatLng 方法将像素坐标转换成经纬度坐标。layerPoint 是中心点距离当前展示瓦片的像素坐标,而当前展示瓦片可能是瓦片地图的一部分。map._pixelOrigin 记录着当前展示瓦片距离整个瓦片地图的像素值,由此可以获知中心点距离整个地图瓦片原点的像素坐标,通过 map.project 可以将它转换成经纬度坐标
  2. 获取展示地图的像素边界。中心点的经纬度坐标通过 map.project(latlng, zoom) 方法可以转换成像素坐标。有了中心点的像素坐标和地图的像素尺寸,就可以获取地图的像素边界
  3. 获取当前展示瓦片编号的 XY 边界。有了地图的像素边界,再通过地图瓦片的尺寸(即上文所说通常为 256*256 像素),就可以获取瓦片编号中的 X、Y 值(即上图中的 0/0、1/0、0/1、1/1)
  4. 根据当前展示瓦片地图的边界,创建待加载瓦片编号队列。如地图瓦片的边界为 0/1、1/0,就会分别加载 1/0/0、1/1/0、1/0/1、1/1/1 这四张瓦片。根据当前展示瓦片地图的大小,加载的瓦片不限于 4 张
  5. 对于瓦片队列编号,它们会从距离瓦片中心点最近的开始加载。对于每一张瓦片,GridLayer 会调用 GridLayer#_addTile 创建 tile 节点并完成挂载。tile 节点的创建依赖于 GridLayer 子类中 createTile 方法的实现,比如 TileLayer#createTile 就会创建 img 节点加载地图瓦片

基于上述,怎么加载地图瓦片的问题就转变成 Leaflet 会在什么时候调用 GridLayer#_update 方法。GridLayer#onAdd 执行过程中会调用 GridLayer#_update 方法,以便在图层初次添加到地图上时加载瓦片;当 Map 实例的 zoom 等事件触发时,也会调用 GridLayer#_update 方法,以便在地图调整缩放级别时加载瓦片。

瓦片图层的顶层节点为 leaflet-layer,它会挂载在 tilePane 容器节点下。leaflet-layer 节点下是多个 leaflet-tile-container 节点。leaflet-tile-container 节点内是加载地图瓦片的 img 节点。

坐标转换

平面地图坐标和经纬度的转换最常使用球面墨卡托投影(CRS.EPSG3857)。墨卡托投影也称为等角度投影,即从地心向经纬度坐标点射出一条线,其与圆柱面的交点就是像素坐标点;圆柱面与赤道线相切。所以,从经纬度到平面地图坐标的换算即为:x = R * λ;y = R * tanφ。其中,R 为地球半径;λ 为经度在赤道线上的弧度;φ 为射线与其在赤道平面的投影线的角度。球面墨卡托投影的优点是保持方位的不变,所以较长用于制作航海图;缺点是会拉伸靠近两极的区域,极点在柱面投影上的 y 值接近无穷。为此,工程上采用 y = R * ln(tan(π/4 + Φ/2)) 计算 y 坐标。计算公式即如 瓦片地图原理 理解墨卡托投影原理 所说,如下图:

Leaflet 中应用球面墨卡托投影转换经纬度和像素坐标的代码见于 SphericalMercator 中:

var earthRadius = 6378137;
export var SphericalMercator = {
	R: earthRadius,
	MAX_LATITUDE: 85.0511287798,// 极限维度
  // 经纬度转平面地图坐标
	project: function (latlng) {
		var d = Math.PI / 180,
		    max = this.MAX_LATITUDE,
		    lat = Math.max(Math.min(max, latlng.lat), -max),
		    sin = Math.sin(lat * d);
		return new Point(
			this.R * latlng.lng * d,
			this.R * Math.log((1 + sin) / (1 - sin)) / 2);
  // 平面地图坐标转经纬度
	unproject: function (point) {
		var d = 180 / Math.PI;
		return new LatLng(
			(2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d,
			point.x * d / this.R);
	bounds: (function () {
		var d = earthRadius * Math.PI;
		return new Bounds([-d, -d], [d, d]);
};

经过这样计算后,实际得到的 x、y 坐标会比地球表面积还大,实际地图可没这么大。Leaflet 会通过 Transformation 转换工具类将地图转变成 1 单位大小,然后再通过把地图扩展成瓦片地图大小(取决于地图瓦片的尺寸以及缩放比例 zoom)。像素坐标也会按此计算。

除了球面墨卡托投影外,Leaflet 内部实现了椭球面投影、等距柱面投影等。我们可以通过 L.map 方法的选项 options.crs 切换坐标系,比如从默认的 CRS.EPSG3857( 球面墨卡托投影)切换成 CRS.Simple,以处理游戏地图。当然,也可以基于 L.CRS.Base 创建其他 CRS 对象。CRS 对象会持有相应的 Projection 投影对象,用于处理经纬度和像素坐标的相互转换,如 CRS.EPSG3857 就会持有上文所说的 SphericalMercator 投影对象。基于 Transformation 转换工具类以及 Projection 投影对象,每一个 CRS 对象都会有 latLngToPoint、pointToLatLng 方法用于实现经纬度和瓦片地图像素坐标的转换。

map 地图实例可以通过 this.options.crs 访问到当前使用的坐标系,也就持有了经纬度和瓦片地图像素坐标的转换方法。实际上,通过 map.project(latlng, zoom)、map.unproject(point, zoom) 就可以实现经纬度和瓦片地图像素坐标的相互转换。

  • project(latlng, zoom):经纬度坐标转换成像素坐标
  • map.unproject(point, zoom):像素坐标转换成经纬度坐标

缩放平移

地图操作免不了缩放和平移。Leaflet 是怎样处理的呢?Leaflet 提供了 map.setView(center, zoom, options) 方法用于设置中心点并进行缩放,设置中心点即平移。有了 map.setView 之后,map.setZoom(zoom, options)、map.zoomIn(delta, options)、map.zoomOut(delta, options) 方法会根据当前中心点进行缩放;map.setZoomAround(latlng, zoom, options) 方法会根据指定点进行缩放,也就是将中心点平移到指定点。

在设置缩放之先,Leaflet 会基于 options.minZoom(最小缩放限制)、options.maxZoom(最大缩放限制)以及 options.zoomSnap(支持的分数级别)调整实际使用的缩放比例。实际设置的中心点也会基于 options.maxBounds(最大边界)进行调整(如果当前展示的地图边界超过最大边界,设置的中心点和最大边界内允许的中心点会有偏移)。

在 map.setView 方法调整中心点或缩放比例的过程中,Map 实例会通过事件机制触发 viewprereset、viewreset、zoomstart、movestart、zoom、move、zoomend、moveend 等事件。上文已有提及,Map 实例会将 viewprerest、viewreset、zoom、moveend、move、zoomanim 事件通知给 Layer 实例。Layer 订阅这些事件后,会做出以下动作:

  • viewprereset 事件:移除所有 tile
  • viewreset、zoom 事件:调用 GridLayer#_resetView 重绘瓦片地图
  • moveend 事件:调用 GridLayer#_update 重绘瓦片地图

瓦片制作

地图瓦片的制作涉及到大计算任务。这里仅贴出部分参考文档:

绘制矢量图层

Leaflet 支持的矢量图层包含 Polyline 线、Polygon 多边形、Rectangle 矩形、Circle 圆。这些矢量图层基于 L.Path 实现,L.Path 又继承了 Layer。渲染这些矢量图层的方式是大同小异的,下方代码即展示了多边形的制作:

  1. 提供矢量图形的经纬度信息 latlngs
  2. 使用 L.polyline、L.polygon、L.rectangle、L.circle 创建矢量图形
  3. 通过 layer.addTo(map) 将矢量图形添加到地图上
const latlngs = [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]];
const polygon = L.polygon(latlngs, {color: 'red'}).addTo(map);
// 将地图缩放到多边形区域
map.fitBounds(polygon.getBounds());

矢量图形

矢量图形的绘制基于 svg 或 canvas,Leaflet 内部将其称为 renderer 渲染器。如下方式可以切换全局渲染器或局部渲染器:

const map = L.map('map', {
    renderer: L.canvas()
const map = L.map('map');
const myRenderer = L.canvas({ padding: 0.5 });
const line = L.polyline( coordinates, { renderer: myRenderer } );
const circle = L.circle( center, { renderer: myRenderer } );

L.Svg、L.Canvas 均是 L.Renderer 的子类。前者借助 svg 技术 ,不适用于 Android 2.x 或 3.x 以及 IE7、IE8(Leaflet 会针对 IE7、IE8 切换使用 VML )。后者借助 canvas 技术 ,也并非所有浏览器都适用。渲染技术虽然不同,Leaflet 适用这两种技术渲染的主流程还是同一的。

首先是在 L.Path 的生命周期中:

  1. beforeAdd:它会使 L.Path 实例持有 _renderer 指向渲染器
  2. onAdd:
    1. 执行 L.Svg、L.Canvas 等渲染器实现的 _initPath,会使 L.Renderer 实例持有 Layer 实例,分别以唯一 id 作为 key 键存储在 renderer._layers 属性中。对于 svg,还会创建 layer._path,即 path 节点
    2. 执行 L.Polyline、L.Circle 等矢量图层实现的 _project,计算绘制线段或矩形时所需的 layer._rings、绘制圆所需的 layer._point 及 layer._radius 等,以及像素边界 layer._pxBounds
    3. 执行 L.Polyline、L.Circle 等矢量图层实现的 _update,其承载的职责就是更新矢量图形(svg 节点即更新 path 节点的属性,canvas 即调用 CanvasRenderingContext2D 接口绘制图形)。不过在处理 L.Polyline 时,Leaflet 会基于渲染器的有效尺寸裁剪掉不必渲染的内容(layer._rings 会转化成 layer._parts),以提升性能
    4. 执行 L.Svg、L.Canvas 等渲染器实现的 _addPath,svg 会尝试创建 svg 节点,并将 path 节点以 g 节点包裹添加 svg 节点下,canvas 为通过 requestAnimationFrame 进行重绘
  3. onRemove:svg 为移除 path 节点;canvas 即从链表中删除内容并加以重绘(canvas 通过链表存储待绘制的内容,从链表中清除即为删除,每次绘制都会重绘所有当前边界内的图形)

其次 L.Svg、L.Canvas 均提供了 _updatePoly、_updateCircle 接口,用于绘制线段、多边形、矩形或圆。各 L.Path 的 _updatePath 方法均会对接这些接口。

地理数据

latlngs

Leaflet 内部表示矢量图形的方式是 latlngs。L.polyline、L.polygon、L.rectangle、L.circle 接收的首参均是 latlngs。比如:

  1. 线段
// 单条线段
const latlngs = [
  [45.51, -122.68],
 	[37.77, -122.43],
 	[34.04, -118.2]
// 多条线段
const latlngs = [
    [45.51, -122.68],
    [37.77, -122.43],
    [34.04, -118.2]
    [40.78, -73.91],
    [41.83, -87.62],
    [32.76, -96.72]
];
  1. 多边形
// 带洞的多边形
const latlngs = [
  [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring
  [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole
// 多个多边形
const latlngs = [
  [ // first polygon
    [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring
    [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole
   [ // second polygon
     [[41, -111.03],[45, -111.04],[45, -104.05],[41, -104.05]]
];
  1. 矩形及圆
// 矩形
const bounds = [[54.559322, -5.767822], [56.1210604, -3.021240]]
// 圆心,L.circle([50.5, 30.5], {radius: 200}).addTo(map);
const point = [50.5, 30.5]

GEOJSON

这是 Leaflet 内部的数据格式,纬度在前,经度在后。另外的地理空间数据格式还有 GeoJSON 、WKT。

标准的 GeoJSON 数据格式包含三个属性:

  • type:"Feature" 表示一个空间边界实体;"FeatureCollection" 表示空间边界实体的集合
  • geometry:空间边界实体的实际形状描述
  • properties:空间边界实体的属性

geometry 包含两个属性:

  • type:Point、MultiPoint 点;LineString、MultiLineString 线;Polygon、MultiPolygon 多边形
  • coordinates:坐标
"geometry": {
  "type": "Point",
  "coordinates": [102.0, 0.5]
"geometry": {
  "type": "MultiPoint",
  "coordinates": [
    [102.0, 0.5],
    [102.0, 0.5],
},
  1. 线段
"geometry": {
  "type": "LineString",
  "coordinates": [[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]]
"geometry": {
  "type": "MultiLineString",
  "coordinates": [
    [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0],
    [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]
}
  1. 多边形
"geometry": {
  "type": "Polygon",
  "coordinates": [[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]
"geometry": {
  "type": "MultiPolygon",
  "coordinates": [
    [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0],
    [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0],
},

在 leaflet 中,Polyline、Polygon、Rectangle、Circle 都会实例方法 toGeoJSON 用于转换数据格式。

此外,GeoJSON 类用于将 GeoJSON 数据格式转换为矢量图形。比如:

L.geoJSON(data, {
  style: function (feature) {
  	return {color: feature.properties.color};
}).bindPopup(function (layer) {
  return layer.feature.properties.description;
}).addTo(map);

WKT

WKT 是一种文本标记语言,可用于表示矢量几何对象。比如:

POINT(6 10)
MULTIPOINT(3.5 5.6, 4.8 10.5)
LINESTRING(3 4,10 50,20 25)
MULTILINESTRING((3 4,10 50,20 25),(-5 -8,-10 -8,-15 -4))
POLYGON((1 1,5 1,5 5,1 5,1 1),(2 2,2 3,3 3,3 2,2 2))
MULTIPOLYGON(((1 1,5 1,5 5,1 5,1 1),(2 2,2 3,3 3,3 2,2 2)),((6 3,9 2,9 4,6 3)))
GEOMETRYCOLLECTION(POINT(4 6),LINESTRING(4 6,7 10))

借助 wicket ,可实现 leaflet 数据、GeoJSON 数据与 WKT 格式数据的相互转换。

绘制图标弹层

绘制图片、视频、svg

leaflet 集成图片、视频、svg 图层绘制功能的意义在于,随着地图的缩放,这些图层也会跟着缩放(通过实现 getEvents 绑定 Map 的 zoom、viewrest 事件完成同步缩放)。这两个功能分别由 ImageOverlay、VedioOverlay、SvgOverlay 提供。ImageOverlay 继承 Layer;VedioOverlay、SvgOverlay 继承 ImageOverlay。

// 图片
const imageUrl = 'https://maps.lib.utexas.edu/maps/historical/newark_nj_1922.jpg';
const imageBounds = [[40.712216, -74.22655], [40.773941, -74.12544]];
L.imageOverlay(imageUrl, imageBounds).addTo(map);
// 视频
const videoUrl = 'https://www.mapbox.com/bites/00188/patricia_nasa.webm';
const	videoBounds = [[ 32, -130], [ 13, -100]];
L.videoOverlay(videoUrl, videoBounds ).addTo(map);
// svg
const svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgElement.setAttribute('xmlns', "http://www.w3.org/2000/svg");
svgElement.setAttribute('viewBox', "0 0 200 200");
svgElement.innerHTML = '<rect width="200" height="200"/><rect x="75" y="23" width="50" height="50" style="fill:red"/><rect x="75" y="123" width="50" height="50" style="fill:#0013ff"/>';
const svgElementBounds = [ [ 32, -130 ], [ 13, -100 ] ];
L.svgOverlay(svgElement, svgElementBounds).addTo(map);

绘制图标

地图上的图标可以通过 Marker 类制作,典型如定位点。Marker 类继承了 Layer,所以也通过 onAdd、onRemove 挂载或卸载图标节点及其阴影。实际的图标可以通过 Icon 类制作,Marker 用于协调 Icon 的渲染。

const greenIcon = L.icon({
  iconUrl: 'leaf-green.png',
  shadowUrl: 'leaf-shadow.png',
  iconSize:     [38, 95], // size of the icon
  shadowSize:   [50, 64], // size of the shadow
  iconAnchor:   [22, 94], // point of the icon which will correspond to marker's location
  shadowAnchor: [4, 62],  // the same for the shadow
  popupAnchor:  [-3, -76] // point from which the popup should open relative to the iconAnchor
L.marker([51.5, -0.09], {icon: greenIcon}).addTo(map);
// 拖拽功能默认关闭,可手动开启,或者通过选项开启
marker.dragging.enable();

除了拖拽功能外,Maker 类还提供控制标记展示的一些实例方法:

  • setIcon(icon):切换图标
  • setLatlng(latlng):修改位置
  • setOpacity(opacity):修改透明度

交互弹层

Popup

Layer 类有实例方法 bindPopup、openPopup。因为瓦片图层、矢量图层、标记图层均基于 Layer,所以它们都可以通过 bindPopup 绑定交互弹层。此外,弹层也可以通过 Popup 类创建并通过 popup.openOn(map) 显示在地图上。在实现上,Popup 继承了 DivOverlay,DivOverlay 又继承了 Layer。

// 基于 Layer#bindPopup
const marker = L.marker([51.5, -0.09]).addTo(map)
  .bindPopup('A pretty CSS3 popup.<br> Easily customizable.')
  .openPopup();
// 基于创建 Popup 实例
const popup = L.popup();
function onMapClick(e) {
  popup.setLatLng(e.latlng)
    .setContent("You clicked the map at " + e.latlng.toString())
    .openOn(map);
map.on('click', onMapClick);

Tooltip

Tooltip 的实现与 Popup 类似,它也继承了 Layer。每种 Layer 可以通过 bindTooltip、openTooltip 打开提示框;leaflet 也允许使用 Tooltip 创建实例。

marker.bindTooltip("my tooltip text").openTooltip();
const tooltip = L.tooltip();
function onMapClick(e) {
  tooltip.setLatLng(e.latlng)
    .setContent("You clicked the map at " + e.latlng.toString())
    .openOn(map);
map.on('click', onMapClick);

绘制控件

在 leaflet 中,用于制作控件的基类为 Control,它并没有像 TileLayer、Polygon、Marker、Popup 那样继承 Layer。与 Map#addLayer、Map#removeLayer、Layer#onAdd、Layer#onRemove 机制相同,Map 也实现了 addControl、removeControl 等实例方法,它们会通过 control.addTo(map)、control.remove 调用到 Control 子类的 onAdd、onRemove 方法。所以,与自定义 Layer 相同,自定义 Control 可以通过实现 onAdd、onRemove 方法在地图上挂载或卸载控件的节点。Control 会依据样式类分布在地图的四个角上,分步位置可以通过选项 options.position 控制。

leaflet 内部实现了几个 Control 子类:Control.Zoom 控制缩放级别、Control.Scale 地图比例尺展示控件(示例如测绘局 叠加控件 )、Control.Layers 图层切换控件。因为 Control 实例会持有 _map 属性指向 Map 实例,所以 Control 实例能通过 Map 实例的方法操控地图。比如 Control.Zoom 会调用 map.zoomIn、map.zoomOut 进行缩放等。

以下是官网 自定义水印控件 的代码示例:

L.Control.Watermark = L.Control.extend({
  onAdd: function(map) {
    var img = L.DomUtil.create('img');
    img.src = '../../docs/images/logo.png';
    img.style.width = '200px';
    return img;
  onRemove: function(map) {
    // Nothing to do here