本文主要探讨了如何利用leaflet-draw插件在地图上绘制图形,以及通过leaflet-measure测量距离和面积,并将经纬度绘制到地图上。首先,我们使用leaflet-draw插件,该插件提供了一种简单而直观的方式来绘制各种形状(如点、线、多边形等)到地图上。然后,我们利用leaflet-measure插件,该插件可以测量地图上任意两点之间的距离,以及任意多边形的面积。最后,我们将经纬度数据绘制到地图上,以便于进行地理位置分析和可视化。这种方法为地理信息的收集、分析和可视化提供了一种有效的工具。
绘制图形
npm i leaflet-draw
简单使用
import '@luomus/leaflet-draw/dist/leaflet.draw';
import '@luomus/leaflet-draw/dist/leaflet.draw.css';
const drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
const drawControl = new L.Control.Draw({});
map.addControl(drawControl);
配置项说明
在L.Control.Draw当中可以添加一下配置项
配置项 | 说明 |
---|---|
position | 所处位置,取值: |
draw | 绘制时配置 |
edit | 编辑时配置 |
其中在draw当中可以对点线面矩形圆灯进行对应的配置,部分配置如下:
其中对于这些配置在源码当中 L.Draw.Polyline 定义了对应的这些对象,所有的配置在option当中,下面只是稍微列举了一部分,源码位置在:node_modules/@luomus/leaflet-draw/dist/leaflet.draw-src.js
const drawControl = new L.Control.Draw({
// 位置
position: 'topright',
// 绘制时候的配置
draw: {
polyline: {
shapeOptions: {
stroke: true,
color: '#3388ff',
weight: 4,
opacity: 0.5,
fill: false,
clickable: true
}
},
polygon: {
allowIntersection: false,
drawError: {
color: '#f40',
message: '请点击别的位置的点'
},
shapeOptions: {
stroke: true,
color: '#3388ff',
weight: 4,
opacity: 0.5,
fill: false,
fillColor: null,
fillOpacity: 0.2,
clickable: true
}
},
circle: {},
rectangle: {
shapeOptions: {
clickable: false
}
},
marker: {
// icon: new MyCustomMarker()
}
},
// 编辑
edit: {
featureGroup: drawnItems
}
});
同样的在插件当中还提供了一个修改绘制配置的方法setDrawingOptions,传递的是一个对象,和draw当中配置的对象是一样的。
drawControl.setDrawingOptions({
rectangle: {
shapeOptions: {
color: '#0000FF'
}
}
});
中英文转换
在上面初始化了一个绘制对象之后,会发现展示的内容都是英文的,这对我们国内使用不是很方便,所以将其改为中文。这里直接把所有的都给替换掉了,将这段插入代码内即可。是因为在插件源码当中也是定义了一个L.drawLocal = {}
然后他里面都是英文,我们在外面重新定义的会直接取进行一个覆盖。
L.drawLocal = {
draw: {
toolbar: {
// #TODO: this should be reorganized where actions are nested in actions
// ex: actions.undo or actions.cancel
actions: {
title: '取消绘图',//'Cancel drawing',
text: '取消'//'Cancel'
},
finish: {
title: '完成绘图',//'Finish drawing',
text: '完成'
},
undo: {
title: '删除最后绘制的点',//'Delete last point drawn',
text: '撤销'//'Delete last point'
},
buttons: {
polyline: '绘制一个多段线',//'Draw a polyline',
polygon: '绘制一个多边形',//'Draw a polygon',
rectangle: '绘制一个矩形',//'Draw a rectangle',
circle: '绘制一个圆',//'Draw a circle',
marker: '绘制一个标记',//'Draw a marker',
circlemarker: '绘制一个圆形标记'//'Draw a circlemarker'
}
},
handlers: {
circle: {
tooltip: {
start: '单击并拖动以绘制圆'//'Click and drag to draw circle.'
},
radius: 'Radius'
},
circlemarker: {
tooltip: {
start: '单击“地图”以放置圆标记'//'Click map to place circle marker.'
}
},
marker: {
tooltip: {
start: '单击“地图”以放置标记'//'Click map to place marker.'
}
},
polygon: {
tooltip: {
start: '单击开始绘制形状',//'Click to start drawing shape.',
cont: '单击继续绘制形状',//'Click to continue drawing shape.',
end: '单击第一个点关闭此形状'//'Click first point to close this shape.'
}
},
polyline: {
error: '<strong>错误:</strong>形状边缘不能交叉!',//'<strong>Error:</strong> shape edges cannot cross!',
tooltip: {
start: '单击开始绘制线',//'Click to start drawing line.',
cont: '单击以继续绘制线',//'Click to continue drawing line.',
end: '单击“最后一点”以结束线'//'Click last point to finish line.'
}
},
rectangle: {
tooltip: {
start: '单击并拖动以绘制矩形'//'Click and drag to draw rectangle.'
}
},
simpleshape: {
tooltip: {
end: '释放鼠标完成绘图'//'Release mouse to finish drawing.'
}
}
}
},
edit: {
toolbar: {
actions: {
save: {
title: '保存更改',//'Save changes',
text: '保存'//'Save'
},
cancel: {
title: '取消编辑,放弃所有更改',//'Cancel editing, discards all changes',
text: '取消'//'Cancel'
},
clearAll: {
title: '清除所有图层',//'Clear all layers',
text: '清除所有'//'Clear All'
}
},
buttons: {
edit: '编辑图层',//'Edit layers',
editDisabled: '无可编辑的图层',//'No layers to edit',
remove: '删除图层',//'Delete layers',
removeDisabled: '无可删除的图层'//'No layers to delete'
}
},
handlers: {
edit: {
tooltip: {
text: '拖动控制柄或标记以编辑要素',//'Drag handles or markers to edit features.',
subtext: '单击“取消”撤消更改'//'Click cancel to undo changes.'
}
},
remove: {
tooltip: {
text: '单击要删除的要素'//'Click on a feature to remove.'
}
}
}
}
};
事件
leaflet-draw插件的事件都是注册到map对象上的,到这里,可以了解一下Evented 事件
// map.fire 也就是 触发指定类型的事件。您可以选择提供一个数据对象——侦听器函数的第一个参数将包含其属性,事件可以选择性地传播到事件父级。
this._map.fire(L.Draw.Event.CREATED, {layer: layer, layerType: this.type});
也就是相当于自定义事件了,在他插件源码中相当于定义了一个map的L.Draw.Event.CREATED事件,然后在引入插件之后就可以通过map去使用这个事件了。
- 由于前面通过fire将事件传播过来,并且传递过来了一个对象,这里通过一个event去接收
- 同时也就是为什么会有event.layerType和event.layer这两个值,对于绘制不同面可以去取不同的值,拿到值之后还可以将这个layer图层给添加到前面定义好的一个drawnItems,之后的修改也是修改这里面的layer。
map.on(L.Draw.Event.CREATED, function (event) {
console.log(' =====', event);
// 作为永久存储 取这几个值给后端存着,后面再拿出来进行渲染
console.log('类型 =====', event.layerType);
console.log('坐标 =====', event.layer._latlngs || event.layer._latlng);
console.log('配置 =====', event.layer.options); // 圆的半径在配置当中
const layer = event.layer;
drawnItems.addLayer(layer);
});
事件说明
事件名 | 说明 | 反参 |
---|---|---|
L.Draw.Event.CREATED | 创建完成 | layer、layerType |
L.Draw.Event.EDITED | 编辑完成 | layers |
L.Draw.Event.DELETED | 删除 | layers |
L.Draw.Event.DRAWSTART | 开始绘制 | layerType |
L.Draw.Event.DRAWSTOP | 停止绘制 | layerType |
L.Draw.Event.DRAWVERTEX | 绘制顶点 | layers |
L.Draw.Event.EDITSTART | 开始编辑 | handler |
L.Draw.Event.EDITMOVE | 编辑移动 | layer |
L.Draw.Event.EDITRESIZE | 编辑缩放 | layer |
L.Draw.Event.EDITVERTEX | 编辑顶点 | layers、poly |
L.Draw.Event.EDITSTOP | 完成编辑 | handler |
L.Draw.Event.DELETESTART | 开始删除 | handler |
L.Draw.Event.DELETESTOP | 完成删除 | handler |
L.Draw.Event.TOOLBAROPENED | 点击了图标,触发绘制 | |
L.Draw.Event.TOOLBARCLOSED | 取消绘制触发 | |
L.Draw.Event.MARKERCONTEXT | 右键单击标记 | marker,layer,poly |
测量
用来测量某两个或多个点之间的距离以及所围成的面积。
// 先执行npm按照插件
npm i leaflet-measure
import 'leaflet-measure/dist/leaflet-measure';
import 'leaflet-measure/dist/leaflet-measure.css';
const measureControl = new L.Control.Measure({
position: 'topright',
primaryLengthUnit: 'feet',
secondaryLengthUnit: 'miles',
activeColor: '#F40',
completedColor: '#F40',
popupOptions: {className: 'leaflet-measure-resultpopup', autoPanPadding: [10, 10]}
});
measureControl.addTo(map);
插件的配置说明
属性 | 说明 |
---|---|
position | 插件按钮所处位置,左上、右下等等 |
primaryLengthUnit | 主要单位 |
secondaryLengthUnit | 次要单位 |
activeColor | 主动执行测量时渲染的地图要素的基色 |
completedColor | 测量完成之后渲染的颜色 |
popupOptions | 弹出框配置,可以额外设置class类名进行自定义样式 |
经纬度标识
可以使用插件,插件可以在github,插件:leaflet.latlng-graticule 上进行下载获取,之后将插件引入,然后通过L.latlngGraticule
进行实例化插件
// 引入刚才下载的插件js
import './plugin/leaflet.latlng-graticule';
L.latlngGraticule({
weight: '2.0',
color: '#f40',
fontColor: '#fff',
opacity: 1,
showLabel: true,
dashArray: [5, 5],
sides: ['北', '南', '东', '西'],
zoomInterval: [
{start: 2, end: 3, interval: 30},
{start: 4, end: 4, interval: 10},
{start: 5, end: 7, interval: 5},
{start: 8, end: 18, interval: 1}
]
}).addTo(map);
插件的配置
属性 | 说明 |
---|---|
weight | number 表示 |
color | 颜色值 表示经纬度线的颜色 |
fontColor | 颜色值 表示经纬度文字的颜色 |
opacity | number 表示透明度 |
showLabel | boolean 是否显示经纬度文字 |
dashArray | Array[number] 表示经纬度线的分隔程度 |
sides | Array[string] 表示四个方向展示的内容 |
zoomInterval | 展示层级 地图在第start到第end层级时,每interval度画一条经纬线 |
插件源码解析
首先可以参考一下Leaflet【三】图层组 & geoJson & 热力图 这篇文章,里面有对leaflet-heat插件的源码分析,这里同时简单看一下这个绘制经纬度的源码。
初始化对象,他是从layer基类当中继承而来。这里面有两个属性,一个是includes这个下面说一下,另外一个是options,也就是对这个插件的配置对象。
includes是Class基类当中的一个属性,是一个特殊的类属性,它将所有指定的对象合并到类中(这样的对象被称为mixins。)理解成vue2当中的混入就好了。那么这里就是通过继承L.Evented基类或L.Mixin.Events,该类获得了事件处理功能
L.LatLngGraticule = L.Layer.extend({
includes: (L.Evented.prototype || L.Mixin.Events),
options:{} // 配置,这里省略了
});
initialize初始化,这里面没什么,就是将传递过来的options配置重新设置一下,以及将配置组装给到对应的样式。
initialize: function (options) {
L.setOptions(this, options);
// 。。。。样式组装,省略
}
onAdd方法,添加图层到map上会执行,这个方法基本上都大同小异,先初始化canvas,将canvas加到html当中,然后把需要监听的事件都监听上,最后绘制。这个内容有点多,简单概括一下。
- 首先拿到canvas的宽高以及地图的当前层级,然后根据zoomInterval当中的start和end和地图层级比较拿到对应的interval
- 已当前地图层级时6为例,这个时候拿到的interval是5,那么绘制的时候会从canvas左上开始绘制,取对应的经纬度,然后往右往下进行绘制,每过了5度就绘制下一条破折线
- 并且绘制的时候将对应的文本一起绘制上去,这样也就完成了绘制
- 之后的地图移动事件会重新执行_reset方法,那么也就会重新__redraw。
onAdd: function (map) {
this._map = map;
if (!this._canvas) {
this._initCanvas();
}
map._panes.overlayPane.appendChild(this._canvas);
map.on('viewreset', this._reset, this);
map.on('move', this._reset, this);
map.on('moveend', this._reset, this);
if (map.options.zoomAnimation && L.Browser.any3d) {
map.on('zoomanim', this._animateZoom, this);
}
this._reset();
},
直接看他绘制的方法,这里的initcanvas初始化一个canvas对象,然后_reset会将canvas大小设置一下,然后执行__draw绘制方法。
__draw: function (label) {
function _parse_px_to_int(txt) {
if (txt.length > 2) {
if (txt.charAt(txt.length - 2) == 'p') {
txt = txt.substr(0, txt.length - 2);
}
}
try {
return parseInt(txt, 10);
} catch (e) {
}
return 0;
};
var self = this,
canvas = this._canvas,
map = this._map,
curvedLon = this.options.lngLineCurved,
curvedLat = this.options.latLineCurved;
if (L.Browser.canvas && map) {
var latInterval = this._currLatInterval,
lngInterval = this._currLngInterval;
// 获取canvas对象,设置对应的宽高样式等
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.lineWidth = this.options.weight;
ctx.strokeStyle = this.options.color;
ctx.fillStyle = this.options.fontColor;
ctx.setLineDash(this.options.dashArray);
if (this.options.font) {
ctx.font = this.options.font;
}
var txtWidth = ctx.measureText('0').width;
var txtHeight = 12;
try {
var _font_size = ctx.font.trim().split(' ')[0];
txtHeight = _parse_px_to_int(_font_size);
} catch (e) {
}
var ww = canvas.width,
hh = canvas.height;
var lt = map.containerPointToLatLng(L.point(0, 0));
var rt = map.containerPointToLatLng(L.point(ww, 0));
var rb = map.containerPointToLatLng(L.point(ww, hh));
var _lat_b = rb.lat,
_lat_t = lt.lat;
var _lon_l = lt.lng,
_lon_r = rt.lng;
var _point_per_lat = (_lat_t - _lat_b) / (hh * 0.2);
_point_per_lat = _point_per_lat < 1 ? 1 : _point_per_lat;
_lat_b = _lat_b < -90 ? -90 : parseInt(_lat_b - _point_per_lat, 10);
_lat_t = _lat_t > 90 ? 90 : parseInt(_lat_t - _point_per_lat, 10);
var _point_per_lon = (_lon_r - _lon_l) / (ww * 0.2);
_point_per_lon = _point_per_lon < 1 ? 1 : _point_per_lon;
if (_lon_l > 0 && _lon_r < 0) {
_lon_r += 360;
}
_lon_r = parseInt(_lon_r + _point_per_lon, 10);
_lon_l = parseInt(_lon_l - _point_per_lon, 10);
var ll, latstr, lngstr, _lon_delta = 0.5;
function __draw_lat_line(self, lat_tick) {
ll = self._latLngToCanvasPoint(L.latLng(lat_tick, _lon_l));
latstr = self.__format_lat(lat_tick);
txtWidth = ctx.measureText(latstr).width;
var spacer = self.options.showLabel && label ? txtWidth + 10 : 0;
if (curvedLat) {
if (typeof (curvedLat) == 'number') {
_lon_delta = curvedLat;
}
var __lon_left = _lon_l, __lon_right = _lon_r;
if (ll.x > 0) {
var __lon_left = map.containerPointToLatLng(L.point(0, ll.y));
__lon_left = __lon_left.lng - _point_per_lon;
ll.x = 0;
}
var rr = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_right));
if (rr.x < ww) {
__lon_right = map.containerPointToLatLng(L.point(ww, rr.y));
__lon_right = __lon_right.lng + _point_per_lon;
if (__lon_left > 0 && __lon_right < 0) {
__lon_right += 360;
}
}
ctx.beginPath();
ctx.moveTo(ll.x + spacer, ll.y);
var _prev_p = null;
for (var j = __lon_left; j <= __lon_right; j += _lon_delta) {
rr = self._latLngToCanvasPoint(L.latLng(lat_tick, j));
ctx.lineTo(rr.x - spacer, rr.y);
if (self.options.showLabel && label && _prev_p != null) {
if (_prev_p.x < 0 && rr.x >= 0) {
var _s = (rr.x - 0) / (rr.x - _prev_p.x);
var _y = rr.y - ((rr.y - _prev_p.y) * _s);
ctx.fillText(latstr, 0, _y + (txtHeight / 2));
} else if (_prev_p.x <= (ww - txtWidth) && rr.x > (ww - txtWidth)) {
var _s = (rr.x - ww) / (rr.x - _prev_p.x);
var _y = rr.y - ((rr.y - _prev_p.y) * _s);
ctx.fillText(latstr, ww - txtWidth, _y + (txtHeight / 2) - 2);
}
}
_prev_p = {x: rr.x, y: rr.y, lon: j, lat: i};
}
ctx.stroke();
} else {
var __lon_right = _lon_r;
var rr = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_right));
if (curvedLon) {
__lon_right = map.containerPointToLatLng(L.point(0, rr.y));
__lon_right = __lon_right.lng;
rr = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_right));
var __lon_left = map.containerPointToLatLng(L.point(ww, rr.y));
__lon_left = __lon_left.lng;
ll = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_left));
}
ctx.beginPath();
ctx.moveTo(1 + spacer, ll.y);
ctx.lineTo(rr.x - 1 - spacer, rr.y);
ctx.stroke();
if (self.options.showLabel && label) {
var _yy = ll.y + (txtHeight / 2) - 2;
ctx.fillText(latstr, 0, _yy);
ctx.fillText(latstr, ww - txtWidth, _yy);
}
}
};
if (latInterval > 0) {
for (var i = latInterval; i <= _lat_t; i += latInterval) {
if (i >= _lat_b) {
__draw_lat_line(this, i);
}
}
for (var i = 0; i >= _lat_b; i -= latInterval) {
if (i <= _lat_t) {
__draw_lat_line(this, i);
}
}
}
function __draw_lon_line(self, lon_tick) {
lngstr = self.__format_lng(lon_tick);
txtWidth = ctx.measureText(lngstr).width;
var bb = self._latLngToCanvasPoint(L.latLng(_lat_b, lon_tick));
var spacer = self.options.showLabel && label ? txtHeight + 5 : 0;
if (curvedLon) {
if (typeof (curvedLon) == 'number') {
_lat_delta = curvedLon;
}
ctx.beginPath();
ctx.moveTo(bb.x, 5 + spacer);
var _prev_p = null;
for (var j = _lat_b; j < _lat_t; j += _lat_delta) {
var tt = self._latLngToCanvasPoint(L.latLng(j, lon_tick));
ctx.lineTo(tt.x, tt.y - spacer);
if (self.options.showLabel && label && _prev_p != null) {
if (_prev_p.y > 8 && tt.y <= 8) {
ctx.fillText(lngstr, tt.x - (txtWidth / 2), txtHeight + 5);
} else if (_prev_p.y >= hh && tt.y < hh) {
ctx.fillText(lngstr, tt.x - (txtWidth / 2), hh - 2);
}
}
_prev_p = {x: tt.x, y: tt.y, lon: lon_tick, lat: j};
}
ctx.stroke();
} else {
var __lat_top = _lat_t;
var tt = self._latLngToCanvasPoint(L.latLng(__lat_top, lon_tick));
if (curvedLat) {
__lat_top = map.containerPointToLatLng(L.point(tt.x, 0));
__lat_top = __lat_top.lat;
if (__lat_top > 90) {
__lat_top = 90;
}
tt = self._latLngToCanvasPoint(L.latLng(__lat_top, lon_tick));
var __lat_bottom = map.containerPointToLatLng(L.point(bb.x, hh));
__lat_bottom = __lat_bottom.lat;
if (__lat_bottom < -90) {
__lat_bottom = -90;
}
bb = self._latLngToCanvasPoint(L.latLng(__lat_bottom, lon_tick));
}
ctx.beginPath();
ctx.moveTo(tt.x, 5 + spacer);
ctx.lineTo(bb.x, hh - 1 - spacer);
ctx.stroke();
if (self.options.showLabel && label) {
ctx.fillText(lngstr, tt.x - (txtWidth / 2), txtHeight + 5);
ctx.fillText(lngstr, bb.x - (txtWidth / 2), hh - 3);
}
}
};
if (lngInterval > 0) {
for (var i = lngInterval; i <= _lon_r; i += lngInterval) {
if (i >= _lon_l) {
__draw_lon_line(this, i);
}
}
for (var i = 0; i >= _lon_l; i -= lngInterval) {
if (i <= _lon_r) {
__draw_lon_line(this, i);
}
}
}
}
},