最近公司给出了新的需求,要求用leaflet
来实现一个功能,具体需求如下:
在地图上画出一个矩形,内部会自动生成网格线,每个网格的中间生成一个图标供人点击获取经纬度,网格线大小可随输入的参数调整。
由于本人从未使用过leaflet
(虽然早早听说过它的大名),所以当前方案可能不是最优解,希望大家多多交流。
废话不多说,最后的效果如下:
下面是过程:
首先搭建好基本环境:
<button (click)="startDrawing()">开始</button>
<input #everyWidth type="text" [(value)]="itemWidth" />
<div id="leaflet"></div>
// 初始化地图,设置地理坐标和缩放等级
this.map = new Map('leaflet', {
doubleClickZoom: false, // 禁止双击来放大,以及在按住 shift 的同时双击来缩小
// zoom: 8, // 放大倍数
// center: [29.562046354387775, 106.60006989453123], // 定位坐标
});
this.map.setView([75.862046354387775, 106.60006989453123], 6); // 设置放大倍数和初始定位坐标
// 添加贴图层到地图上
let tile = new LayerGroup([
new TileLayer(
'底图链接',
{
maxZoom: 17,
minZoom: 14,
tileSize: 256,
}
),
new TileLayer(
'底图链接',
{
maxZoom: 17,
minZoom: 14,
tileSize: 256,
}
),
]);
this.map.addLayer(tile);
环境搭好之后我说一下我的思路:点击开始按钮启动鼠标的按下、松开监听,在移动的过程中进行移动监听并画出实时的矩形预览图像,最后松开的时候进行网格线和图标的绘制。
确定好思路后,开始动手吧:
map: Map;
rectangle: L.Rectangle<any>;
rectangleArr = [];
iconArr = [];
tmprec: L.Rectangle<any>;
latlngs = [];
rectanglesInfo = [];
itemWidth: number = 50;
rectangleFn = {
// 开始
onClick: (e) => {
if (typeof this.rectangle !== 'undefined') {
this.rectangle.remove();
}
if (this.rectangleArr.length !== 0) {
this.rectangleArr.forEach((item) => {
item.remove();
});
this.iconArr.forEach((item) => {
item.remove();
});
}
if (typeof this.tmprec !== 'undefined') {
this.tmprec.remove();
}
//左上角坐标
this.latlngs[0] = [e.latlng.lat, e.latlng.lng];
//开始绘制,监听鼠标移动事件
this.map.on('mousemove', this.rectangleFn.onMove);
},
// 移动
onMove: (e) => {
this.latlngs[1] = [e.latlng.lat, e.latlng.lng];
// 删除临时矩形
if (typeof this.tmprec !== 'undefined') {
this.tmprec.remove();
}
// 添加临时矩形
this.tmprec = L.rectangle(this.latlngs, { color: '#eb2214', weight: 2 }).addTo(this.map);
},
// 结束
onClickEnd: (e) => {
const endLatlngs = this.lineDraw();
// 矩形绘制完成,移除临时矩形,并停止监听鼠标移动事件
this.tmprec.remove();
this.map.off('mousemove');
// 右下角坐标
this.latlngs[1] = endLatlngs;
const rectangle = L.rectangle(this.latlngs, {
color: '#eb2214',
fillOpacity: 0,
weight: 2,
});
rectangle.addTo(this.map);
this.rectangleArr.push(rectangle);
// 调整view范围
this.map.fitBounds(this.latlngs);
this.map.off('mousedown');
this.map.off('mouseup');
this.map.dragging.enable();
},
};
...
// 点击开始按钮触发
startDrawing(): void {
this.map.on('mousedown', this.rectangleFn.onClick);
this.map.on('mouseup', this.rectangleFn.onClickEnd);
this.map.dragging.disable();
}
接下来到了重点:怎么画出网格线来。其实此“网格线”非彼“网格线”,我所画的“网格线”实际上是一个个小小的正方形,许多个正方形叠加在一起就形成了我们所看到的“网格线”了,如果有更好的思路欢迎在评论区讨论。
敲定了实现方案后,我们来解决下一个问题:由于我们鼠标移动画出的大矩形是动态且随机的,并且需求要求我们可以任意的调整正方形的宽度,所以也就会存在一个问题:外部大矩形可能不会被填满或者溢出。
对于这种问题,我的解决思路是在鼠标移动的大矩形生成前算出刚刚溢出大矩形所需要的小正方形的横竖个数,先渲染小正方形,然后用小正方形群的全局大小(此时该大小相对之前算出的必然是溢出的)去替代之前的大矩形,以此达到目的(当然这种方法算不上完美)。
确定好写法后,就开始码代码吧~
@ViewChild('everyWidth') everyWidth: ElementRef;
...
lineDraw(): number[] {
const rectangleBig = {
leftTopMeter: L.Projection.Mercator.project({ lat: this.latlngs[0][0], lng: this.latlngs[0][1] }), // 利用Leaflet自带的方法进行坐标转换
rightBottomMeter: L.Projection.Mercator.project({ lat: this.latlngs[1][0], lng: this.latlngs[1][1] }),
widthMeter: 0,
heightMeter: 0,
};
rectangleBig.widthMeter = rectangleBig.rightBottomMeter.x - rectangleBig.leftTopMeter.x;
rectangleBig.heightMeter = rectangleBig.leftTopMeter.y - rectangleBig.rightBottomMeter.y;
this.itemWidth = this.everyWidth.nativeElement.value; // 获取输入的正方形的宽度,默认50米
this.itemWidth = Number(this.itemWidth);
const xNum = Math.ceil(rectangleBig.widthMeter / this.itemWidth); // 确定横向个数
const yNum = Math.ceil(rectangleBig.heightMeter / this.itemWidth); // 确定纵向个数
for (let i = 0; i < yNum; i++) {
for (let j = 0; j < xNum; j++) {
const startLatMeter = rectangleBig.leftTopMeter.y - i * this.itemWidth;
const startLonMeter = rectangleBig.leftTopMeter.x + j * this.itemWidth;
const endLatMeter = startLatMeter - this.itemWidth;
const endLonMeter = startLonMeter + this.itemWidth;
const centerLatMeter = startLatMeter - this.itemWidth / 2;
const centerLonMeter = startLonMeter + this.itemWidth / 2;
const startLatlngs = L.Projection.Mercator.unproject({ x: startLonMeter, y: startLatMeter } as L.Point);
const endLatlngs = L.Projection.Mercator.unproject({ x: endLonMeter, y: endLatMeter } as L.Point);
const centerLatlngs = L.Projection.Mercator.unproject({ x: centerLonMeter, y: centerLatMeter } as L.Point);
const icon = L.icon({
iconUrl: '../../assets/person.png',
iconSize: [20, 20],
});
const iconItem = L.marker([centerLatlngs.lat, centerLatlngs.lng], { icon }).addTo(this.map); // 正方形中心点位的图标生成
this.iconArr.push(iconItem);
const rectangle = L.rectangle(
[
[startLatlngs.lat, startLatlngs.lng],
[endLatlngs.lat, endLatlngs.lng],
],
{
color: '#eb2214',
fillOpacity: 0,
weight: 2,
}
);
rectangle.addTo(this.map);
this.rectangleArr.push(rectangle);
if (i + 1 === yNum && j + 1 === xNum) {
return [endLatlngs.lat, endLatlngs.lng];
}
}
}
}
大功告成!最后奉上源代码一份:
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Map, LayerGroup, TileLayer } from 'leaflet';
import * as L from 'leaflet';
@Component({
selector: 'app-leaflet',
templateUrl: './leaflet.component.html',
styleUrls: ['./leaflet.component.less'],
})
export class LeafletComponent implements OnInit {
@ViewChild('everyWidth') everyWidth: ElementRef;
map: Map;
rectangle: L.Rectangle<any>;
rectangleArr = [];
iconArr = [];
tmprec: L.Rectangle<any>;
latlngs = [];
rectanglesInfo = [];
itemWidth: number = 50;
rectangleFn = {
// 开始
onClick: (e) => {
if (typeof this.rectangle !== 'undefined') {
this.rectangle.remove();
}
if (this.rectangleArr.length !== 0) {
this.rectangleArr.forEach((item) => {
item.remove();
});
this.iconArr.forEach((item) => {
item.remove();
});
}
if (typeof this.tmprec !== 'undefined') {
this.tmprec.remove();
}
//左上角坐标
this.latlngs[0] = [e.latlng.lat, e.latlng.lng];
//开始绘制,监听鼠标移动事件
this.map.on('mousemove', this.rectangleFn.onMove);
},
// 移动
onMove: (e) => {
this.latlngs[1] = [e.latlng.lat, e.latlng.lng];
// 删除临时矩形
if (typeof this.tmprec !== 'undefined') {
this.tmprec.remove();
}
// 添加临时矩形
this.tmprec = L.rectangle(this.latlngs, { color: '#eb2214', weight: 2 }).addTo(this.map);
},
// 结束
onClickEnd: (e) => {
const endLatlngs = this.lineDraw();
// 矩形绘制完成,移除临时矩形,并停止监听鼠标移动事件
this.tmprec.remove();
this.map.off('mousemove');
// 右下角坐标
this.latlngs[1] = endLatlngs;
const rectangle = L.rectangle(this.latlngs, {
color: '#eb2214',
fillOpacity: 0,
weight: 2,
});
rectangle.addTo(this.map);
this.rectangleArr.push(rectangle);
// 调整view范围
this.map.fitBounds(this.latlngs);
this.map.off('mousedown');
this.map.off('mouseup');
this.map.dragging.enable();
},
};
constructor() {}
ngOnInit(): void {
this.initMap();
}
initMap(): void {
// 初始化地图,设置地理坐标和缩放等级
this.map = new Map('leaflet', {
doubleClickZoom: false, // 禁止双击来放大,以及在按住 shift 的同时双击来缩小
// zoom: 8, // 放大倍数
// center: [29.562046354387775, 106.60006989453123], // 定位坐标
});
this.map.setView([75.862046354387775, 106.60006989453123], 6); // 设置放大倍数和初始定位坐标
// 添加贴图层到地图上
let tile = new LayerGroup([
new TileLayer(
'底图链接',
{
maxZoom: 17,
minZoom: 14,
tileSize: 256,
}
),
new TileLayer(
'底图链接',
{
maxZoom: 17,
minZoom: 14,
tileSize: 256,
}
),
]);
this.map.addLayer(tile);
}
startDrawing(): void {
this.map.on('mousedown', this.rectangleFn.onClick); // 点击地图
this.map.on('mouseup', this.rectangleFn.onClickEnd);
this.map.dragging.disable();
}
lineDraw(): number[] {
const rectangleBig = {
leftTopMeter: L.Projection.Mercator.project({ lat: this.latlngs[0][0], lng: this.latlngs[0][1] }),
rightBottomMeter: L.Projection.Mercator.project({ lat: this.latlngs[1][0], lng: this.latlngs[1][1] }),
widthMeter: 0,
heightMeter: 0,
};
rectangleBig.widthMeter = rectangleBig.rightBottomMeter.x - rectangleBig.leftTopMeter.x;
rectangleBig.heightMeter = rectangleBig.leftTopMeter.y - rectangleBig.rightBottomMeter.y;
this.itemWidth = this.everyWidth.nativeElement.value;
this.itemWidth = Number(this.itemWidth);
const xNum = Math.ceil(rectangleBig.widthMeter / this.itemWidth);
const yNum = Math.ceil(rectangleBig.heightMeter / this.itemWidth);
for (let i = 0; i < yNum; i++) {
for (let j = 0; j < xNum; j++) {
const startLatMeter = rectangleBig.leftTopMeter.y - i * this.itemWidth;
const startLonMeter = rectangleBig.leftTopMeter.x + j * this.itemWidth;
const endLatMeter = startLatMeter - this.itemWidth;
const endLonMeter = startLonMeter + this.itemWidth;
const centerLatMeter = startLatMeter - this.itemWidth / 2;
const centerLonMeter = startLonMeter + this.itemWidth / 2;
const startLatlngs = L.Projection.Mercator.unproject({ x: startLonMeter, y: startLatMeter } as L.Point);
const endLatlngs = L.Projection.Mercator.unproject({ x: endLonMeter, y: endLatMeter } as L.Point);
const centerLatlngs = L.Projection.Mercator.unproject({ x: centerLonMeter, y: centerLatMeter } as L.Point);
const icon = L.icon({
iconUrl: '../../assets/person.png',
iconSize: [20, 20],
});
const iconItem = L.marker([centerLatlngs.lat, centerLatlngs.lng], { icon }).addTo(this.map);
this.iconArr.push(iconItem);
const rectangle = L.rectangle(
[
[startLatlngs.lat, startLatlngs.lng],
[endLatlngs.lat, endLatlngs.lng],
],
{
color: '#eb2214',
fillOpacity: 0,
weight: 2,
}
);
rectangle.addTo(this.map);
this.rectangleArr.push(rectangle);
if (i + 1 === yNum && j + 1 === xNum) {
return [endLatlngs.lat, endLatlngs.lng];
}
}
}
}
}