文章目录
- 一、项目背景
- 二、页面效果
- 三、代码
- 1.ElectronicMap.vue
- 2.TransferDeskRSSIMap.vue
- 3.Map.js
- 4.src/stores/index.js Vuex存储属性
- 四、注意点
- 本人其他相关文章链接
一、项目背景
项目采用:vue3+java+Arco Design+SpringBoot+OpenStreetMap 数据的地图切片服务
。我们的项目会上报或者手动添加多台中转台,中转台有属性:经度、纬度、海拔。我们想在在线/离线地图上展示设备信息。
二、页面效果
电子地图
中转台RSSI地图
点击工具,测距
点击工具,开启中转台覆盖范围
三、代码
1.ElectronicMap.vue
<template><layout_2 style="position: relative"><div id="electronic_map"></div><div class="transparent-box"></div><div class="transparent-box-bottom"></div><div class="--search-line in-map-tl"><div><div class="key">{{$t('TreeViewRepeater')}} :</div><div class="val"><a-tree-select class="arco-tree-select --arco-select" style="width: 230px":field-names="{ key: 'serialNo', title: 'name', children: 'children' }" :data="treeSelectNodeData":multiple="true" :tree-checkable="true" tree-checked-strategy="child" :max-tag-count="1"v-model:model-value="param.repeaterSNs"></a-tree-select></div></div><a-button class="huge" @click="queryTopoView" type="primary"><template #icon><icon-search size="18" /></template>{{$t('Query')}}</a-button><a-checkbox :default-checked="isChecked" @change="changeOnlineMap">{{$t('OnLineMap')}}</a-checkbox></div><statistic-repeater ref="statisticRepeaterRef" @click-transfer-desk="queryAllDeviceListByTypeFunction"></statistic-repeater></layout_2><base-info v-model:visible="baseInfoShow" v-if="baseInfoShow" ref="baseInfoRef" @refresh-flag="successFresh"></base-info><site-alias v-model:visible="siteAliasShow" v-if="siteAliasShow"></site-alias><monitor-alarm v-model:visible="monitorAlarmShow" v-if="monitorAlarmShow" ref="monitorAlarmRef"></monitor-alarm><device-param v-model:visible="deviceParamShow" v-if="deviceParamShow" ref="deviceParamRef"></device-param><a-popover arrow-class="--arrow-none" v-model:popup-visible="popoverVisible" :style="overlayStyle" content-class="--arco-popover-popup-content"@mouseenter="handlePopoverMouseEnter" @mouseleave="handlePopoverMouseLeave"><template #content><div class="dropdownBasic"><div class="dropdownItemitem"><div class="baselayersFWrapper"><svg-loader class="baselayersFIcon" name="base-info"></svg-loader></div><div class="text" @click="openBaseInfo">{{$t('BaseInfo')}}</div></div><div class="dropdownItemitem"><div class="baselayersFWrapper"><svg-loader class="baselayersFIcon" name="alarm-monitor"></svg-loader></div><div class="text" @click="openMonitorAlarm">{{$t('MonitoringAlarm')}}</div></div><div class="dropdownItemitem"><div class="baselayersFWrapper"><svg-loader class="baselayersFIcon" name="device-param"></svg-loader></div><div class="text" @click="openDeviceParam">{{$t('ParameterSetting')}}</div></div></div></template></a-popover><a-modal v-model:visible="showVisible" @ok="handleOk" :hide-cancel="true"><template #title>{{$t('Prompt')}}</template><div>{{$t('RepeaterOffline')}}</div></a-modal>
</template><script setup>
import Layout_2 from "@/views/pages/_common/layout_2.vue";
import {computed, inject, nextTick, onMounted, onUnmounted, provide, reactive, ref} from "vue";
import {LeafletMap} from "@/views/pages/_class/Map";
import {qryTransferNodeList} from "@/views/pages/topology/_request";
import {queryButtonValue} from "@/views/pages/_common/enum";
import StatisticRepeater from "@/views/pages/topology/StatisticRepeater.vue";
import SiteAlias from "@/views/pages/topology/topologyView/SiteAlias.vue";
import BaseInfo from "@/views/pages/topology/topologyView/BaseInfo.vue";
import MonitorAlarm from "@/views/pages/topology/topologyView/MonitorAlarm.vue";
import DeviceParam from "./topologyView/DeviceParam.vue";
import {useStore} from "@/stores";
import {queryAllDeviceList, queryAllDeviceListByType} from "@/views/pages/system/system.js";const initTreeLayout = ref(1)
provide('initTreeLayout', initTreeLayout.value)
const isChecked = ref(true);
const mapClass = ref(new LeafletMap())
const treeSelectNodeData = ref([])
const t = inject('t')
const deviceManageList = ref([])
const statisticRepeaterRef = ref(null)
const param = reactive({repeaterSNs: [],
})const computedPopoverVisible = computed(() => {return useStore().popoverVisible;
})
const popoverVisible = computedPopoverVisible
const computedPopoverPosition = computed(() => {return useStore().popoverPosition;
})
const popoverPosition = computedPopoverPosition
const computedSelectTopoNode = computed(() => {return useStore().selectTopoNode;
})
const selectTopoNode = computedSelectTopoNode
const baseInfoShow = ref(false)
const siteAliasShow = ref(false)
const monitorAlarmShow = ref(false)
const deviceParamShow = ref(false)
const showVisible = ref(false)
const sipLoading = ref(true)const baseInfoRef = ref(null)
const deviceParamRef = ref(null)
const monitorAlarmRef = ref(null)const queryAllDeviceListByTypeFunction = (index) => {queryAllDeviceListByType({"rptState": index}).then(response => {if (response.data) {deviceManageList.value = response.data;mapClass.value.handleAllMarkerInMap(deviceManageList.value)}})
}const queryTopoView = async () => {await getAllDeviceManageListFunction();mapClass.value.handleAllMarkerInMap(deviceManageList.value)
}const openBaseInfo = () => {baseInfoShow.value = truesipLoading.value = truenextTick(() => {const repeater = useStore().websocketRepeaterList.find(repeater => repeater.serialNo === selectTopoNode.value.serialNo);const rptState = repeater ? repeater.rptState : null;baseInfoRef.value.setData(useStore().selectTopoNode.serialNo, rptState)})
}const openMonitorAlarm = () => {if (useStore().selectTopoNode.rptState != 1 && useStore().selectTopoNode.rptState != 2) {showVisible.value = truereturn}monitorAlarmShow.value = truenextTick(() => {monitorAlarmRef.value.setRssiId(useStore().selectTopoNode.serialNo)})
}const openDeviceParam = () => {if (useStore().selectTopoNode.rptState != 1 && useStore().selectTopoNode.rptState != 2) {showVisible.value = truereturn}deviceParamShow.value = truenextTick(() => {deviceParamRef.value.baseSettingXptFunction(useStore().selectTopoNode.serialNo)})}const handlePopoverMouseEnter = () => {useStore().popoverVisible = true;
};const handlePopoverMouseLeave = () => {useStore().popoverVisible = false;
};const overlayStyle = computed(() => ({position: 'absolute',top: `${popoverPosition.value.top}px`,left: `${popoverPosition.value.left}px`,zIndex: 1000,
}));const successFresh = () => {baseInfoShow.value = false
}const handleOk = () => {showVisible.value = false
}const getTransferNodeList = () => {const principal = sessionStorage.getItem('principal');if (principal) {const principalObject = JSON.parse(principal);qryTransferNodeList({"userName": principalObject.userName}).then(response => {if (response.data) {treeSelectNodeData.value = [{serialNo: '-1',name: t(queryButtonValue[22]),children: response.data}]}})}
}
const changeOnlineMap = (val) => {mapClass.value.changeOnlineMap(val)
}const getAllDeviceManageListFunction = async () => {await queryAllDeviceList({"serialNoArr": param.repeaterSNs}).then(response => {if (response.data) {deviceManageList.value = response.data;}})
}let webSocket = nullconst connectWebSocket = (url) => {if (webSocket) webSocket.close()webSocket = new WebSocket(url)webSocket.onopen = () => console.log('ElectronicMap.vue WebSocket已连接')webSocket.onmessage = handleWebSocketMessagewebSocket.onclose = () => console.log('ElectronicMap.vue WebSocket已关闭')webSocket.onerror = (error) => console.error('ElectronicMap.vue WebSocket错误:', error)
}const handleWebSocketMessage = (event) => {try {const message = JSON.parse(event.data)const index = useStore().websocketRepeaterList.findIndex(item => item.serialNo === message.serialNo);if (index !== -1) {useStore().websocketRepeaterList[index] = message;} else {useStore().websocketRepeaterList.push(message);}const receiveRepeaterId = message.repeaterIdconst receiveRptState = message.rptStatedeviceManageList.value.forEach(repeater => {if (repeater.repeaterId == receiveRepeaterId) repeater.rptState = receiveRptState;})mapClass.value.handleAllMarkerInMap(deviceManageList.value)statisticRepeaterRef.value.queryTypeCountFunction();} catch (error) {console.error('ElectronicMap.vue WebSocket消息处理错误:', error)}
}onMounted(async () => {mapClass.value.initMap('electronic_map', deviceManageList.value)await getAllDeviceManageListFunction()mapClass.value.handleAllMarkerInMap(deviceManageList.value)changeOnlineMap(true);connectWebSocket('/ws/topoView');
})onUnmounted(() => {if (webSocket) webSocket.close()
})const init = () => {getTransferNodeList();
}
init()
</script><style scoped lang="less">
#electronic_map {height: 100%;
}
.transparent-box {position: absolute;z-index: 400;pointer-events: none;background-image: linear-gradient(to bottom, #FFFFFF, transparent);opacity: 0.8;top: 0;height: 80px;width: 100%;
}
.transparent-box-bottom {position: absolute;z-index: 400;pointer-events: none;background-image: linear-gradient(to top, #FFFFFF, transparent);opacity: 0.8;bottom: 0;height: 80px;width: 100%;
}
.in-map-tl {position: absolute;z-index: 401;left: 20px;top: 17px;
}
.in-map-rt {position: absolute;display: flex;flex-direction: column;gap: 20px;z-index: 401;top: 16px;right: 20px;.card-item {box-sizing: border-box;padding: 16px;border-radius: 12px;background: #F7F9FC;box-shadow: 0px 3px 6px 0px rgba(193, 203, 214, 0.70);width: 172px;cursor: pointer;&.active {border-radius: 12px;border: 2px solid #7FAFFF;padding: 14px;background: #FAFCFF;box-shadow: 0px 3px 6px 0px rgba(193, 203, 214, 0.70);}&-title {color: #202B40;font-family: "PingFang SC";font-size: 14px;font-style: normal;font-weight: 400;line-height: 22px;}&-count {position: relative;margin-top: 4px;height: 48px;color: #202B40;font-family: Roboto;font-size: 32px;font-style: normal;font-weight: 600;line-height: 48px;&-percentage {position: absolute;top: 19px;right: 0;color: #A7B1C6;text-align: right;font-family: "PingFang SC";font-size: 14px;font-style: normal;font-weight: 400;line-height: 22px;}}.percentage-chart {}}
}
</style>
<style>
.custom-popupp .leaflet-popup-content-wrapper {background: #EFF8FF;width: 330px
}.custom-popupp .leaflet-popup-tip-container {display: none;
}.--arrow-none {display: none
}.--arco-popover-popup-content {box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.1), 0px 8px 24px rgba(0, 0, 0, 0.1);border-radius: 12px;background: linear-gradient(180deg, #eff8ff, #fff);border: 1.5px solid #fff;box-sizing: border-box;width: 170px;
}.baselayersFIcon {width: 16px;position: relative;height: 16px;overflow: hidden;flex-shrink: 0;
}.baselayersFWrapper {width: 24px;border-radius: 80px;background-color: #fff;border: 1px solid #dfdfdf;box-sizing: border-box;height: 24px;display: flex;flex-direction: row;align-items: center;justify-content: center;padding: 8px;
}.text {flex: 1;position: relative;line-height: 22px;display: inline-block;height: 22px;cursor: pointer;
}.dropdownItemitem {align-self: stretch;height: 36px;display: flex;flex-direction: row;align-items: center;justify-content: flex-start;padding: 5px 12px 5px 16px;box-sizing: border-box;gap: 8px;
}.dropdownItemitem:hover {background: #E8F7FF;font-weight: bold;color: #3348FF;
}.dropdownBasic {width: 100%;position: relative;display: flex;flex-direction: column;align-items: center;justify-content: center;padding: 8px 0px;text-align: left;font-size: 14px;color: #202b40;font-family: 'PingFang SC';
}
</style>
2.TransferDeskRSSIMap.vue
<template><layout_2 style="position: relative"><div id="transfer_rssi_map"></div><div class="legend"><div class="legend-item"><div class="legend-item-color" style="background-color: #00AB07"></div><div class="legend-item-text">Good</div></div><div class="legend-item"><div class="legend-item-color" style="background-color: #3374FF"></div><div class="legend-item-text">Normal</div></div><div class="legend-item"><div class="legend-item-color" style="background-color: #DC6300"></div><div class="legend-item-text">Average</div></div><div class="legend-item"><div class="legend-item-color" style="background-color: #913DFF"></div><div class="legend-item-text">Bad</div></div><div class="legend-item"><div class="legend-item-color" style="background-color: #F53F3F"></div><div class="legend-item-text">Very Bad</div></div></div><div class="transparent-box"></div><div class="--search-line search-in-map-tl"><div><div class="key">发送方ID </div><div class="val"><a-range-pickerstyle="width: 280px":allow-clear="false"v-model="param.timeRange":disabled-date="disabledDate"><template #suffix-icon><svg-loader :width="20" :height="20" name="clock"></svg-loader></template><template #separator><svg-loader:width="16":height="16"name="arrow-right"></svg-loader></template></a-range-picker></div></div><div><div class="key">目的ID </div><div class="val"><a-tree-selectclass="arco-tree-select --arco-select"style="width: 230px":field-names="{key: 'serialNo',title: 'name',children: 'children',}":data="treeSelectNodeData":multiple="true":tree-checkable="true"tree-checked-strategy="child":max-tag-count="1"v-model:model-value="param.repeaterSNs"></a-tree-select></div></div><a-button class="huge" @click="search" type="primary"><template #icon><icon-search size="18" /> </template>{{ $t(queryButtonValue[2]) }}</a-button><a-checkbox :default-checked="isChecked" @change="changeOnlineMap">{{$t(queryButtonValue[5])}}</a-checkbox></div><div class="search-in-map-br"><a-tooltip :content="居中显示" position="left"><div class="btn-item"><img :src="centerImg" /></div></a-tooltip><a-tooltip :content="测距" position="left"><divclass="btn-item":class="{ active: drawDistanceSwitch }"@click="drawDistanceSwitchChange"><img :src="distanceImg" /></div></a-tooltip><a-tooltip :content="开启中转台覆盖范围" position="left"><div class="btn-item":class="{ active: rangeFlag }"@click="rangeClick"><img :src="coverageImg" /></div></a-tooltip></div></layout_2>
</template><script setup>
import Layout_2 from "@/views/pages/_common/layout_2.vue";
import { LeafletMap } from "@/views/pages/_class/Map";
import { onMounted, reactive, ref, inject } from "vue";
import * as moment from "moment/moment";
import centerImg from "@/assets/img/center.png";
import distanceImg from "@/assets/img/distance.png";
import coverageImg from "@/assets/img/coverage.png";
import {qryTransferNodeList,qryTransferRSSIList,
} from "@/views/pages/topology/_request";
import { commonResponse } from "@/views/pages/_common";
import { TransferDesk } from "@/views/pages/_class/TransferDesk";
import { queryButtonValue, queryColumnValue } from "@/views/pages/_common/enum";
import {queryAllDeviceList} from "@/views/pages/system/system.js";
const t = inject("t");
const isChecked = ref(true);
const param = reactive({timeRange: [null, null],repeaterSNs: [],
});
let reqParam = {startTime: null,endTime: null,repeaterSNs: [],
};
const mapClass = ref(new LeafletMap());
const transferDeskClass = ref(new TransferDesk({ mapClass }));const disabledDate = (date) => {return date.getTime() > moment().format("x");
};const changeOnlineMap = (val) => {mapClass.value.changeOnlineMap(val);
};const drawDistanceSwitch = ref(false);
const drawDistanceSwitchChange = () => {drawDistanceSwitch.value = !drawDistanceSwitch.value;if (drawDistanceSwitch.value) {mapClass.value.openDrawDistance();} else {mapClass.value.closeDrawDistance();}
};const treeSelectNodeData = ref([]);
const getTransferNodeList = () => {const principal = sessionStorage.getItem('principal');if (principal) {const principalObject = JSON.parse(principal);qryTransferNodeList({"userName": principalObject.userName}).then(response => { commonResponse({response,onSuccess: () => {treeSelectNodeData.value = [{serialNo: "-1",name: t(queryButtonValue[22]),children: response.data,},];},});});}
};const transferRssiMap = ref(new Map());
const getRSSIList = () => {qryTransferRSSIList({...reqParam,}).then((response) => {commonResponse({response,onSuccess: () => {handleTransferDesk(response.data);},});});
};const handleTransferDesk = (data) => {transferRssiMap.value.clear();data.repeaterList.forEach((item) => {item.longitude = item.lng;item.latitude = item.lat;transferRssiMap.value.set(item.serialNo, item);});data.queryRssiResults.forEach((item) => {let obj = transferRssiMap.value.get(item.repeaterSN);obj = {...obj,...item,};if (item.pos?.longitude && item.pos?.latitude) {obj.longitude = item.pos?.longitude;obj.latitude = item.pos?.latitude;}transferRssiMap.value.set(item.repeaterSN, obj);});for (let [key, val] of transferRssiMap.value) {handleTransferDeskInMap(val);}
};const handleTransferDeskInMap = (item) => {transferDeskClass.value.handleTransferDeskInMap(item);// if (item.)
};const search = () => {reqParam = {...reqParam,...param,};reqParam.startTime = moment(reqParam.timeRange[0]).format("YYYY-MM-DD");reqParam.endTime = moment(reqParam.timeRange[1]).format("YYYY-MM-DD");delete reqParam.timeRange;getRSSIList();
};const getAllTransferInMap = () => {queryAllDeviceList({"serialNoArr": param.repeaterSNs}).then((response) => {commonResponse({response,onSuccess: () => {mapClass.value.handleAllMarkerInMap(response.data)},});});
}const rangeFlag = ref(false)
const rangeClick = () => {if (rangeFlag.value) {mapClass.value.closeCoverageRange()} else {mapClass.value.openCoverageRange()}rangeFlag.value = !rangeFlag.value
}const init = () => {param.timeRange = [moment().add(-9, "days"), moment()];param.repeaterSNs = [];getTransferNodeList();getAllTransferInMap()
};init();onMounted(() => {mapClass.value.initMap("transfer_rssi_map");changeOnlineMap(true);
});
</script><style scoped lang="less">
#transfer_rssi_map {height: 100%;
}
.transparent-box {position: absolute;z-index: 400;pointer-events: none;background-image: linear-gradient(to bottom, #ffffff, transparent);opacity: 0.8;top: 0;height: 80px;width: 100%;
}
.legend {position: absolute;box-sizing: border-box;display: flex;flex-direction: column;gap: 8px;border-radius: 6px;padding: 12px 14px;z-index: 401;left: 20px;bottom: 14px;width: 134px;height: 166px;background-color: rgba(255, 255, 255, 0.70);stroke-width: 1.5px;stroke: #FFF;filter: drop-shadow(0px 8px 24px rgba(0, 0, 0, 0.10));backdrop-filter: blur(4px);&-item {position: relative;height: 22px;&-color {position: absolute;top: 6px;border-radius: 5px;height: 10px;width: 10px;}&-text {margin-left: 18px;line-height: 22px;color: var(--80, #202B40);font-family: "PingFang SC";font-size: 13px;font-style: normal;font-weight: 400;}}
}
.transparent-box-bottom {position: absolute;z-index: 400;pointer-events: none;background-image: linear-gradient(to top, #ffffff, transparent);opacity: 0.8;bottom: 0;height: 80px;width: 100%;
}
.search-in-map-tl {position: absolute;z-index: 401;left: 20px;top: 17px;
}
.search-in-map-br {position: absolute;display: flex;flex-direction: column;gap: 20px;z-index: 401;right: 32px;bottom: 36px;width: 42px;.btn-item {box-sizing: border-box;padding: 9px;border-radius: 21px;height: 42px;width: 42px;background-color: #404750;cursor: pointer;&:hover {background-color: #3348ff;}&.active {background-color: #3348ff !important;}}
}
</style>
3.Map.js
import * as L from 'leaflet'
import 'leaflet/dist/leaflet.css'
import './map.less'
import deviceImg0 from '@/assets/img/device-0.png'
import {ref} from "vue";
import deviceImg1 from "@/assets/img/device-1.png";
import deviceImg2 from "@/assets/img/device-2.png";
import {useStore} from "@/stores";const baseUrl = ref("")export class LeafletMap {constructor() {baseUrl.value = window.location.originthis.markers = []; // 用于存储地图上的标记}mapUrl = {online: 'https://a.tile.geofabrik.de/15173cf79060ee4a66573954f6017ab0/{z}/{x}/{y}.png',offline: '/mapShow/{z}/{x}/{y}.png'}getMap = () => {return this.map}rangeLayerGroup = L.layerGroup()initMap (containerId, list) {document.querySelector(`#${containerId}`).innerHTML += `<div class="latlng-box"><div class="inline">纬度:</div><div class="inline" id="${containerId}_lat"></div>, <div class="inline">经度:</div><div class="inline" id="${containerId}_lng"></div></div>`const latBox = document.querySelector(`#${containerId}_lat`)const lngBox = document.querySelector(`#${containerId}_lng`)this.map = L.map(containerId, {center: [45.7531, 126.6343],zoom: 5,minZoom: 1,maxZoom: 16,contextmenu: true,contextmenuWidth: 160,contextmenuHeight: 640,});this.tileLayer = L.tileLayer(this.mapUrl.offline)this.tileLayer.addTo(this.map)this.map.addEventListener('mousemove', (e) => {latBox.innerHTML = e.latlng.latlngBox.innerHTML = e.latlng.lng})this.rangeLayerGroup.addTo(this.map)list?.forEach(item => {this.addMarkerWithPopup(item);})}deviceMap = new Map()handleAllMarkerInMap = (list) => {// 清除之前的设备标记this.markers.forEach(marker => marker.remove());this.markers = [];this.deviceMap.clear();list?.forEach(item => {this.deviceMap.set(item.serialNo, item);this.addMarkerWithPopup(item);});this.map.invalidateSize(); // 更新地图视图}addMarkerWithPopup (info) {const {lat, lng, rptState, alias} = infoif (!lat || !lng) returnconst marker = L.marker([lat, lng], {icon: L.icon({iconUrl: rptState == 1 ? deviceImg1 : rptState == 2 ? deviceImg2 : deviceImg0, // 使用 deviceImg0 作为图标iconSize: [32, 32], // 设置图标的大小iconAnchor: [16, 16] // 设置图标的锚点})}).addTo(this.map);this.markers.push(marker); // 将标记添加到数组中// 添加悬停事件处理逻辑marker.on('mouseover', () => {marker.bindTooltip(alias, ).openTooltip();});const handleNodeClick = (params) => {event.preventDefault();useStore().selectTopoNode = info;// 计算弹出框的位置const chartDom = document.querySelector('#electronic_map');const chartRect = chartDom.getBoundingClientRect();const offsetX = params.containerPoint.x;const offsetY = params.containerPoint.y;useStore().popoverPosition = {top: chartRect.top + offsetY + window.scrollY,left: chartRect.left + offsetX + window.scrollX,};useStore().popoverVisible = true;}marker.on('contextmenu', handleNodeClick);}changeOnlineMap = (onlineStatus) => {if (onlineStatus) {this.tileLayer.setUrl(this.mapUrl.online)} else {this.tileLayer.setUrl(this.mapUrl.offline)}}/** 测距功能 start */lastClickPoint = nulltotalDistance = 0pointMarkerArr = []lastMovePointObj = nulllastMoveLineObj = nulldrawPermission = falseopenDrawDistance = () => {this.map.addEventListener('click', (e) => {this.drawPermission = trueconst { lat, lng } = e.latlngif (!this.lastClickPoint) {const marker = L.marker([lat, lng], { icon: L.divIcon({ className: 'point-start' }) })marker.bindTooltip('0km', {offset: [0, -14],permanent: true,direction: 'top'}).openTooltip()marker.addTo(this.map)this.pointMarkerArr.push(marker)} else {const marker = L.marker([lat, lng], { icon: L.divIcon({ className: 'point-process' }) })const distance = this.calculateDistance(this.lastClickPoint[0], this.lastClickPoint[1], lat, lng) / 1000this.totalDistance += distancemarker.bindTooltip(`${this.totalDistance.toFixed(3)}km`, {offset: [0, -14],permanent: true,direction: 'top'}).openTooltip()marker.addTo(this.map)const polyline = L.polyline([this.lastClickPoint, [lat, lng]], { color: '#FFA100' })polyline.addTo(this.map)this.pointMarkerArr.push(marker)this.pointMarkerArr.push(polyline)}this.lastClickPoint = [lat, lng]})this.map.addEventListener('mousemove', (e) => {if (!this.drawPermission) returnconst { lat, lng } = e.latlngif (this.lastClickPoint) {const polyline = L.polyline([this.lastClickPoint, [lat, lng]], {color: '#FFA100',dashArray: '8'})const marker = L.marker([lat, lng], { icon: L.divIcon({ className: 'point-start' }) })marker.bindTooltip(`右键取消`, {offset: [0, -14],permanent: true,direction: 'top'}).openTooltip()this.lastMoveLineObj && this.lastMoveLineObj.remove()this.lastMovePointObj && this.lastMovePointObj.remove()polyline.addTo(this.map)marker.addTo(this.map)this.lastMoveLineObj = polylinethis.lastMovePointObj = marker}})this.map.addEventListener('contextmenu', () => {this.drawPermission = falsethis.lastMoveLineObj && this.lastMoveLineObj.remove()this.lastMovePointObj && this.lastMovePointObj.remove()this.lastMoveLineObj = nullthis.lastMovePointObj = null})}closeDrawDistance = () => {this.map.removeEventListener('click')this.map.removeEventListener('mousemove')this.lastClickPoint = nullthis.pointMarkerArr.forEach(item => item.remove())this.pointMarkerArr = []this.lastMoveLineObj && this.lastMoveLineObj.remove()this.lastMovePointObj && this.lastMovePointObj.remove()this.lastMoveLineObj = nullthis.lastMovePointObj = nullthis.totalDistance = 0}// Vincenty公式进行计算(更准确但计算复杂度较高)calculateDistance (lat1, lon1, lat2, lon2) {const a = 6378137; // 长轴半径,单位为米const b = 6356752.314245; // 短轴半径,单位为米const f = 1 / 298.257223563; // 扁率const L = (lon2 - lon1) * Math.PI / 180;const U1 = Math.atan((1 - f) * Math.tan(lat1 * Math.PI / 180));const U2 = Math.atan((1 - f) * Math.tan(lat2 * Math.PI / 180));const sinU1 = Math.sin(U1), cosU1 = Math.cos(U1);const sinU2 = Math.sin(U2), cosU2 = Math.cos(U2);let iterLimit = 100;let lambda = L, lambdaP, sinSigma, cosSigma, sigma, sinAlpha, cosSqAlpha, cos2SigmaM;do {const sinLambda = Math.sin(lambda), cosLambda = Math.cos(lambda);sinSigma = Math.sqrt((cosU2 * sinLambda) * (cosU2 * sinLambda) +(cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) * (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda));if (sinSigma === 0) {return 0; // 两点重合}cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;sigma = Math.atan2(sinSigma, cosSigma);sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;cosSqAlpha = 1 - sinAlpha * sinAlpha;cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha;const C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha));lambdaP = lambda;lambda = L + (1 - C) * f * sinAlpha *(sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM)));} while (Math.abs(lambda - lambdaP) > 1e-12 && --iterLimit > 0);if (iterLimit === 0) {return NaN; // 迭代次数过多}const uSq = cosSqAlpha * (a * a - b * b) / (b * b);const A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));const B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));const deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 *(cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) -B / 6 * cos2SigmaM * (-3 + 4 * sinSigma * sinSigma) *(-3 + 4 * cos2SigmaM * cos2SigmaM)));const distance = b * A * (sigma - deltaSigma);return distance;}/** 测距功能 end */openCoverageRange = () => {for (const [key, value] of this.deviceMap) {const {lat, lng} = valueL.circle([lat, lng], {radius: 100000,color: 'rgba(51, 72, 255, 0.50)',fillColor: 'rgba(51, 116, 255, 0.30)',}).addTo(this.rangeLayerGroup) // 磊哥说写个假的}}closeCoverageRange = () => {this.rangeLayerGroup.clearLayers()}
}
4.src/stores/index.js Vuex存储属性
/*** @Name:* @Author:贾志博* @description:*/
import {defineStore} from "pinia";
import {ref} from 'vue'export const useStore = defineStore('main', () => {const mode = ref(0)const setMode = (modeVal) => {mode.value = modeVal}const getMode = () => {return mode.value}const openMenuItem = ref([]);const setOpenMenuItemFunction = (modeVal) => {openMenuItem.value = modeVal}const getOpenMenuItemFunction = () => {return openMenuItem}const selectedMenuItemKey = ref(null);const setSelectedMenuItemKeyFunction = (modeVal) => {selectedMenuItemKey.value = modeVal}const getSelectedMenuItemKeyFunction = () => {return selectedMenuItemKey}const selectedMenuKey = ref("");const setSelectedMenuKeyFunction = (modeVal) => {selectedMenuKey.value = modeVal}const getSelectedMenuKeyFunction = () => {return selectedMenuKey}const hasAuth = ref(false)const routes = ref([])const popoverVisible = ref(false);const popoverPosition = ref({ top: 0, left: 0 });const selectTopoNode = ref(null)const websocketRepeaterList = ref([])return {getMode,setMode,setSelectedMenuKeyFunction,getSelectedMenuKeyFunction,setSelectedMenuItemKeyFunction,getSelectedMenuItemKeyFunction,setOpenMenuItemFunction,getOpenMenuItemFunction,hasAuth,routes,popoverVisible,popoverPosition,selectTopoNode,websocketRepeaterList,}
})
四、注意点
注意点1:
地图分在线地图/离线地图,离线地图需要上传瓦片地图。
注意点2:
在线地图采用,在线地图服务:https://a.tile.geofabrik.de/
注意点3:
使用OpenStreetMap地图步骤
要获取类似于 https://a.tile.geofabrik.de/15173cf79060ee4a66573954f6017ab0/{z}/{x}/{y}.png 的地图切片 URL,您可以按照以下步骤进行:
选择地图服务提供商
您可以选择不同的地图服务提供商,Geofabrik 是一个提供基于 OpenStreetMap 数据的切片服务的选项。其他常见的提供商包括:
- OpenStreetMap: https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
- Mapbox: 需要注册并获取 API 密钥。
- Carto: 需要注册并获取 API 密钥。
-
确定区域和数据集
如果您希望使用 Geofabrik 的服务,您需要确定您想要的地图区域。Geofabrik 提供了不同区域的切片,您可以在其网站上找到这些区域。通常,URL 中的标识符(例如 15173cf79060ee4a66573954f6017ab0)对应于特定的区域。 -
获取区域的切片 URL,访问 Geofabrik 网站
- 访问 Geofabrik 的切片服务页面:
- 前往 Geofabrik 网站。
- 选择区域:
- 在网站上,您可以选择特定的区域(如国家或城市)以获取相应的切片服务。
- 查找切片服务的 URL:
- 在选择的区域页面上,您通常可以找到用于该区域的切片服务 URL,类似于 https://a.tile.geofabrik.de/{区域标识}/{z}/{x}/{y}.png。
其他注意事项
- 使用条款: 在使用任何地图切片服务之前,请确保遵循其使用条款和条件,尤其是在商业应用中。
- API 密钥: 某些地图服务(如 Mapbox 和 Carto)需要您注册并获取 API 密钥才能使用其服务。
通过上述步骤,您可以获取并使用类似于 https://a.tile.geofabrik.de/15173cf79060ee4a66573954f6017ab0/{z}/{x}/{y}.png 的地图切片 URL。
注意点4:
地图下方会实时显示鼠标的经纬度信息。
注意点5:
地图右下角还有3个小工具:居中显示、测距、开启中转台覆盖范围
- 居中显示:未开发
- 测距:已开发,
- 开启中转台覆盖范围:这个是用户会配置颜色范围
操作手册中有详细介绍
注意点6:
由于代码比较乱,这里详细介绍下具体使用。
电子地图页面显示
<div id="electronic_map"></div>import {onMounted, ref} from "vue";
import {LeafletMap} from "@/views/pages/_class/Map";const mapClass = ref(new LeafletMap())
const deviceManageList = ref([])
const isChecked = ref(true);const changeOnlineMap = (val) => {mapClass.value.changeOnlineMap(val)
}onMounted(async () => {mapClass.value.initMap('electronic_map', deviceManageList.value)mapClass.value.handleAllMarkerInMap(deviceManageList.value)changeOnlineMap(true);
})
右下角工具
<div class="search-in-map-br"><a-tooltip :content="$t(queryColumnValue[25])" position="left"><div class="btn-item"><img :src="centerImg" /></div></a-tooltip><a-tooltip :content="$t(queryColumnValue[26])" position="left"><divclass="btn-item":class="{ active: drawDistanceSwitch }"@click="drawDistanceSwitchChange"><img :src="distanceImg" /></div></a-tooltip><a-tooltip :content="$t(queryColumnValue[27])" position="left"><div class="btn-item":class="{ active: rangeFlag }"@click="rangeClick"><img :src="coverageImg" /></div></a-tooltip>
</div>import centerImg from "@/assets/img/center.png";
import distanceImg from "@/assets/img/distance.png";
import coverageImg from "@/assets/img/coverage.png";const drawDistanceSwitch = ref(false);
const rangeFlag = ref(false)const drawDistanceSwitchChange = () => {drawDistanceSwitch.value = !drawDistanceSwitch.value;if (drawDistanceSwitch.value) {mapClass.value.openDrawDistance();} else {mapClass.value.closeDrawDistance();}
};const rangeClick = () => {if (rangeFlag.value) {mapClass.value.closeCoverageRange()} else {mapClass.value.openCoverageRange()}rangeFlag.value = !rangeFlag.value
}
本人其他相关文章链接
1.vue3 开发电子地图功能
2.vue java 实现大地图切片上传
3.java导入excel更新设备经纬度度数或者度分秒
4.快速上手Vue3国际化 (i18n)