引言:这里以天地图作为底图使用leaflet.js封装部分地图常用功能,前篇有写过纯天地图封装hooks,所以为什么又换技术栈了呢?起因是为解决天地图渲染十万条数据卡顿问题,自从用了leaflet渲染后就没那么折腾了,使用canvas矢量图层绘制这加载速度也就解决了,心情也舒畅了~除了查leaflet相关攻略大部分都是收费一说,难顶。经过自己开发体验顺便也说一下纯天地图与leaflet的优缺点和感受吧,放在了文章最后,这部分也不重要,仅供参考!
——————————————————————————————————————————
文章改动日志:
2024/8/2:useLeafletMap hooks代码里新增tips注释提醒,旨在针对部分方法封装进行说明,不影响原hooks功能使用
——————————————————————————————————————————
言归正传,仅根据个人习惯封装,仅供参考,内部默认配置项请根据项目实际情况自行配置调整。
leaflet中文文档参考地址1:Documentation - Leaflet - 一个交互式地图 JavaScript 库
leaflet中文文档参考地址2:https://leafletjs.cn/reference.htmlLeaflet 1.9.3 中文文档 - 帮助手册&教程 - 《leaflet 中文文档 - 帮助手册 - 教程》 - 极客文档https://leafletjs.cn/reference.html
天地图API地址:天地图API
vue3引入leaflet参考地址:vue3.0使用leaflet_leaflet vue3-CSDN博客
各方法详细说明均在代码里体现,各方法参数配置参考leaflet文档对应功能,首先创建一个hooks——useLeafletMap.js
/** * leaflet hooks * params: * id: 地图绑定的html标签id,具有唯一性 * mapOptions: 地图初始化配置 * tips: * 1. 以下关于coordinate默认格式调整:{ lng: lng, lat: lat } * 2. 入参默认为Null的为非必须传参参数 * 3. 封装旨在外部能更简洁调用使用,以尽量在外部不需要再次引用leaflet一堆直接使用L的前提下进行封 * 装,故有些封装习惯看似冗余实则个人斟酌考虑,如不喜欢请自行调整, 以下均以tips提醒 */import { createVNode, render } from 'vue'import L from 'leaflet'import 'leaflet.chinatmsproviders'import 'leaflet/dist/leaflet.css'export const useLeafletMap = (id) => {let map = null // 地图对象// 地图初始化,mapOption:自定义地图配置const mapInit = (mapOption = {}) => {//天地图keyconst TDT_KEY = '您申请的天地图key';const normalm = L.tileLayer.chinaProvider('TianDiTu.Normal.Map', {key: TDT_KEY,maxZoom: 18,minZoom: 5})const normala = L.tileLayer.chinaProvider('TianDiTu.Normal.Annotion', {key: TDT_KEY,maxZoom: 18,minZoom: 5})const normal = L.layerGroup([normalm, normala])// 地图默认配置const defaultMapOption = {renderer: L.canvas(), // 默认矢量图层绘制方法center: [37.03, 111.92],zoom: 16,layers: [normal],zoomControl: false, // 缩放组件logoControl: false, // 去掉logoattributionControl: false, // 去掉右下角logominZoom: 12,maxZoom: 18, //最大显示层级preferCanvas: true,doubleClickZoom: false, // 取消双击放大closePopupOnClick: false // 默认点击地图关闭popup行为}map = new L.Map(id, Object.assign(defaultMapOption, mapOption))return Promise.resolve() // 回调地图初始化完成}// 地图平移const setMapCenter = (coordinate) => {const { lng, lat } = coordinatemap.panTo(L.latLng(lat, lng), { animate: true })}// 地图缩放const setMapScale = (coordinate, zoom) => {const { lng, lat } = coordinatemap.setView(L.latLng(lat, lng), zoom, { animate: true })}// 地图显示范围 coordinate1:范围点位1 coordinate2:范围点位2const setMapBounds = (coordinate1, coordinate2) => {const { lng: lng1, lat: lat1 } = coordinate1const { lng: lng2, lat: lat2 } = coordinate2map.setMaxBounds(L.latLngBounds(L.latLng(lat1, lng1), L.latLng(lat2, lng2)))}// 地图设置显示级别const setZoomLevels = (minLevels = 1, maxLevels = 18) => {map.setMinZoom(minLevels)map.setMaxZoom(maxLevels)}/** * 添加图片覆盖物类 * params: * coordinate1: 经纬度 格式:默认 * coordinate2: 经纬度 格式:默认 * imgUrl: 点位显示图标 * option: 图片覆盖物属性,{ opacity: 图片透明度, alt: 如果无法显示图像,浏览器将显示替代文本 } */const addMapImgOverLay = (coordinate1, coordinate2, imgUrl, option = {}) => {const { lng: lng1, lat: lat1 } = coordinate1const { lng: lng2, lat: lat2 } = coordinate2let bd = L.latLngBounds(L.latLng(lat1, lng1), L.latLng(lat2, lng2))let imgMarker = L.imageOverlay(imgUrl, bd, option)imgMarker.addTo(map)return imgMarker}/** * 地图添加自定义点位并返回该点位对象 * params: * coordinate: 经纬度 格式:默认 * markerOption: 地图图标对象配置 * iconOption: 地图ICON图标配置 * 格式:{ * iconUrl: String, 图标地址 * iconSize: [30, 30], 图标大小,非必需配置 * iconAnchor: [15, 30] 图标偏移,非必需配置 * } * clickEvent: 监听点击事件回调 * tips:iconOption本可直接写入markerOption配置里,但因其配置直接使用L,故另封装一个入参, * 可考虑单独对ICON封装函数,这里就不行此举了 */const addMapMarker = (coordinate,markerOption = {},iconOption = null,clickEvent = null) => {const { lng, lat } = coordinatelet option = {}if (iconOption) {let icon = L.icon(Object.assign({iconSize: [30, 30],iconAnchor: [15, 30]},iconOption))option.icon = icon}let marker = L.marker(L.latLng(lat, lng), Object.assign(option, markerOption))if (clickEvent) {marker.on('click', clickEvent)}marker.addTo(map)return marker}/** * 地图添加自定义线并返回该线对象 * params: * coordinateList: 线的各点位经纬度数组 格式:[{ lng: 'lng', lat: 'lat' }] * polylineOption: 线(polyline)配置项 * clickEvent: 监听点击事件回调 */const addMapLine = (coordinateList, polylineOption = null, clickEvent = null) => {let points = coordinateList.map((item) => {return L.latLng(item.lat, item.lng)})let marker = L.polyline(points, polylineOption)if (clickEvent) {marker.on('click', clickEvent)}marker.addTo(map)return marker}/** * 地图添加自定义面并返回该面对象 * params: * coordinateList: 面的各点位经纬度数组 格式:[[{ lng: 'lng', lat: 'lat' }], [{ lng: 'lng', lat: 'lat' }]] * 格式说明:处理面内环情况 外环位置是逆时针定义 内环位置值是顺时针定义 * polylineOption: 面(polygon)配置项 * clickEvent: 监听点击事件回调 */const addMapPolygon = (coordinateList, polygonOption = null, clickEvent = null) => {let polygon = coordinateList.map((item) => {return item.map((pointItem) => {return L.latLng(pointItem.lat, pointItem.lng)})})let marker = L.polygon(polygon, polygonOption)if (clickEvent) {marker.on('click', clickEvent)}marker.addTo(map)return marker}/** * 地图添加自定义窗口并返回窗口对象 * params: * coordinate: 弹出窗口经纬度 格式:默认 * popupOption: 弹出窗口(popup)配置项 * template: 自定义模板组件 * params: 传入组件自定义参数 * clickEvent: 监听点击事件回调 */const addMapPopup = (coordinate, popupOption = {}, template, params = {}, clickEvent = null) => {// 弹窗默认配置,请根据业务需求参考leaflet文档自定义设置const defaultOption = {autoPan: false, // 地图做平移动画,如果同时展示多个弹出窗口则默认不做平移interactive: true // popup将监听鼠标事件}// 创建虚拟节点并传参const popupContent = getTemplateNode(template, params)// 监听组件自定义handleClick事件clickEvent && popupContent.el.addEventListener('click', clickEvent)// 创建天地图弹出窗口 popupContent.el:test模板domlet popup = getPopupObj(popupContent.el, coordinate, Object.assign(defaultOption, popupOption))// 地图添加弹出窗口addMapOverLay(popup)return popup}/** * 生成一个模板节点并传参 * params: * template: 自定义模板组件 * params: 传入组件自定义参数 */const getTemplateNode = (template, params) => {// 创建虚拟节点并传参const popupContent = createVNode(template, params)let node = document.createElement('div') // 创建一个div节点render(popupContent, node) // 实例组件挂载到node节点上return popupContent}// 生成弹出窗口对象const getPopupObj = (el, coordinate, popupOption) => {const { lng, lat } = coordinatelet popup = L.popup(popupOption).setLatLng(L.latLng(lat, lng)).setContent(el)return popup}// 地图添加覆盖物 obj: 覆盖物对象const addMapOverLay = (obj) => {map.addLayer(obj)}// 地图移除覆盖物 obj: 覆盖物对象const removeMapOverLay = (obj) => {map.removeLayer(obj)}// 地图移除所有覆盖物const removeMapAllOverLay = () => { // 地图图层遍历移除map.eachLayer((layer) => {removeMapOverLay(layer)})}// 获取mapconst getMap = () => {return map}return {mapInit,setMapCenter,setMapScale,setMapBounds,setZoomLevels,addMapImgOverLay,addMapMarker,addMapLine,addMapPolygon,addMapPopup,getTemplateNode,getPopupObj,addMapOverLay,removeMapOverLay,removeMapAllOverLay,getMap}}
引入天地图hooks并初始化地图,以下都为部分关键代码:
<template><div id="map"></div></template><script setup name="index">import { useLeafletMap } from '@/hooks/useLeafletMap' // 引入const {mapInit,setMapCenter,setMapScale,setMapBounds,setZoomLevels,addMapImgOverLay,addMapMarker,addMapLine,addMapPolygon,addMapPopup,getTemplateNode,getPopupObj,addMapOverLay,removeMapOverLay,removeMapAllOverLay,getMap} = useLeafletMap('map') // 初始化地图onMounted(() => {mapInit().then(() => {// 地图初始化完毕后回调操作})})</script><style scoped lang="less">#map {width: 100vw;height: 100vh;}</style>
setMapCenter:手动设置地图平移示例:
const coordinate = { lat: '39.89945', lng: '116.40769'}setMapCenter(coordinate)
setMapScale:手动设置地图中心点及比例尺级别示例:
const coordinate = { lat: '39.89945', lng: '116.40769'}setMapScale(coordinate, 13)
setMapBounds:手动设置地图显示范围示例:
const coordinate1 = { lat: '30.49365', lng: '103.90114'}const coordinate2 = { lat: '30.64736', lng: '104.23073'}setMapBounds(coordinate1, coordinate2 )
setZoomLevels:手动设置地图显示级别示例:
setZoomLevels(12, 18) // 设置地图级别
addMapImgOverLay:添加地图图片覆盖物示例:
import testImg from '@/assets/image/test.png'const coordinate1 = { lat: '30.49365', lng: '103.90114'}const coordinate2 = { lat: '30.64736', lng: '104.23073'}addMapImgOverLay( coordinate1, coordinate2, testImg, { opacity: 100,alt: '' })
addMapMarker:添加自定义点位并返回该点位对象示例:
import testIcon from '@/assets/image/test.png'const coordinate = { lat: '39.89945', lng: '116.40769'}const marker = addMapMarker( coordinate, // leaflet文档里maiker options选项 { title: '这是title'}, // // leaflet文档里maiker的icon options选项 { iconUrl: pointImg }, (e) => { console.log(e) },)
addMapLine:添加自定义线并返回该线对象示例:
const coordinateList = [ { lng: '116.40769', lat: '39.89945' }, { lng: '116.50769', lat: '39.99945' }, { lng: '116.60769', lat: '39.79945' }, { lng: '116.70769', lat: '39.69945' }, { lng: '116.80769', lat: '39.59945' }] // 默认线配置const marker = addMapLine(coordinateList)
addMapPolygon:添加自定义面并返回该面对象示例:
// 蒙层四角 中国四角坐标 外部图层const smegmaPointList = [{lng: '73.0',lat: '59.0'},{lng: '73.0',lat: '3.0'},{lng: '136.0',lat: '3.0'},{lng: '136.0',lat: '59.0'},{lng: '73.0',lat: '59.0'}]// 区域范围const mapCoordinateData = [{lng: '104.0325',lat: '30.56888'},{lng: '104.04353',lat: '30.56795'},{lng: '104.04375',lat: '30.53942'},{lng: '104.02315',lat: '30.53931'},{lng: '104.02164',lat: '30.54504'},{lng: '104.02542',lat: '30.55657'},{lng: '104.03053',lat: '30.56278'},{lng: '104.0325',lat: '30.56888'}]// 常规画面示例const marker = addMapPolygon([smegmaPointList])// 画内环圆示例并配置相关信息const marker = addMapPolygon( [smegmaPointList, mapCoordinateData], { color: 'black', weight: 3, opacity: 0.2, fillColor: '#0c2940', // 蒙层颜色 fillOpacity: 0.7 // 透明度 }, () => {} )
addMapPopup:添加自定义信息窗口并返回该窗口对象示例:
自定义test组件示例:
// 自定义模块test @/components/test/index.vue<template> <div>{{ title }}</div><template> <scropt setup> defineProps({ title: { type: String, default: '' } })</scropt>
addMapPopup调用示例:
import TestTemplate from '@/components/test/index.vue'let coordinate = { lng: 111.92,lat: 37.03}// 示例场景一:popup直接挂载到point点上const marker = addMapMarker(coordinate) // 创建点let content = getTemplateNode(testTemplate, { parentName: '父组件传子组件' }) // 创建窗口节点content.el.addEventListener('click', (e) => { console.log(e) }) // 窗口节点点击事件marker1.bindPopup(content.el, { offset: L.point(0, -15), autoClose: false }) // 挂载// 示例场景二:popup自定义挂载到某个坐标位置const marker = addMapPopup(coordinate,{ autoClose: false },testTemplate,{ parentName: '这是自定义popup parentName' },(e) => {console.log(e)console.log(2222)})
addMapOverLay:添加覆盖物示例:
addMapOverLay(marker) // marker同上示例
addMapOverLay:移除覆盖物示例:
removeMapOverLay(marker) // marker同上示例
removeMapAllOverLay:移除天地图所有覆盖物示例:
removeMapAllOverLay()
这里放一个之前写得封装:【天地图】VUE3关于天地图部分功能hooks封装useTdtMap.js_vue3 天地图-CSDN博客
leaflet与纯天地图开发使用比较:
1.论开发地图自定义绘制工具:leaflet依赖第三方插件,插件暂时感觉没天地图灵活(待我再研究研究),天地图只需按管网API开发既是。
2.论十万数据加载速度:leaflet利用canvas绘制方式加载流畅,天地图需另想办法手动优化。
3.论API文档上手难度:体感两个差不多的,个别语法略有不同,比如移除地图所有图层方式上,leaflet相对在细节上感觉更细致些。
4.论加载Geojson数据:leaflet原有对应API方式加载,天地图需手动处理加载。
5.论官网文档查询:leaflet中文文档网页有时候会挂掉,所以我贴了两个地址。
6.论引入方式:leaflet我个人是没在官网找到关于VUE项目引入的,天地图就依照官网引导引入就行。
7.论开发出现问题寻求解决方案:就绿色上网来说,leaflet更多搜的是付费方案,天地图有些搜不太到,两者体感就大差不差了。
8.其它想到了以后再补充吧…………