【Vue】MineData 地图接入
一、文档资料:
MineData开放平台:
https://minedata.cn/md-platform/login/login
MineData V2.1.0 接口文档:
http://113.108.157.29:7070/support/static/api/doc/js/v2.1.0/api-reference/index.html#map
MineData 在线实例:
http://113.108.157.29:7070/support/api/demo/js-api/zh/map/base/map-show
二、上手案例:
- 1、引入方式:
1、可以直接在打包的index.html页面写script脚本接入
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="renderer" content="webkit"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <title>projectName</title> <script type="text/javascript" src="static/config.js"></script> <!-- 引入MineMap API插件 --> <link rel="stylesheet" href="http://minedata.cn/minemapapi/v2.1.1/minemap.css"> <script src="http://minedata.cn/minemapapi/v2.1.1/minemap.js"></script> </head> <body style="background: #f5f7f8"> <div id="app"></div> </body> </html>
2、或者在Vue编写动态接入的方法 import-js.js
import axios from 'axios' const loadJs = src => { return new Promise((resolve, reject) => { const script = document.createElement('script') script.type = 'text/javascript' script.src = src document.body.appendChild(script) script.onload = () => { resolve(script) } script.onerror = err => { console.log(`loadJsFail: ${err}`) reject() } }) } const loadCss = src => { return new Promise((resolve, reject) => { axios.get(src).then(res => { const css = document.createElement('style') css.type = 'text/css' css.innerHTML = res.data document.head.appendChild(css) resolve(css) }).catch(err => { console.log(`loadCssFail: ${err}`) reject() }) }) } export { loadJs, loadCss }
在具体某个需要使用的组件中初始化资源:
import { loadCss, loadJs } from './存放路径/import-js.js' async created() { await loadCss('http://minedata.cn/minemapapi/v2.1.1/minemap.css') await loadJs('http://minedata.cn/minemapapi/v2.1.1/minemap.js') }
- 2、地图初始化:
需要先赋值默认的参数变量,这里先用配置文件放好加载的变量 config.js
window._config = { MAP_LAT: 29.670908, MAP_LNG: 115.963214, minemapKey: '应用key,开放平台账号创建', minemapWhiteSolution: '地图样式id', minemapBlackSolution: '地图样式id', minemapDomainUrl: 'https://minemap.minedata.cn', minemapDataDomainUrl: 'https://minemap.minedata.cn', minemapServerDomainUrl: 'https://sd-data.minedata.cn', minemapSpriteUrl: 'https://minemap.minedata.cn/minemapapi/v2.1.1/sprite/sprite', minemapServiceUrl: 'https://service.minedata.cn/service' }
地图初始化还是基于dom对象创建的,所以所有关于地图加载的方法一定要放到mounted周期下执行
mounted() { this.initialMapContainer() }, methods: { async initialMapContainer() { window.minemap.domainUrl = window._config.minemapDomainUrl window.minemap.dataDomainUrl = window._config.minemapDataDomainUrl window.minemap.serverDomainUrl = window._config.minemapServerDomainUrl window.minemap.spriteUrl = window._config.minemapSpriteUrl window.minemap.serviceUrl = window._config.minemapServiceUrl window.minemap.key = window._config.minemapKey window.minemap.solution = window._config.minemapBlackSolution this.mapInstance = new window.minemap.Map({ container: 'mapContainer', style: `https://service.minedata.cn/map/solu/style/${window.minemap.solution}`, center: [window._config.MAP_LNG, window._config.MAP_LAT], zoom: 12, // 缩放大小直 pitch: 0, // 倾斜度参数 projection: window.minemap.ProjectionType.LATLON // 经纬度投影(根据实际项目情况使用,线上环境点位偏移,坐标参数不是配套的) }) } }
- 3、打点位:
addStationMarker(station) { // 创建点位的元素 const isOnline = station['runStatus'] === 1 const markerDom = window.document.createElement('div') markerDom.id = `station${station.id}` markerDom.style['background-image'] = `url(${isOnline ? this.stationIconPath2 : this.stationIconPath})` markerDom.style.width = '50px' markerDom.style.height = '50px' markerDom.style['border-radius'] = '2px' // 入参dom元素, 和元素的偏离参数(基于图标像素调整) const mineMarker = new window.minemap.Marker(markerDom, { offset: [-25, -50] }) // 点位经纬度参数对象 const markerLngLat = new window.minemap.LngLat(station.longitude, station.latitude) mineMarker.setLngLat(markerLngLat) // addTo方法将点位展示在地图上 mineMarker.addTo(this.mapInstance) }
图片资源我使用require直接转为具体对象来渲染的
stationIconPath: require('@/assets/image/station-point-1.png'), stationIconPath2: require('@/assets/image/station-point-2.png'),
如果在非打包的public目录下,可以直接写路径访问
- 4、创建点位信息窗体:
在点位创建的基础上追加弹窗窗体
addStationPopup(station, mineMarker) { const isOnline = station['runStatus'] === 1 const markerLngLat = new window.minemap.LngLat(station.longitude, station.latitude) // 参数说明: // (1)closeButton,true表示会展示一个关闭按钮; // (2)closeOnClick,设置为true表示当地图被点击时该信息窗体会被关闭; // (3)offset,参数为点位置相对于其左上角偏移像素大小; // (4)anchor,停靠点,值域为[top,bottom,left,right,top-left,top-right,bottom-left,bottom-right],如果不设置该参数,则会根据map container动态调整。 // (5)autoPan,设置为true时,当地图拖动到看不到popup的时候,自动将地图平移到可以看到popup,此参数只对固定popup有效; const stationPopup = new window.minemap.Popup({ closeOnClick: false, closeButton: true, anchor: 'bottom', offset: [0, -40], autoPan: false }) stationPopup.setLngLat(markerLngLat) // 弹窗内容创建 const popupContent = window.document.createElement('div') popupContent.className = isOnline ? 'station-popup online' : 'station-popup offline' popupContent.innerHTML = ` <h3>${station.stationName} ${station['stationLocation']}</h3> <p>区域编码:${station.regionCode}</p> <p>站点编号:${station.stationNo}</p> <p>站点状态:${station['runStatus'] === 1 ? '在线' : '离线'}</p> ` stationPopup.setDOMContent(popupContent) // stationPopup.addTo(this.mapInstance) /* 默认不打开弹窗 */ mineMarker.setPopup(stationPopup) }
预览内容:
窗体的样式可以直接修改:
/* mineMap地图窗体样式设置 */ /deep/ .minemap-popup-content { background: rgba(26,32,62,.7) !important; min-height: 100px; padding-top: 10px; overflow-y: auto; min-width: 200px; max-width: 500px !important; color: #D0D3DA; } /* 关闭按钮调整 */ /deep/ .minemap-popup-close-button { font-size: 20px; color: #D0D3DA; right: 8px; top: 6px; } /* 窗体下标箭头的颜色调整 */ /deep/ .minemap-popup-tip { border-top-color: rgba(26,32,62,.7) !important; } /* 窗体内部容器元素样式设置 */ /deep/ .station-popup { padding: 5px; width: 250px; font-size: 14px; } /deep/ .station-popup > p { margin-top: 10px; }
- 3.1 移动居中的点位:
地图平移API,有两种,相对平移和绝对平移:
http://113.108.157.29:7070/support/api/demo/js-api/zh/map/state/map-move
panBy基于相对位置平移,panTo基于点位绝对平移
function moveMap(vPixel, hPixel) { if (map) { map.panBy([vPixel, hPixel]); } } function moveMapCenter() { if (map) { map.panTo([116.46, 39.92]); } }
- 4.1 特殊窗体信息样式开发:
真是想不通为啥一定要这个样式,写起来真麻烦
可以发现这里是需要两种窗体样式的,一个是入口样式,一个是出口展示
所以不能直接对mineData的样式设置的,这样就不支持两种或多个窗体样式了
解决思路也是参考原型系统来的,我发现原型系统的默认弹窗是隐藏不展示的
这样就按我们自己声明的类名实现动态效果
第一步先把mineData的信息弹窗隐藏
/* 弹窗信息弹层不支持动态样式,隐藏展示 */ /deep/ .minemap-popup-content { max-width: none; /* 移除最大宽度 */ overflow: visible !important; /* 溢出的时直接展示,不要滚动条 */ background: none; /* 元素无背景 */ box-shadow: none; /* 元素无阴影 */ padding: 0; /* 内边距0 */ }
然后再是弹窗边框,内部布局这些内容:
/* 驶入弹窗边框 */ /deep/ .drive-in{ border: 1px solid #62b500; box-shadow: 0 0 10px 0 #62b50045; } /* 驶入小箭头 */ /deep/ .station-popup.drive-in:after { border-color: #62b500 transparent transparent transparent; } /* 驶出弹窗边框 */ /deep/ .drive-out{ border: 1px solid #f54336; box-shadow: 0 0 10px 0 #f5433645; } /* 驶出小箭头 */ /deep/ .station-popup.drive-out:after { border-color: #f54336 transparent transparent transparent; } /* 移除默认的下标箭头 */ /deep/ .minemap-popup-tip { display: none; } /* 入 */ /deep/ .station-popup.drive-in > div:first-child { background: #62b500; color: white; width: 40px; float: left; line-height: 120px; text-align: center; font-size: 16px; } /* 出 */ /deep/ .station-popup.drive-out > div:first-child { background: #f54336; width: 40px; color: white; line-height: 120px; text-align: center; font-size: 16px; } /deep/ .sfz_name { font-weight: 800; } /deep/ .station-pop-info { padding: 5px; } /* 关闭按钮调整 */ /deep/ .minemap-popup-close-button { font-size: 18px; right: 0; top: 5px; }
弹窗初始化方法:
addStationPopup(station, mineMarker) { const isDriveIn = station.type === '1' const markerLngLat = new window.minemap.LngLat(station.longitude, station.latitude) const stationPopup = new window.minemap.Popup({ closeOnClick: false, closeButton: true, anchor: 'bottom', offset: [0, isDriveIn ? -48 : -51], autoPan: false /* 关闭自动拖动 */ }) stationPopup.setLngLat(markerLngLat) const popupContent = window.document.createElement('div') popupContent.className = isDriveIn ? 'station-popup drive-in' : 'station-popup drive-out' popupContent.innerHTML = ` <div>${isDriveIn ? '入' : '出'}</div> <div class="station-pop-info"> <p class="sfz_name">${isDriveIn ? this.record.cashName : this.record.outCashName}</p> <p><span>所在区划:</span>${isDriveIn ? this.record.cashArea : this.record.outCashArea}</p> <p><span>所在高速:</span>${isDriveIn ? this.record.highwayName : this.record.outHighwayName}</p> <p>时间:${isDriveIn ? this.record.driveTime : this.record.outDriveTime}</p> </div> ` stationPopup.setDOMContent(popupContent) stationPopup.addTo(this.mapInstance) mineMarker.setPopup(stationPopup) }
2023年11月02日更新:
- 5 迁徙图 / 飞行图
新功能有一个飞行图的要求,同事是说在Echarts里面有,叫迁徙图,不过我没在文档实例上看到
我想了下这个东西在mineData应该提供了支持,找了下确实有的
http://113.108.157.29:7070/support/api/demo/js-api/zh/layer/plugin/od-1
归类在这图层插件里面:
还有一个是动态航线的图层,效果类似
http://113.108.157.29:7070/support/api/demo/js-api/zh/layer/special/airline-layer
注意要先引入echarts的插件js,不然找不到这个对象
<script src="http://minedata.cn/minemapapi/v2.1.0/plugins/echarts/echarts.3.8.5.min.js"></script> <script src="http://minedata.cn/minemapapi/v2.1.0/plugins/template/template.js"></script>
2023年11月28日更新:
功能进入测试阶段发现了一个问题,因为迁徙图的轨迹记录要随着主查询的数量不同而更新地图的展示
结果忘了写更新操作,这里又追补查看了下文档
要每次渲染图层之前,先删除之前渲染的图层
一、先说线图层的:
/** * 创建地图对象后,开始加载地图资源,地图资源加载完成后触发load事件 */ map.on('load', function () { // 增加自定义数据源、自定义图层 addSources(); addLayers(); });
线图层是通过追加Source和Layer实现的,所以肯定有对应的删除方法
按ID获取Layer,如果存在时再删除即可,不要直接删除,有可能Layer对象没有初始化
同理Sources也是一样的
迁徙图是在地图中嵌入了Echarts实现的,这个方法已经写好了尚未测试结果...
主要是在文档里面也没找到,待后续更新
2023年11月21日更新:
- 6 点位选取功能
需求是这样,主查询有个采集点位下拉选择,检索方式有两种
在下拉列表中选取
在点位图上选取
查询条件项设置:
<el-form-item label="采集点位"> <el-select v-model="queryForm.stationNo" placeholder="请选择" clearable> <el-option v-for="(item, idx) in stationList" :key="`station${idx}`" :label="`${item.stationNo} | ${item.stationName}`" :value="`${item.stationNo}`" /> </el-select> <el-button round type="default" size="small" icon="el-icon-map-location" circle @click="openStationChoose" /> </el-form-item>
点位选取弹窗和组件:
<el-dialog title="站点选取" :append-to-body="true" :close-on-click-modal="false" :visible="stationChooseVisible" width="75vw" custom-class="fixed-dialog" @close="stationChooseVisible = false" > <station-choose v-if="stationChooseVisible" @when-choose="whenStationChoose" /> </el-dialog>
弹窗样式控制:
/* 弹层高度固定 80vh */ /deep/ .fixed-dialog .el-dialog__body { height: calc(70vh + 60px); /*max-height: 80vh;*/ overflow-y: auto; } /* 弹层顶部间距固定 5vh */ /deep/ .fixed-dialog.el-dialog { margin-top: 5vh !important; }
方法块:
whenStationChoose(stationNo) { this.queryForm.stationNo = stationNo this.stationChooseVisible = false }, openStationChoose() { this.stationChooseVisible = true },
点位选取组件:
<template> <div id="choosePanel"> <div id="container" /> <div id="chooseForm"> <el-form :ref="chooseFormRef" :model="chooseForm" :rules="chooseRules" inline> <el-form-item label="所在区域" size="small"> <el-cascader v-model="chooseForm.regionCode" size="small" :options="regionTree" :props="regionProps" placeholder="所在区域" clearable @change="handleRegionChange" /> </el-form-item> <el-form-item label="站点状态" size="small"> <el-select v-model="chooseForm.runStatus" placeholder="请选择" clearable size="small"> <el-option v-for="item in runStatusList" :key="item.diCode" :label="item.diName" :value="item.diCode" /> </el-select> </el-form-item> <el-form-item label="站点名称" size="small"> <el-input v-model.trim="chooseForm.stationName" clearable placeholder="请输入站点名称" size="small" /> </el-form-item> <el-form-item label="站点编号" size="small"> <el-input v-model.trim="chooseForm.stationNo" clearable placeholder="请输入站点编号" size="small" /> </el-form-item> <el-form-item size="small"> <el-button type="primary" size="small" @click="chooseSubmit">查询</el-button> <el-button size="small" @click="chooseReset">重置</el-button> </el-form-item> </el-form> <div v-show="resultVisible" id="resultList"> <span>查询结果共有 {{ resultList.length }} 条 <i class="el-icon-close result-close-btn" @click="resultVisible = false" /></span> <div v-for="(result, idx) in resultList" :key="`result${idx}`" class="result-info"> <i class="result-seq">{{ idx + 1 }}</i> <div class="result-link"> <div> <el-link type="primary" @click="locateStationInMap(result)"> {{ result.stationName }} ({{ result['stationLocation'] }}) </el-link> </div> <div style="color: #8C8C8C; margin-top: 5px;">{{ result.stationNo }}</div> </div> </div> </div> </div> </div> </template> <script> import { getSysRegionCascadeList } from '../../api/perception/basic/region' import { getAllStation, getStationListByChoose } from '../../api/perception/basic/station' export default { name: 'StationChoose', data() { return { stationIconOnline: require('@/assets/image/station-online.png'), stationIconOffline: require('@/assets/image/station-offline.png'), mapInstance: null, currentPopup: null, stationList: [], runStatusList: [ { diCode: '', diName: '全部' }, { diCode: 1, diName: '在线' }, { diCode: -1, diName: '离线' } ], chooseFormRef: 'chooseFormRefKey', chooseRules: {}, chooseForm: { regionCode: '', runStatus: '', stationName: '', stationNo: '' }, resultList: [], resultVisible: true, regionTree: [], regionProps: { label: 'regionName', value: 'regionCode', children: 'subRegions', checkStrictly: true, emitPath: false, lazy: true, async lazyLoad(node, resolve) { if (node.root) { const { data: regionTree } = await getSysRegionCascadeList({ levelNo: 0 }) resolve(regionTree) } else { const { data: subRegions } = await getSysRegionCascadeList({ parentCode: node.data.regionCode }) if (subRegions && subRegions.length > 0) { const mapList = subRegions.map(x => ({ ... x, leaf: x.levelNo === 2 })) return resolve(mapList) } else return resolve([]) } } } } }, mounted() { this.mapInitialize() }, methods: { handleRegionChange(val) {}, async mapInitialize() { window.minemap.domainUrl = window._config.minemapDomainUrl window.minemap.dataDomainUrl = window._config.minemapDataDomainUrl window.minemap.serverDomainUrl = window._config.minemapServerDomainUrl window.minemap.spriteUrl = window._config.minemapSpriteUrl window.minemap.serviceUrl = window._config.minemapServiceUrl window.minemap.key = window._config.minemapKey window.minemap.solution = window._config.minemapWhiteSolution this.mapInstance = new window.minemap.Map({ container: 'container', style: `${window._config.minemapSolutionUrl}/${window.minemap.solution}`, center: [window._config.MAP_LNG, window._config.MAP_LAT], zoom: 8, pitch: 0, projection: window.minemap.ProjectionType.LATLON // 经纬度投影 }) /* 添加控件 */ const mineMapScale = new window.minemap.Scale() const mineMapNavigation = new window.minemap.Navigation() this.mapInstance.addControl(mineMapScale, 'bottom-right') this.mapInstance.addControl(mineMapNavigation, 'top-right') await this.initialAllStation() }, async initialAllStation() { const { data: stationList } = await getAllStation() this.stationList = stationList stationList.forEach(station => { this.addStationMarker(station) }) }, addStationMarker(station) { const isOnline = station['runStatus'] === 1 const markerDom = window.document.createElement('div') markerDom.id = `station${station.id}` markerDom.style['background-image'] = `url(${isOnline ? this.stationIconOnline : this.stationIconOffline})` markerDom.style.width = '32px' markerDom.style.height = '30px' markerDom.style['border-radius'] = '2px' const mineMarker = new window.minemap.Marker(markerDom, { offset: [-16, -30] }) const markerLngLat = new window.minemap.LngLat(station['longitudeGps'], station['latitudeGps']) mineMarker.setLngLat(markerLngLat) mineMarker.addTo(this.mapInstance) markerDom.addEventListener('click', event => { this.mapInstance.panTo([station['longitudeGps'], station['latitudeGps']]) this.addStationPopup(station) }) }, addStationPopup(station) { if (this.currentPopup) this.currentPopup.remove() this.currentPopup = null const isOnline = station['runStatus'] === 1 const markerLngLat = new window.minemap.LngLat(station['longitudeGps'], station['latitudeGps']) // 参数说明: // (1)closeButton,true表示会展示一个关闭按钮; // (2)closeOnClick,设置为true表示当地图被点击时该信息窗体会被关闭; // (3)offset,参数为点位置相对于其左上角偏移像素大小; // (4)anchor,停靠点,值域为[top,bottom,left,right,top-left,top-right,bottom-left,bottom-right],如果不设置该参数,则会根据map container动态调整。 // (5)autoPan,设置为true时,当地图拖动到看不到popup的时候,自动将地图平移到可以看到popup,此参数只对固定popup有效; const stationPopup = new window.minemap.Popup({ closeOnClick: false, closeButton: true, anchor: 'bottom', offset: [-1, -35], autoPan: false }) stationPopup.setLngLat(markerLngLat) /* 设置弹窗样式 */ const popupContent = window.document.createElement('div') popupContent.className = isOnline ? 'station-popup online' : 'station-popup offline' popupContent.innerHTML = ` <h3>${station.stationName} ${station['stationLocation']}</h3> <p>区域编码:<span>${station.regionCode}</span></p> <p>站点编号:<span>${station.stationNo}</span></p> <p>站点状态:<span>${station['runStatus'] === 1 ? '在线' : '离线'}</span></p> <div class="choose-btn" id="${station.stationNo}">选定</div> ` stationPopup.setDOMContent(popupContent) stationPopup.addTo(this.mapInstance) /* 默认不打开弹窗 */ // mineMarker.setPopup(stationPopup) const btn = document.getElementById(station.stationNo) btn.addEventListener('click', event => { console.log('事件触发,发射数据 ' + station.stationNo) this.$emit('when-choose', station.stationNo) }) this.currentPopup = stationPopup }, chooseSubmit() { this.$refs[this.chooseFormRef].validate(async(valid) => { if (!valid) return const withoutName = !this.chooseForm.stationName const withoutNo = !this.chooseForm.stationNo if (withoutName && withoutNo) return this.$message.warning('站点名称和站点编号不能同时为空') const { data: resultList } = await getStationListByChoose(this.chooseForm) this.resultList = resultList this.resultVisible = true }) }, chooseReset() { this.resultVisible = false this.chooseForm = { regionCode: '', runStatus: '', stationName: '', stationNo: '' } }, locateStationInMap(station) { this.mapInstance.panTo([station['longitudeGps'], station['latitudeGps']]) this.addStationPopup(station) } } } </script> <style scoped> #choosePanel { width: 100%; height: 100%; background-color: white; position: relative; } #container { width: 100%; height: 100%; } #chooseForm { width: 90%; position: absolute; top: 20px; left: 20px; padding: 10px 10px 0 10px; background-color: white; box-shadow: 0 2px 4px #9e9e9ec2; } #resultList { position: absolute; bottom: 0; right: 0; transform: translateY(110%); background: #fff; box-shadow: 0 0 4px #9e9e9ec2; padding: 8px; width: 400px; } .result-close-btn { float: right; font-size: 16px; cursor: pointer; } .result-info { display: flex; flex-direction: row; align-items: center; justify-content: space-between; margin: 15px 0; } .result-seq { display: block; font-style: italic; width: 15px; font-weight: bolder; } .result-link { width: 365px; text-align: left; } /* 替换回原本颜色 */ /deep/ .el-form-item__label { color: #666666 !important; } /deep/ #chooseForm .el-input__inner { color: #666666 !important; border-color: #CBCFD5 !important; border-radius: 0 !important; } /deep/ #chooseForm .el-input__inner:hover, .el-textarea__inner:hover { border-color: #CBCFD5 !important; } /deep/ #chooseForm .el-range-input { background-color: transparent; color: rgba(255, 255, 255, 0.6) !important; } /* 地图样式 */ /* mineMap地图样式设置 */ /deep/ .minemap-popup-content { background: rgb(10,76,150,.9) !important; min-height: 100px; padding-top: 10px; overflow-y: auto; min-width: 200px; max-width: 500px !important; color: #D0D3DA; } /* 关闭按钮调整 */ /deep/ .minemap-popup-close-button { font-size: 20px; color: #D0D3DA; right: 8px; top: 6px; } /* 移除默认的箭头 */ /deep/ .minemap-popup-tip { /*background: rgb(10,76,150,.7) !important;*/ } /deep/ .station-popup { padding: 5px; width: 250px; font-size: 14px; } /deep/ .station-popup > .choose-btn { cursor: pointer; background: #46a6ff; color: #fff; text-align: center; border-radius: 2px; padding: 4px 2px 2px 2px; } /deep/ .station-popup > p, /deep/ .station-popup > .choose-btn { margin-top: 10px; } /deep/ .station-popup > p > span, /deep/ .station-popup > h3 { color: white !important; } .zdgk-statistic-val { color: #f1ba05; font-size: 12px; font-weight: bold; } /deep/.minemap-popup-anchor-bottom .minemap-popup-tip { border-top-color: rgb(10, 76, 150, 0.9) !important; } </style>
其实主要难点是在点位信息窗体上加事件传递信息,这个是手写的dom元素,不会给vue去绑定处理
没有啥好办法,就直接用id属性设置站点编号,然后加个点击事件来触发方法
调用发射器给业务组件用
2023年12月13日更新内容:
1、真实路线需求
产品对线图层的效果不满意,因为无法得知真实路线的走线情况
这里同事追加了一个细腻点位的接口,传入原始点位集合,返回真实路线的点位集合
如果使用投射参数偏移了定位,路线是无效的,因为要依据地图路线映射
效果预览:
接口地址:
minemapCarLocus: 'https://service.minedata.cn/service/lbs/route/v1/driving?key=#####',
调用方法:
getCarPath: async function (path) { let realPath = [] let lastRoute = path for (; lastRoute.length > 0;) { const first = lastRoute[0] let last = '' const waypoints = [] if (lastRoute.length > 52) { last = lastRoute[51] for (let i = 1; i < 51; i++) { waypoints.push(lastRoute[i]) } lastRoute = lastRoute.slice(52, lastRoute.length) } else { last = lastRoute[lastRoute.length - 1] for (let i = 1; i < lastRoute.length - 1; i++) { waypoints.push(lastRoute[i]) } lastRoute = [] } /* 调用四维接口获取真实路径轨迹 */ const { data: data } = await window.$Axios.get(window._config.minemapCarLocus + '&origin=' + first + '&destination=' + last + '&waypoints=' + waypoints.join(';')) const steps = data.result.routes[0].steps steps.forEach(e => { const polyline = e.polyline if (polyline) { const polylines = polyline.split(';') polylines.forEach(loc => { const locs = loc.split(',') realPath.push([locs[0], locs[1]]) }) } }) } return realPath; }
2、解决投射参数配置问题:
/** * 是否使用投影参数 * http://113.108.157.29:7070/support/static/api/doc/js/v2.1.0/api-reference/index.html#map * true -> projection: window.minemap.ProjectionType.LATLON * false -> projection: window.minemap.ProjectionType.MERCATOR */ projectionFlag: false,
所有地图初始化之前访问flag变量检查是否启用投射
因为线上环境需要投射,但是本地环境和测试环境不需要投射展示
this.highwayMapInstance = new window.minemap.Map({ container: 'highwayMapContainer', style: `${window._config.minemapSolutionUrl}/${window._config.minemapBlackSolution}`, center: [window._config.MAP_LNG, window._config.MAP_LAT], zoom: 12, pitch: 0, projection: window._config.projectionFlag ? 'LATLON' : 'MERCATOR' // 经纬度投影 })
2023年12月21日 更新:
新增了一个地图绘制布控区域的需求
- 1、用户可以在地图上绘制一片区域,并输入文本表示是什么区域
- 2、区域回显之后,光标悬浮区域时,显示信息窗体,展示该区域内的站点统计信息
先看图层编辑的案例:
https://minedata.com.cn/support/api/demo/js-api/zh/edit/data/data-add
提供了一套编辑的API和交互案例
但是案例没有说清楚是怎么读取编辑的图层对象
然后在注释里找到了:
http://113.108.157.29:7070/minemapapi/v2.1.0/plugins/edit/api.html
因为不关心编辑时如何,只要拿到所有编辑好的图层信息即可
要回显之前编辑的图层,用案例的setFeatures的API
但是一定要放到地图的加载周期里面:
因为有多用户操作布控区域的可能,所以要区分用户,表结构:
CREATE TABLE `bk_features` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '记录主键', `features` text COLLATE utf8mb4_general_ci COMMENT '布控区域矢量信息', `creator` varchar(32) COLLATE utf8mb4_general_ci NOT NULL COMMENT '创建人', `create_time` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='布控区域记录表';
业务逻辑暂时想的简单,无非是查询用户有没有编辑图层信息
有就回显,如果重复编辑就更新
/** * @author OnCloud9 * @version 1.0 * @project road-perception-server * @date 2023年12月18日 19:17 */ @Service("bkFeaturesService") public class BkFeaturesServiceImpl extends ServiceImpl<BkFeaturesDAO, BkFeaturesDTO> implements IBkFeaturesService { @Override public void saveCurrentFeatures(BkFeaturesDTO dto) { UserContext user = LoginUserContext.getUser(); String id = user.getId(); dto.setCreator(id); dto.setCreateTime(new Date()); List<BkFeaturesDTO> list = lambdaQuery().eq(BkFeaturesDTO::getCreator, id).list(); boolean hasOne = CollectionUtils.isNotEmpty(list); if (hasOne) { BkFeaturesDTO one = list.get(0); one.setFeatures(dto.getFeatures()); baseMapper.updateById(one); } else { baseMapper.insert(dto); } } @Override public BkFeaturesDTO getCurrentFeatures() { UserContext user = LoginUserContext.getUser(); String id = user.getId(); List<BkFeaturesDTO> list = lambdaQuery().eq(BkFeaturesDTO::getCreator, id).list(); return CollectionUtils.isEmpty(list) ? null : list.get(0); } }
控制器:
因为保存前端提交的json数据不被转义,就加上转义逃逸注解处理
/** * @author OnCloud9 * @version 1.0 * @project road-perception-server * @date 2023年12月18日 19:18 */ @RestController @RequestMapping("${api.path}/features") public class BkFeaturesController extends BaseController { @Resource private IBkFeaturesService bkFeaturesService; /** * @author OnCloud9 * @date 2023/12/18 19:21 * @description * @params [dto] * @return void */ @ActiveDeXss @PostMapping("/save") public void saveCurrentFeatures(@RequestBody BkFeaturesDTO dto) { bkFeaturesService.saveCurrentFeatures(dto); } @GetMapping("/current") public BkFeaturesDTO getCurrentFeatures() { return bkFeaturesService.getCurrentFeatures(); } }
地图前端方法:
1、初始化处理
async initialMapContainer() { window.minemap.domainUrl = window._config.minemapDomainUrl window.minemap.dataDomainUrl = window._config.minemapDataDomainUrl window.minemap.serverDomainUrl = window._config.minemapServerDomainUrl window.minemap.spriteUrl = window._config.minemapSpriteUrl window.minemap.serviceUrl = window._config.minemapServiceUrl window.minemap.key = window._config.minemapKey window.minemap.solution = window._config.minemapBlackSolution this.mapInstance = new window.minemap.Map({ container: 'mapContainer', style: `${window._config.minemapSolutionUrl}/${window._config.minemapBlackSolution}`, center: [window._config.MAP_LNG, window._config.MAP_LAT], zoom: 12, pitch: 0, projection: window._config.projectionFlag ? 'LATLON' : 'MERCATOR' // 经纬度投影 }) this.initialMapEditor() this.mapInstance.on('load', () => { // 向编辑池增加数据 this.initialMapFeatures() }) this.initialAllStation() }, async initialMapFeatures() { const { data } = await getCurrentFeatures() console.log(data) if (!data) return const featuresCollection = JSON.parse(data['features']) console.log(featuresCollection) if (!featuresCollection) return /* 回显编辑内容 */ this.mapEditor.setFeatures(featuresCollection) },
2、保存方法:
saveFeatures() { const allFeatures = this.mapEditor.draw.getAll() saveCurrentFeatures({ features: JSON.stringify(allFeatures) }) this.$message.success('保存成功') },
3、文本编辑问题
因为案例是写死的,这个肯定要交给用户输入处理
文本输入弹窗
<el-dialog title="添加文字" width="400px" :visible.sync="textVisible" :close-on-click-modal="false" append-to-body> <el-form size="small"> <el-form-item> <el-input v-model.trim="featureText" /> </el-form-item> <el-form-item align="center"> <el-button type="primary" @click="featureTextConfirm">确定</el-button> <el-button type="default" @click="textVisible = false">取消</el-button> </el-form-item> </el-form> </el-dialog>
确认文本后再触发编辑器交互:
featureTextConfirm() { if (!this.featureText) return this.$message.error('请填写文字内容') this.textVisible = false this.mapEditor.onBtnCtrlActive('text', { style: { 'textField': this.featureText + '', 'textColor': '#00FF00', 'textSize': 24, 'custom_style': 'true' // 此参数为string类型 } }) },
4、回显展示图层的问题:
因为首页和编辑功能不一样,这里需要做图层转换处理
面图层和图标图层的绘制方式不一样,我们先用分组方法进行分组处理:
/* 分为面图层渲染(el.geometry.type 'Polygon')和图标图层渲染(el.geometry.type 'Point') */ const fcTypeMap = groupBy(featureCollection['features'], el => el.geometry.type) const polygonList = fcTypeMap['Polygon'] const pointList = fcTypeMap['Point']
分组方法仿照Java的Stream方法实现:
/** * 集合分组方法 * @param collect * @param groupFun */ const groupBy = (collect, groupFun) => { return collect.reduce((groupedMap, el) => { const keyVal = groupFun(el) if (!keyVal) return groupedMap if (!groupedMap[keyVal]) groupedMap[keyVal] = [] groupedMap[keyVal].push(el) return groupedMap }, {}) }
面图层相对简单,直接丢进去就可以了
因为可能绘制多个区域,每个区域追加点击事件,所以这样循环添加Source和Layer
方便后面的事件绑定处理,
Source和Layer的逻辑还是比较好理解的,现有Source矢量信息,然后Layer创建时绑定Source信息
而Feature对象是编辑器edit对象创建的,可以交给Source转换
Feature对象的id创建时自动分配,应该是唯一的
这里可以把 Source-Id, Layer-Id Feature-Id 统一使用,方便维护
polygonList.forEach((el, idx) => { const featureId = el.id const coordinates = el.geometry.coordinates this.mapInstance.addSource(featureId, { type: 'geojson', data: { type: 'FeatureCollection', features: [el] } }) this.mapInstance.addLayer({ 'id': featureId, 'type': 'fill', 'source': featureId, 'layout': { 'visibility': 'visible' }, 'paint': { 'fill-color': '#00ff00', 'fill-opacity': 0.4, 'fill-outline-color': '#ff0000' }, 'minzoom': 7, 'maxzoom': 17.5 }) })
给图层绑定鼠标事件,鼠标进入,鼠标离开两个事件
https://minedata.com.cn/support/static/api/doc/js/v2.1.0/api-reference/index.html#mapon
但是要统计绘制区域内的站点信息,就要知道我们的打的站点是否在这个区域内,这个API正好还被我找到了哈哈哈
http://113.108.157.29:7070/support/api/demo/js-cmpt/zh/geometry/relation/point-in-polygon
我找到的这个是带了一个地图工具库对象的方法
默认的案例中是使用turf对象判断的,还需要转换coordinates信息
http://113.108.157.29:7070/support/api/demo/js-api/zh/geometry/relation/point-in-polygon
这里挂载事件的时候和上面说的一样,直接丢featureId进来
this.mapInstance.on('mouseenter', featureId, async event => { console.log(`鼠标移入触发! ${featureId} ${event.lngLat.lng} ${event.lngLat.lat}`) if (this.currentFeaturePopup) this.currentFeaturePopup.remove() /* 计算站点信息统计 */ const featureStations = [] this.allStations.forEach(station => { const stationCoord = [station['longitudeGps'], station['latitudeGps']] const isInPolygon = window.minemaputil.SpaceUtil.pointWithinPolygon(stationCoord, coordinates) if (isInPolygon) featureStations.push(station) }) /* 统计设备信息 */ const deviceCount = featureStations.length const onlineCount = featureStations.filter(s => s['runStatus'] === 1).length const offlineCount = featureStations.filter(s => s['runStatus'] !== 1).length /* 统计在线站点采集数 */ const stationNoList = featureStations.map(x => x.stationNo) let carCount = 0 let bleCount = 0 if (deviceCount !== 0) { const { data } = await getFeaturesAreaOverView({ stationNoList: stationNoList }) carCount = data['carCount'] bleCount = data['bleCount'] } /* 区域名称提取 */ const target = pointList.find(p => window.minemaputil.SpaceUtil.pointWithinPolygon(p.geometry.coordinates, coordinates)) let title = target.properties.textField title = title || '未命名' /* 设置弹窗样式 */ const popupContent = window.document.createElement('div') popupContent.className = 'station-popup' popupContent.innerHTML = ` <h3>${title}</h3> <p>设备:${deviceCount}</p> <p>在线:${onlineCount}</p> <p>离线:${offlineCount}</p> <p>车辆 ${carCount}, 蓝牙 ${bleCount}</p> ` const popup = new window.minemap.Popup({ closeOnClick: true, closeButton: false, anchor: 'bottom-left', offset: [5, 10], autoPan: false }) popup.setDOMContent(popupContent) popup.trackPointer() // 此方法可以让popup跟随鼠标指针,使用此方法就不再需要使用setLngLat()方法 popup.addTo(this.mapInstance) this.currentFeaturePopup = popup }) this.mapInstance.on('mouseleave', featureId, event => { console.log(`鼠标移触发触发! ${featureId}`) /* 如果存在,删除这个弹窗 */ if (this.currentFeaturePopup) this.currentFeaturePopup.remove() })
图标图层渲染文字时,发现它是带了一个种类,有个锚点图标
案例地址:
https://minedata.com.cn/support/api/demo/js-api/zh/layer/base/symbol-layer
这个事情问了下维护人员,他说你就删掉icon信息就可以了.... (这里可以对比我的代码和案例代码)
但是feature对象不能直接套用,要按照demo的结构重新构造一遍,麻烦
pointList.forEach(el => { const featureId = el.id /* 原feature对象无法适配,创建一个适配的feature对象 */ const adaptFeature = { id: featureId, type: 'Feature', geometry: el.geometry, properties: { title: el.properties.textField, kind: 'label' } } this.mapInstance.addSource(featureId, { type: 'geojson', data: { type: 'FeatureCollection', features: [adaptFeature] } }) this.mapInstance.addLayer({ 'id': featureId, 'type': 'symbol', 'source': featureId, 'layout': { 'visibility': 'visible', 'text-field': '{title}', 'text-offset': [0, 0.6], 'text-anchor': 'top', 'text-size': 24, 'icon-allow-overlap': true, // 图标允许压盖 'text-allow-overlap': true // 图标覆盖文字允许压盖 }, 'paint': { 'text-color': { 'type': 'categorical', 'property': 'kind', 'stops': [['label', '#ff0000'], ['park', '#00ff00'], ['hospital', '#0000ff']], 'default': '#ff0000' }, 'text-halo-color': '#000000', 'text-halo-width': 0.5 }, 'minzoom': 7, 'maxzoom': 17.5 }) })
实现效果:
2023年12月25日 更新:
下一步是样式设置的问题,弹窗的样式设置在上面有提到过
但是图层样式没有,这个得自己找找了
问题一:面图层的边框线条只能设置颜色
面图层样式参数文档:
https://minedata.com.cn/support/static/api/doc/js/v2.1.0/style-specification/index.html#/layers/fill/*
但是产品的原型效果是这样的
于是继续问那边的维护人员,解决方案的话是在面图层的基础上添加一个线图层来实现
虚线的实现就是多加了一个虚线数组参数: 'line-dasharray': [1, 4],
/** * 线图层绘制方法 */ drawLineLayer(layerId, coordinates) { /* 额外追加线图层来设置样式 */ this.mapInstance.addSource(layerId, { 'type': 'geojson', 'data': { 'type': 'FeatureCollection', 'features': [{ 'type': 'Feature', 'geometry': { 'type': 'LineString', 'coordinates': coordinates }, 'properties': { 'title': '路线一', 'kind': 1 } }] } }) this.mapInstance.addLayer({ 'id': layerId, 'type': 'line', 'source': layerId, 'layout': { 'line-join': 'round', 'line-cap': 'round', 'border-visibility': 'visible' // 是否开启线边框 }, 'paint': { 'line-width': 6, 'line-dasharray': [1, 4], 'line-color': { 'type': 'categorical', 'property': 'kind', 'stops': [[1, '#02F4FF'], [2, '#02F4FF']], 'default': '#02F4FF' }, 'line-border-width': 2, // 设置线边框宽度 'line-border-opacity': 1, // 设置线边框透明度 'line-border-color': 'blue' // 设置线边框颜色 }, 'minzoom': 7, 'maxzoom': 17.5 }) },
面图层的坐标信息不能直接挪用到线图层去,因为面图层的坐标是三维数组,但是线图层是二维数组
问题二:样式切换问题
参考图层样式设置demo:
https://minedata.com.cn/support/api/demo/js-api/zh/layer/manage/layer-opacity-setting
之前有提到利用鼠标监听事件来实现,所以设置一定是在监听事件中执行的
async initialEditorFeature() { const { data } = await getCurrentFeatures() if (!data) return const featureCollection = JSON.parse(data['features']) if (!featureCollection) return /* 分为面图层渲染(el.geometry.type 'Polygon')和图标图层渲染(el.geometry.type 'Point') */ const fcTypeMap = groupBy(featureCollection['features'], el => el.geometry.type) const polygonList = fcTypeMap['Polygon'] const pointList = fcTypeMap['Point'] polygonList.forEach((el, idx) => { const featureId = el.id const lineId = `line-${featureId}` const pointId = `point-${featureId}` const coordinates = el.geometry.coordinates console.log(`coordinates ${JSON.stringify(coordinates)}`) const pointTarget = pointList.find(p => window.minemaputil.SpaceUtil.pointWithinPolygon(p.geometry.coordinates, coordinates)) /* 原feature对象无法适配,创建一个适配的feature对象 */ const adaptFeature = { id: pointId, type: 'Feature', geometry: pointTarget.geometry, properties: { title: pointTarget.properties.textField, kind: 'label' } } this.drawLineLayer(lineId, coordinates[0]) this.drawPolygonLayer(featureId, el) this.drawPointLayer(pointId, adaptFeature) this.mapInstance.on('mouseenter', featureId, async event => { if (this.currentFeaturePopup) this.currentFeaturePopup.remove() const featureInfo = await this.getFeatureStationsInfo(coordinates, pointTarget) this.openFeatureInfoWindow(featureInfo) /* 设置激活的Layer样式 */ this.mapInstance.setPaintProperty(featureId, 'fill-color', '#FFEA59') this.mapInstance.setLayoutProperty(pointId, 'visibility', 'none') this.mapInstance.setPaintProperty(lineId, 'line-color', { 'type': 'categorical', 'property': 'kind', 'stops': [[1, '#FFEA59'], [2, '#FFEA59']], 'default': '#02F4FF' }) }) this.mapInstance.on('mouseleave', featureId, event => { /* 如果存在,删除这个弹窗 */ if (this.currentFeaturePopup) this.currentFeaturePopup.remove() /* 恢复默认的Layer样式 */ this.mapInstance.setPaintProperty(featureId, 'fill-color', '#059ded') this.mapInstance.setLayoutProperty(pointId, 'visibility', 'visible') this.mapInstance.setPaintProperty(lineId, 'line-color', { 'type': 'categorical', 'property': 'kind', 'stops': [[1, '#02F4FF'], [2, '#02F4FF']], 'default': '#02F4FF' }) }) }) },
拆分出来的方法代码:
openFeatureInfoWindow(featureInfo) { /* 设置弹窗样式 */ const popupContent = window.document.createElement('div') popupContent.className = 'bk-area-popup' popupContent.innerHTML = ` <h3>${featureInfo.title}</h3> <p>设备:${featureInfo.deviceCount} 在线:${featureInfo.onlineCount} 离线:${featureInfo.offlineCount}</p> <p>车辆:${featureInfo.carCount} 蓝牙:${featureInfo.bleCount}</p> ` const popup = new window.minemap.Popup({ closeOnClick: true, closeButton: false, anchor: 'bottom-left', offset: [5, 0], autoPan: false }) popup.setDOMContent(popupContent) popup.trackPointer() // 此方法可以让popup跟随鼠标指针,使用此方法就不再需要使用setLngLat()方法 popup.addTo(this.mapInstance) this.currentFeaturePopup = popup }, /** * 获取弹窗信息 */ async getFeatureStationsInfo(rangeCoordinates, pointTarget) { /* 1、计算站点信息统计 */ const featureStations = [] this.allStations.forEach(station => { const stationCoord = [station['longitudeGps'], station['latitudeGps']] const isInPolygon = window.minemaputil.SpaceUtil.pointWithinPolygon(stationCoord, rangeCoordinates) if (isInPolygon) featureStations.push(station) }) /* 2、统计设备信息 */ const deviceCount = featureStations.length const onlineCount = featureStations.filter(s => s['runStatus'] === 1).length const offlineCount = featureStations.filter(s => s['runStatus'] !== 1).length /* 3、统计在线站点采集数 */ const stationNoList = featureStations.map(x => x.stationNo) let carCount = 0 let bleCount = 0 if (deviceCount !== 0) { const { data } = await getFeaturesAreaOverView({ stationNoList: stationNoList }) carCount = data['carCount'] bleCount = data['bleCount'] } /* 4、区域名称提取 */ let title = pointTarget.properties.textField title = title || '未命名' return { deviceCount, onlineCount, offlineCount, carCount, bleCount, title, pointTarget } }, /** * 面(多边形)图层绘制方法 */ drawPolygonLayer(layerId, featureObj) { this.mapInstance.addSource(layerId, { type: 'geojson', data: { type: 'FeatureCollection', features: [featureObj] } }) this.mapInstance.addLayer({ 'id': layerId, 'type': 'fill', 'source': layerId, 'layout': { 'visibility': 'visible' }, 'paint': { 'fill-antialias': true, /* 启用抗锯齿 */ 'fill-color': '#059ded', 'fill-opacity': 0.4, 'fill-outline-color': '#059ded' }, 'minzoom': 7, 'maxzoom': 17.5 }) }, /** * 线图层绘制方法 */ drawLineLayer(layerId, coordinates) { /* 额外追加线图层来设置样式 */ this.mapInstance.addSource(layerId, { 'type': 'geojson', 'data': { 'type': 'FeatureCollection', 'features': [{ 'type': 'Feature', 'geometry': { 'type': 'LineString', 'coordinates': coordinates }, 'properties': { 'title': '路线一', 'kind': 1 } }] } }) this.mapInstance.addLayer({ 'id': layerId, 'type': 'line', 'source': layerId, 'layout': { 'line-join': 'round', 'line-cap': 'round', 'border-visibility': 'visible' // 是否开启线边框 }, 'paint': { 'line-width': 6, 'line-dasharray': [1, 4], 'line-color': { 'type': 'categorical', 'property': 'kind', 'stops': [[1, '#02F4FF'], [2, '#02F4FF']], 'default': '#02F4FF' }, 'line-border-width': 2, // 设置线边框宽度 'line-border-opacity': 1, // 设置线边框透明度 'line-border-color': 'blue' // 设置线边框颜色 }, 'minzoom': 7, 'maxzoom': 17.5 }) }, drawPointLayer(layerId, featureObj) { this.mapInstance.addSource(layerId, { type: 'geojson', data: { type: 'FeatureCollection', features: [featureObj] } }) this.mapInstance.addLayer({ 'id': layerId, 'type': 'symbol', 'source': layerId, 'layout': { 'visibility': 'visible', 'text-field': '{title}', 'text-offset': [0, 0.6], 'text-anchor': 'top', 'text-size': 24, 'icon-allow-overlap': true, // 图标允许压盖 'text-allow-overlap': true // 图标覆盖文字允许压盖 }, 'paint': { 'text-color': { 'type': 'categorical', 'property': 'kind', 'stops': [['label', '#48D4FF'], ['park', '#fff'], ['hospital', '#fff']], 'default': '#fff' }, 'text-halo-color': '#fff', 'text-halo-width': 0.2 }, 'minzoom': 7, 'maxzoom': 17.5 }) },
这里涉及到了一个深度对象设置参数的问题,但是我懒得解决了,直接整个对象丢进来
那边是提供了一个表达式文档,可以参考看看:
https://minedata.com.cn/support/static/api/doc/js/v2.1.0/style-specification/index.html#/expressions
效果实现:
悬停在绘制区域内时
‘
脱离悬停区域时:
目前来说对这个效果还算满意哈哈哈
2023年12月26日更新:
昨天还遗留了这个问题,就是跟随光标的弹窗留了一个箭头样式没修复
之前我们设置的样式是这么处理的:
/deep/.minemap-popup-anchor-bottom > .minemap-popup-tip { border-top-color: rgba(10,76,150,0.9) !important; }
对应的achor参数是bottom,但是这个光标跟随的是bottom-left
我就想应该是对应的一个class类名,试了下还真是这样
悬浮样式的箭头是不需要的,所以这个边框头直接不要任何样式
/* 悬浮弹窗箭头样式移除 */ /deep/.minemap-popup-anchor-bottom-left > .minemap-popup-tip { border-top: none !important; }
这样就修好了
2024年01月22日更新:
因为存在编辑图层交叉区域的情况,还存在打点信息交互的点图层
这里就出现了悬浮区域信息展示的BUG,会多次重复展示,直接进入交叉区域会将两个区域的信息重叠展示
之前是以为只有一个区域弹窗信息出现:
currentFeaturePopup: null
那么现在我们改为:
currentFeaturePopup: []
清除弹窗信息方法:
之前是用forEach处理的好像,这里老老实实用api的方式处理
clearFeaturePopup() { for (let i = 0; i < this.currentFeaturePopup.length; i++) this.currentFeaturePopup[i].remove() this.currentFeaturePopup = [] },
所有涉及地图交互的事件,建议调用清除方法:
因为首页只有一个区域和点位展示,这里打开点位时的事件追加清除方法
包括鼠标移入移出这个点位也要清除
处理交叉区域的事件,通过console打印发现,交叉区域会同时触发两个图层的事件
无法避免的,所以这里我补加了一个之后的判断,检查弹层是不是超出1个,不是的话只保留最新的那个
2024年01月30日更新:
1、失焦BUG问题
产品测试发现展示弹窗的时候如果光标处在图层的边界位置时,弹窗的坐标定位失焦
这个bug在去年就提了,我改了好几个事件的处理都发现没有用,最后定位是这个API有BUG
要不要处理就看产品和测试要求了,我个人认为是能接受的
这里跟那边人员沟通写了demo看效果:
编辑器地址:
https://minedata.com.cn/support/api/demo/js-api/zh/layer/base/fill-layer
bug-demo.html 源码 (丢到上面的编辑器里面运行看效果):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>面图层</title> <link rel="stylesheet" href="//minedata.com.cn/support/static/api/demo/js-api/zh/css/demo.css"> <!-- 引入MineMap API插件 --> <link rel="stylesheet" href="//minedata.com.cn/minemapapi/v2.1.0/minemap.css"> <script src="//minedata.com.cn/minemapapi/v2.1.0/minemap.js"></script> <style> #map { width: 100%; height: 100%; } </style> </head> <body> <div id="map"></div> <script> /** * 全局参数设置 */ minemap.domainUrl = '//minedata.com.cn'; minemap.dataDomainUrl = '//minedata.com.cn'; minemap.serverDomainUrl = '//minedata.com.cn'; minemap.spriteUrl = '//minedata.com.cn/minemapapi/v2.1.0/sprite/sprite'; minemap.serviceUrl = '//minedata.com.cn/service/'; minemap.key = '16be596e00c44c86bb1569cb53424dc9'; minemap.solution = 12877; var popups = []; var map = new minemap.Map({ container: 'map', style: '//minedata.com.cn/service/solu/style/id/12877', center: [116.46,39.92], zoom: 14, pitch: 0, maxZoom: 17, minZoom: 3, projection: 'MERCATOR' }); /** * 创建地图对象后,开始加载地图资源,地图资源加载完成后触发load事件 */ map.on('load', function () { // 增加自定义数据源、自定义图层 addSources(); addLayers(); addMouseEvent(); }); function addMouseEvent() { var layerId = 'fillLayer' map.on('mouseenter', layerId, function(event) { // 参数说明: // (1)maxWidth,默认值240px,popup容器的最大宽度,超过后内容将滚动出现; var _popup = new minemap.Popup({closeOnClick: true, closeButton: false, anchor: 'top-left', offset: [15, 20], maxWidth: '100px'}) .setText("我是一个跟随鼠标指针信息窗体") .trackPointer() //此方法可以让popup跟随鼠标指针,使用此方法就不再需要使用setLngLat()方法 .addTo(map); popups.push(_popup); }) map.on('mouseleave', layerId, function(event) { for (let i = 0; i < popups.length; i++) popups[i].remove(); popups = [] }) } function addSources() { var center = map.getCenter(); var jsonData = { "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [[[center.lng, center.lat], [center.lng, center.lat + 0.006], [center.lng - 0.006, center.lat + 0.006], [center.lng - 0.006, center.lat], [center.lng, center.lat]]] } } ] }; map.addSource("fillSource", { "type": "geojson", "data": jsonData }); } function addLayers() { map.addLayer({ "id": "fillLayer", "type": "fill", "source": "fillSource", "layout": { "visibility": "visible", }, "paint": { "fill-color": "#00ff00", "fill-opacity": 0.8, "fill-outline-color": "#ff0000" }, "minzoom": 7, "maxzoom": 17.5 }); } </script> </body>
bug效果:
2、解决方案
他那边给的建议时换成move事件监听,持续的刷新弹窗展示
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>面图层</title> <link rel="stylesheet" href="http://minedata.com.cn/support/static/api/demo/js-api/zh/css/demo.css"> <!-- 引入MineMap API插件 --> <link rel="stylesheet" href="http://minedata.com.cn/minemapapi/v2.1.0/minemap.css"> <script src="http://minedata.com.cn/minemapapi/v2.1.0/minemap.js"></script> <style> #map { width: 100%; height: 100%; } </style> </head> <body> <div id="map"></div> <script> /** * 全局参数设置 */ minemap.domainUrl = 'http://minedata.com.cn'; minemap.dataDomainUrl = 'http://minedata.com.cn'; minemap.serverDomainUrl = 'http://minedata.com.cn'; minemap.spriteUrl = 'http://minedata.com.cn/minemapapi/v2.1.0/sprite/sprite'; minemap.serviceUrl = 'http://minedata.com.cn/service/'; minemap.key = '16be596e00c44c86bb1569cb53424dc9'; minemap.solution = 12877; var popups = []; var map = new minemap.Map({ container: 'map', style: 'http://minedata.com.cn/service/solu/style/id/12877', center: [116.46,39.92], zoom: 14, pitch: 0, maxZoom: 17, minZoom: 3, projection: 'MERCATOR' }); /** * 创建地图对象后,开始加载地图资源,地图资源加载完成后触发load事件 */ map.on('load', function () { // 增加自定义数据源、自定义图层 addSources(); addLayers(); addMouseEvent(); }); function addMouseEvent() { var layerId = 'fillLayer' map.on('mousemove', layerId, function(event) { for (let i = 0; i < popups.length; i++) popups[i].remove(); popups = [] // 参数说明: // (1)maxWidth,默认值240px,popup容器的最大宽度,超过后内容将滚动出现; var _popup = new minemap.Popup({closeOnClick: true, closeButton: false, anchor: 'top-left', offset: [15, 20], maxWidth: '1000px'}) .setText("我是一个跟随鼠标指针信息窗体") .setLngLat(event.lngLat) //.trackPointer() //此方法可以让popup跟随鼠标指针,使用此方法就不再需要使用setLngLat()方法 .addTo(map); popups.push(_popup); console.log(popups.length) }) map.on('mouseleave', layerId, function(event) { for (let i = 0; i < popups.length; i++) popups[i].remove(); popups = [] }) } function addSources() { var center = map.getCenter(); var jsonData = { "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [[[center.lng, center.lat], [center.lng, center.lat + 0.006], [center.lng - 0.006, center.lat + 0.006], [center.lng - 0.006, center.lat], [center.lng, center.lat]]] } } ] }; map.addSource("fillSource", { "type": "geojson", "data": jsonData }); } function addLayers() { map.addLayer({ "id": "fillLayer", "type": "fill", "source": "fillSource", "layout": { "visibility": "visible", }, "paint": { "fill-color": "#00ff00", "fill-opacity": 0.8, "fill-outline-color": "#ff0000" }, "minzoom": 7, "maxzoom": 17.5 }); } </script> </body>
3、数据加载的处理
我直接替换事件名称,改成事件源获取的坐标赋值处理弹窗
结果发现弹窗加载阻塞了.... 移动时会连续出现多个没关闭的弹窗
因为之前是enter进入事件,只有一次加载的时候
现在换成move事件会持续触发,请求就一直在执行,再加上async / await阻塞中持续
我感觉这个方式有点不对,然后猜测是接口响应阻塞导致的
那就这样,把接口的数据缓存起来,move事件有featureId挂着,那接口数据和featureId匹配即可
缓存池没找到这个区域的数据,就加载一次,放到池里面,然后渲染
下次触发时继续在池子找数据,不用再请求接口了
所以这个逻辑又拆成现在这段:
渲染的逻辑并不阻塞,效果是正常的了
this.mapInstance.on('mouseenter', featureId, async event => { /* 设置激活的Layer样式 */ this.mapInstance.setPaintProperty(featureId, 'fill-color', '#FFEA59') if (this.mapInstance.getLayer(pointId)) this.mapInstance.setLayoutProperty(pointId, 'visibility', 'none') this.mapInstance.setPaintProperty(lineId, 'line-color', { 'type': 'categorical', 'property': 'kind', 'stops': [[1, '#FFEA59'], [2, '#FFEA59']], 'default': '#02F4FF' }) }) this.mapInstance.on('mousemove', featureId, async event => { this.clearFeaturePopup() let featureInfo = this.featureWindowDataList.find(x => x['featureId'] === featureId) if (!featureInfo) { featureInfo = await this.getFeatureStationsInfo(coordinates, pointTarget) featureInfo['featureId'] = featureId this.featureWindowDataList.push(featureInfo) } this.openFeatureInfoWindow(featureInfo, event['lngLat']) /* 校验是否存在交叉区域 */ this.fixConflictFeaturePopup() }) this.mapInstance.on('mouseleave', featureId, event => { /* 如果存在,删除这个弹窗 */ this.clearFeaturePopup() /* 恢复默认的Layer样式 */ this.mapInstance.setPaintProperty(featureId, 'fill-color', '#059ded') if (this.mapInstance.getLayer(pointId)) this.mapInstance.setLayoutProperty(pointId, 'visibility', 'visible') this.mapInstance.setPaintProperty(lineId, 'line-color', { 'type': 'categorical', 'property': 'kind', 'stops': [[1, '#02F4FF'], [2, '#02F4FF']], 'default': '#02F4FF' }) })