引言
在地理信息可视化与交互应用日益普及的今天,Leaflet 作为一款轻量级、高性能且开源的 JavaScript 地图库,受到了广大前端开发者的青睐。它提供了强大的地图绘制、交互功能,并且具有良好的扩展性和兼容性,能够轻松满足各类 Web 地图应用的需求。本文将深入探讨 Leaflet 地图的核心技术,通过实际案例和代码示例,帮助前端开发者快速掌握其应用技巧。
一、安装
npm install leaflet
二、页面引入
main.js
import * as leaflet from 'leaflet'
import 'leaflet/dist/leaflet.css'
Vue.prototype.$leaflet = leaflet
三、创建地图
<template>
<div id="map-container"></div>
</template>
四、初始化地图
initMap() {
//定义图层样式
const layer = this.$leaflet.tileLayer(
'http://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}'
)
// 创建地图
this.leafletMap = this.$leaflet.map('map-container', {
center: [28.19854, 112.8347],
zoom: 10,
minZoom: 3,
maxZoom: 14,
// zoomSnap: 1,
attributionControl: true, // 是否将 attribution 版权控件添加到地图中
zoomControl: true, // 是否将 zoom 缩放控件添加到地图中
crs: this.$leaflet.CRS.EPSG3857, // 该地图使用的坐标系。如果你不确定坐标系这是什么意思,请不要更改它。
keyboard: true, // 地图是否获得焦点,并且允许用户通过键盘和 +/- 来进行浏览地图。
keyboardPanDelta: 80, // 按下方向键时,平移的像素数量。
scrollWheelZoom: 'center', // 地图是否允许通过使用鼠标滚轮进行缩放。如果通过'center',不管鼠标在哪里,都将会放大到视图的中心。
layers: [layer] // 图层
})
},
瓦片图层
高德矢量图层:http://webrd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}
高德影像图层:http://webst0{1-4}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}
百度地图:http://online{0-3}.map.bdimg.com/onlinelabel/?qt=tile&x={x}&y={y}&z={z}&styles=pl&scaler=1&p=1
OpenStreetMap:https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
其中OpenStreetMap瓦片需要添加跨域,否则访问不了:
const layer = this.$leaflet.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
crossOrigin: true
})
- 添加图层控制
const layer = this.$leaflet.tileLayer(
'http://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}'
)
const gaodeSatellite = this.$leaflet.tileLayer(
'http://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}'
)
// 添加图层控制
const baseMaps = {
高德矢量: layer,
高德影像: gaodeSatellite
}
this.$leaflet.control.layers(baseMaps).addTo(this.leafletMap)
五、地图上绘制点、线、多边形、弹框
1、描点
// 描点
addMarker() {
this.marker = this.$leaflet
.marker([28.19854, 112.8347], {
icon: this.myIcon
})
.addTo(this.leafletMap)
},
2、画线
// 画线
addPolyline() {
const points = [
[28.19854, 112.8347],
[28.29854, 112.4347],
[28.39854, 112.6347]
]
this.$leaflet.polyline(points, { color: 'red' }).addTo(this.leafletMap)
},
3、画圆
// 画圆
addCircle() {
const popup = this.$leaflet.popup().setContent('<p style="color:green;">欢迎你</p>')
this.$leaflet
.circle([28.2908, 113.26625], 5000, {
color: 'red',
fillColor: '#f03',
fillOpacity: 0.5
})
.addTo(this.leafletMap)
.bindPopup(popup)
.openPopup()
},
4、画多边形
// 画多边形
addPolygon() {
const points = [ [28.19854, 113.8347],
[28.29854, 113.4347],
[28.39854, 113.6347]
]
this.$leaflet
.polygon(points, { color: '#aa0000', fillColor: '#ff15c9', weight: 1 })
.addTo(this.leafletMap)
},
5、pm绘制
// 安装依赖
npm install leaflet.pm
// main.js引入
import 'leaflet.pm'
import 'leaflet.pm/dist/leaflet.pm.css'
<a-radio-group v-model="drawerType" @change="drawStart">
<a-radio-button value="Circle">绘制圆形</a-radio-button>
<a-radio-button value="Rectangle">绘制矩形</a-radio-button>
<a-radio-button value="Polygon">绘制多边形</a-radio-button>
<a-radio-button class="red" v-if="drawerType" value=""> 取消绘制</a-radio-button>
</a-radio-group>
drawStart() {
if (this.drawerType) this.handleStart()
else this.handleCancel()
},
handleStart() {
this.leafletMap.pm.enableDraw(this.drawerType, {
snappable: false
})
this.getlatLngs(this.drawerType)
},
getlatLngs(drawerType) {
// pm:drawstart 开始第一个点的时候调用
// pm:drawend 禁止绘制时调用
// pm:create 创建完成时调用
// pm:remove 移除完成时调用
this.leafletMap.on('pm:drawstart', (e) => {
console.log(e, '开始第一个点的时候调用')
})
this.leafletMap.on('pm:drawend', (e) => {
console.log(e, '禁止绘制')
})
this.leafletMap.on('pm:create', (e) => {
console.log(e, '绘制完成时调用', drawerType)
// 获取被选中的设备
const polygon = e.layer
const bounds = polygon.getBounds()
let center = null // 圆心坐标
let radius = null // 单位:米
let fillMarket = this.deviceListData.filter((item) => bounds.contains(item.latlng))
console.log('图形包含选中设备:', fillMarket)
if (fillMarket.length) {
// 绘制多边形
if (drawerType === 'Polygon') {
console.log('Polygon===')
fillMarket = []
this.deviceListData.forEach((item) => {
if (this.isPointInPolygon(this.$leaflet.latLng(item.latlng), polygon)) {
fillMarket.push(item)
}
})
} else if (drawerType === 'Circle') {
// 绘制圆形
console.log('Circle===')
fillMarket = []
center = polygon.getLatLng() // 圆心坐标
radius = polygon.getRadius() // 半径
// 筛选在圆形范围内的 Marker
fillMarket = this.deviceListData.filter(
(item) => center.distanceTo(item.latlng) <= radius
)
}
console.log('fillMarket===', fillMarket)
if (fillMarket?.length > 0) this.handleSelected(fillMarket)
}
// 循环markersLayer图层上所有marker
// this.markersLayer.eachLayer((marker) => {
// // 判断marker在绘制范围内
// if (bounds.contains(marker.getLatLng())) {
// const polygonFlag =
// drawerType === 'Polygon' && !this.isPointInPolygon(marker.getLatLng(), polygon)
// const circleFlag =
// drawerType === 'Circle' && center.distanceTo(marker.getLatLng()) > radius
// if (polygonFlag || circleFlag) return
// const { alt } = marker.options
// marker.setIcon(
// this.$leaflet.icon({
// iconUrl: this.transIcons({ devType: Number(alt.split('-')[1]), status: 99 }),
// iconSize: [40, 40], // 图标大小,单位(px)
// popupAnchor: [-20, 0], // popup相对于锚点中心的坐标
// tooltipAnchor: [0, -20] // tooltip相对于锚点中心的坐标
// })
// )
// }
// })
this.cancelDraw()
})
this.leafletMap.on('pm:remove', (e) => {
console.log(e, '移除绘制时调用')
})
},
// 取消绘制
handleCancel() {
this.leafletMap.pm.disableDraw(this.drawerType)
// 移除当前绘制图像
this.leafletMap.eachLayer((layer) => {
const { _path: path } = layer
if (path) layer.remove()
})
this.drawerType = null
},
6、绘制弹框
// 绘制弹框
addPopup() {
const popup = this.$leaflet
.popup()
.setContent('<p style="color:green;">我是hzz!<br />我在长沙</p>')
this.marker.bindPopup(popup)
},
7、定义图片覆盖层
const bounds = [
[0, 0],
[600, 300]
]
this.$leaflet.imageOverlay(imgUrl, bounds).addTo(this.map)
this.map.fitBounds(bounds)
8、设置语言
this.leafletMap.pm.setLang('en') // zh
9、绘制区域颜色
-
- 获取区域数据 从datav.aliyun.com/portal/scho… 下载你需要的区域数据(json格式)
-
- 区域样式
addAreaColor() {
// 区域样式
const style = {
color: '#55ff7f', // 边框颜色
weight: 3, // 边框粗细
opacity: 0.5, // 透明度
fillColor: '#55ff7f', // 区域填充颜色
fillOpacity: 0.2 // 区域填充颜色的透明
}
const s = this.$leaflet.geoJSON(changsha, { style }).addTo(this.leafletMap)
console.log('addAreaColor===', s)
},
10、热力图
- 下载插件
npm install heatmap.js
- 页面引入
import HeatmapOverlay from 'heatmap.js/plugins/leaflet-heatmap'
- 应用
addHeartLayer() {
const testData = {
max: 8, // 最大值
data: [
{
lat: 28.39854,
lng: 113.3347,
count: 5
},
{
lat: 28.38854,
lng: 113.3447,
count: 8
},
{
lat: 28.39654,
lng: 113.3647,
count: 2
},
{
lat: 28.41854,
lng: 113.3447,
count: 7
},
{
lat: 28.52554,
lng: 113.4247,
count: 8
},
{
lat: 28.53554,
lng: 114.4447,
count: 8
},
{
lat: 28.55554,
lng: 114.4447,
count: 8
},
{
lat: 28.91554,
lng: 113.4147,
count: 8
},
{
lat: 28.88654,
lng: 113.3647,
count: 3
}
]
}
// 配置
const config = {
radius: 0.015, // 设置每一个热力点的半径
maxOpacity: 0.8, // 设置最大的不透明度
minOpacity: 0, // 设置最小的不透明度
scaleRadius: true, // 设置热力点是否平滑过渡
useLocalExtrema: false, // 使用局部极值
latField: 'lat', // 纬度
lngField: 'lng', // 经度
valueField: 'count', // 热力点的值
gradient: {
// 热力点颜色的变化范围
0.99: 'rgba(255,0,0,1)',
0.9: 'rgba(255,255,0,1)',
0.8: 'rgba(0,255,0,1)',
0.5: 'rgba(0,255,255,1)',
0: 'rgba(0,0,255,1)'
}
}
const heatmapLayer = new HeatmapOverlay(config)
heatmapLayer.setData(testData)
this.leafletMap.addLayer(heatmapLayer)
}
效果图:
六、性能优化
-
图层加载优化
懒加载:对于包含多个图层的地图,可以采用懒加载策略,只在用户需要时加载特定图层,减少初始加载时间
瓦片预加载:通过设置瓦片图层的
preload
属性,提前加载部分瓦片,提高地图浏览的流畅性const layer = this.$leaflet.tileLayer( 'http://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}', { preload: true // 开启瓦片预加载 } )
-
事件管理
合理管理地图事件,避免过多的事件监听导致性能下降
map.on('click', handler); map.off('click', handler);
-
地图渲染优化
减少图层数量:避免在地图上同时显示过多图层,根据实际需求动态切换图层。
优化图形绘制:对于复杂的图形(如多边形、折线),尽量减少顶点数量,简化图形结构,提高渲染性能。
七、常见问题与解决方案
-
地图偏移问题
在使用某些地图服务(如高德地图、百度地图)时,可能会出现地图坐标偏移的情况。这是因为这些地图服务使用了自己的坐标系统。解决方案是使用相应的坐标转换库(如
bd09-ll
、gcj02-ll
)将坐标转换为正确的经纬度坐标。npm install gcj02-ll const { wgs84togcj02, gcj02towgs84 } = require('gcj02-ll'); // 假设这是高德地图的坐标 const gcj02Point = [34.0522, 118.2437]; // 转换为 WGS-84 坐标 const wgs84Point = gcj02towgs84(gcj02Point[0], gcj02Point[1]); console.log('WGS-84 坐标:', wgs84Point);
-
移动端适配问题
优化触摸交互:调整地图的触摸事件处理逻辑,使其更适合移动端操作。
const map = L.map('map', { zoomControl: false, // 禁用默认的缩放控件 dragging: false, // 禁用默认的拖动行为 touchZoom: false, // 禁用默认的触摸缩放行为 doubleClickZoom: false // 禁用默认的双击缩放行为 }); // 自定义触摸事件 map.on('touchstart', function (e) { console.log('Touch start event:', e); }); map.on('touchmove', function (e) { console.log('Touch move event:', e); }); map.on('touchend', function (e) { console.log('Touch end event:', e); });
响应式布局:使用 CSS 媒体查询等技术,实现地图在不同屏幕尺寸下的自适应显示。
总结
Leaflet 地图库以其简洁的 API、强大的功能和良好的扩展性,为前端开发者提供了高效的地图开发解决方案。通过掌握 Leaflet 的基础概念、核心功能、进阶应用以及性能优化和问题解决方法,开发者能够创建出功能丰富、性能优良的 Web 地图应用。在实际项目中,应根据具体需求灵活运用这些技术,并结合插件和第三方服务,不断拓展地图应用的边界。