From b42ea4328de7e9e1a50ac30262269e12a029fd3e Mon Sep 17 00:00:00 2001 From: air <30444667+sszdot@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:12:28 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E7=B1=BB=20=20=E5=9E=8B=E3=80=91?= =?UTF-8?q?=EF=BC=9Afeat=20=E3=80=90=E5=8E=9F=20=20=E5=9B=A0=E3=80=91?= =?UTF-8?q?=EF=BC=9A1=E9=9B=86=E7=BE=A4=20=E7=82=B9=E9=A3=9E=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=AE=8C=E5=96=84=20=E5=8F=91=E5=91=BD=E4=BB=A4=20?= =?UTF-8?q?=E8=A7=84=E5=88=92=E5=BC=95=E5=AF=BC=E7=BA=BF=E7=AD=89=20=202?= =?UTF-8?q?=E5=8D=95=E4=BD=93=E7=82=B9=E9=A3=9E=E5=A2=9E=E5=8A=A0=20?= =?UTF-8?q?=E8=A7=84=E5=88=92=E5=BC=95=E5=AF=BC=E7=BA=BF=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=20=203=E9=9B=86=E7=BE=A4=E9=A3=9E=E6=9C=BA=E8=BD=A8=E8=BF=B9?= =?UTF-8?q?=E6=98=BE=E7=A4=BAbug=20=E3=80=90=E8=BF=87=20=20=E7=A8=8B?= =?UTF-8?q?=E3=80=91=EF=BC=9A=20=E3=80=90=E5=BD=B1=20=20=E5=93=8D=E3=80=91?= =?UTF-8?q?=EF=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 类型 包含: # feat:新功能(feature) # fix:修补bug # docs:文档(documentation) # style: 格式(不影响代码运行的变动) # refactor:重构(即不是新增功能,也不是修改bug的代码变动) # test:增加测试 # chore:构建过程或辅助工具的变动 --- src/components/MapBox.vue | 177 ++++++++++++------ src/store/index.js | 14 ++ src/utils/index.js | 120 +++++++++--- .../layout/components/main/home/index.vue | 76 ++++---- .../layout/components/main/planes/index.vue | 23 ++- .../layout/components/main/planes/swarm.vue | 152 ++++++++++----- 6 files changed, 384 insertions(+), 178 deletions(-) diff --git a/src/components/MapBox.vue b/src/components/MapBox.vue index 96e715b..6e9b21f 100644 --- a/src/components/MapBox.vue +++ b/src/components/MapBox.vue @@ -748,8 +748,7 @@ export default { * @description: 创建飞机轨迹 ps:原理删除之前的轨迹 重新绘制 用于实时轨迹 * @param {arr} coordinatesArray 飞机经纬高度数组 */ - createPathWithArray (coordinatesArray) { - // 创建一个包含纬度、经度和高度信息的 GeoJSON LineString + createPathWithArray (coordinatesArray, pathId = 'path') { const geojson = { type: 'Feature', properties: {}, @@ -762,21 +761,21 @@ export default { ]) } } - // 不是第一次 则改变路径图层里面得数据 - if (this.map.getLayer('path')) { - this.map.getSource('path').setData(geojson) + + // 如果已有图层,更新数据 + if (this.map.getLayer(pathId)) { + this.map.getSource(pathId).setData(geojson) } else { - // 第一次在地图里添加路径图层 - // 如果坐标数组不为空,创建新路径 if (coordinatesArray.length > 0) { - // 添加3D图层 + // 添加新的 source 和 layer + this.map.addSource(pathId, { + type: 'geojson', + data: geojson + }) this.map.addLayer({ - id: 'path', + id: pathId, type: 'line', - source: { - type: 'geojson', - data: geojson - }, + source: pathId, layout: { 'line-cap': 'round', 'line-join': 'round' @@ -976,8 +975,8 @@ export default { if (plane != null) { plane.setLngLat([lonLat.lon, lonLat.lat]) } - // 创建轨迹 - this.createPathWithArray(pathArr) // 创建轨迹 + // 创建轨迹,路径 ID 每架飞机独有 + this.createPathWithArray(pathArr, `path-${index}`) // 镜头跟随飞机 if (this.isflow) { this.map.flyTo({ @@ -1090,65 +1089,112 @@ export default { }, 200) } }, - drawTestPoints (positions, center) { - // 清除旧的图层和数据源(可选) - if (this.map.getSource('test-points')) this.map.removeSource('test-points') - if (this.map.getLayer('test-points')) this.map.removeLayer('test-points') - if (this.map.getSource('center-point')) this.map.removeSource('center-point') - if (this.map.getLayer('center-point')) this.map.removeLayer('center-point') + /** + * @description: 点飞 - 绘制引导线与中心点 ps: 该方法用于在地图上绘制从中心点到点击点的引导线,以及每架飞机到终点的拓扑偏移线。 + * @param {Object} centerPoint - 中心点经纬度对象 {lng: number, lat: number} + * @param {Object} endPoint - 点击点经纬度对象 {lng: number, lat: number} + * @param {Array} planeCoords - 飞机坐标数组 [[lng, lat], ...] + */ + drawGuidedLines ({ centerPoint, endPoint, planeCoords }) { + const sourceId = 'guided-lines-source' + const lineLayerId = 'guided-lines-layer' + const pointLayerId = 'guided-center-point-layer' - // 构造蓝色飞机点的 GeoJSON - const features = positions.map(pos => ({ + // 清除旧图层和数据源 + if (this.map.getLayer(lineLayerId)) this.map.removeLayer(lineLayerId) + if (this.map.getLayer(pointLayerId)) this.map.removeLayer(pointLayerId) + if (this.map.getSource(sourceId)) this.map.removeSource(sourceId) + + const deltaLng = endPoint.lng - centerPoint.lng + const deltaLat = endPoint.lat - centerPoint.lat + + const features = [] + + // 🔵 每架飞机 → 拓扑偏移终点 引导线 + planeCoords.forEach(([lng, lat]) => { + const targetLng = lng + deltaLng + const targetLat = lat + deltaLat + + features.push({ + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [[lng, lat], [targetLng, targetLat]] + }, + properties: { + color: 'blue', + type: 'line' + } + }) + }) + + // 🔴 中心点 → 点击点 引导线 + features.push({ + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [[centerPoint.lng, centerPoint.lat], [endPoint.lng, endPoint.lat]] + }, + properties: { + color: 'red', + type: 'line' + } + }) + + // 🔴 中心点标记 + features.push({ type: 'Feature', geometry: { type: 'Point', - coordinates: [pos[0], pos[1]] - } - })) - - this.map.addSource('test-points', { - type: 'geojson', - data: { - type: 'FeatureCollection', - features: features + coordinates: [centerPoint.lng, centerPoint.lat] + }, + properties: { + color: 'red', + type: 'center-point' } }) + // 添加 GeoJSON 源 + this.map.addSource(sourceId, { + type: 'geojson', + data: { + type: 'FeatureCollection', + features + } + }) + + // 添加线图层 this.map.addLayer({ - id: 'test-points', + id: lineLayerId, + type: 'line', + source: sourceId, + filter: ['==', ['get', 'type'], 'line'], + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': ['get', 'color'], + 'line-width': 2, + 'line-dasharray': [2, 2] + } + }) + + // 添加中心点图层(圆点) + this.map.addLayer({ + id: pointLayerId, type: 'circle', - source: 'test-points', + source: sourceId, + filter: ['==', ['get', 'type'], 'center-point'], paint: { 'circle-radius': 6, - 'circle-color': '#3399ff' // 蓝色 - } - }) - - // 添加重心点(红色) - this.map.addSource('center-point', { - type: 'geojson', - data: { - type: 'FeatureCollection', - features: [{ - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [center.lon, center.lat] - } - }] - } - }) - - this.map.addLayer({ - id: 'center-point', - type: 'circle', - source: 'center-point', - paint: { - 'circle-radius': 8, - 'circle-color': '#ff3333' // 红色 + 'circle-color': ['get', 'color'], + 'circle-stroke-color': '#fff', + 'circle-stroke-width': 2 } }) } + }, beforeDestroy () { if (this.map) { @@ -1184,8 +1230,15 @@ export default { /*飞机popup 心跳图标样式*/ @keyframes heartbeat { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.3); } + + 0%, + 100% { + transform: scale(1); + } + + 50% { + transform: scale(1.3); + } } .heart-icon { diff --git a/src/store/index.js b/src/store/index.js index cfac473..4a687a0 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -30,6 +30,20 @@ const store = new Vuex.Store({ ADSBList: [] // 存放当前活跃的 ADSB 飞机数据 }, mutations: { + updatePlanePosition (state, { planeId, position }) { + const plane = state.airList.find(p => p.id === planeId) + if (plane) { + if (!plane.planeState) { + Vue.set(plane, 'planeState', {}) + } + if (!plane.planeState.position) { + Vue.set(plane.planeState, 'position', []) + } + + // 追加新点 + plane.planeState.position.push(...position) + } + }, /** * @description: 设置商铺列表 */ diff --git a/src/utils/index.js b/src/utils/index.js index e5acddf..d22018a 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -159,6 +159,97 @@ export function formatPrice (value) { return formattedPrice } +/** + * @description: 把对象保存为 JSON 文件并触发下载 + * @param {*} data - 要保存的对象数据 + * @param {string} filename - 下载的文件名,默认为 data_时间 + */ +export function saveObjectToFile (data, filename = `data_${Date.now()}.json`) { + if (!data || typeof data !== 'object') { + alert('无效的数据对象') + return + } + + // 将对象格式化为 JSON 字符串 + const content = JSON.stringify(data, null, 2) + + // 创建 Blob 对象 + const blob = new Blob([content], { type: 'application/json' }) + + // 创建一个临时下载链接 + const link = document.createElement('a') + link.href = URL.createObjectURL(blob) + link.download = filename + + // 模拟点击以触发下载 + link.click() + + // 释放资源 + URL.revokeObjectURL(link.href) +} + +/* ----------------------- 几何 线代 等 相关 ----------------------- */ + +/** + * @abstract 传入一组坐标组,返回包围盒中心点(即最小矩形包围框的中心) + * @param {Array} positions - 包含经度、纬度和高度的数组,格式:[[经度, 纬度, 高度], ...] + * @return {Object|null} 返回包围盒中心点对象 { lon: 经度, lat: 纬度, alt: 高度 } + * 如果输入无效则返回 null + */ +export function getBoundingCenter (positions) { + if (!Array.isArray(positions) || positions.length === 0) return null + + // 初始化边界极值 + let minLon = Infinity; let maxLon = -Infinity + let minLat = Infinity; let maxLat = -Infinity + let minAlt = Infinity; let maxAlt = -Infinity + + // 遍历所有点,计算边界最大最小值 + positions.forEach(pos => { + const [lon, lat, alt = 0] = pos + if (lon < minLon) minLon = lon + if (lon > maxLon) maxLon = lon + if (lat < minLat) minLat = lat + if (lat > maxLat) maxLat = lat + if (alt < minAlt) minAlt = alt + if (alt > maxAlt) maxAlt = alt + }) + + // 计算包围盒中心点坐标,返回对象格式 + return { + lon: (minLon + maxLon) / 2, + lat: (minLat + maxLat) / 2, + alt: (minAlt + maxAlt) / 2 + } +} + +/** + * @abstract 传入一组坐标组,返回几何中心点(重心) + * @param {Array} positions - 包含经度、纬度和高度的数组,格式:[[经度, 纬度, 高度], ...] + * @return {Object|null} 返回几何中心点对象 { lon: 经度, lat: 纬度, alt: 高度 } + * 如果输入无效则返回 null + */ +export function getGeometricCenter (positions) { + if (!Array.isArray(positions) || positions.length === 0) return null + + // 累加所有点的经纬高坐标 + const sum = positions.reduce((acc, [lon, lat, alt = 0]) => { + acc.lon += lon + acc.lat += lat + acc.alt += alt + return acc + }, { lon: 0, lat: 0, alt: 0 }) + + const count = positions.length + + // 计算平均值作为几何中心,返回对象格式 + return { + lon: sum.lon / count, + lat: sum.lat / count, + alt: sum.alt / count + } +} + /** * @description:等待 Mapbox 地图的画布容器(Canvas Container)准备就绪。因为 Mapbox 初始化是异步的,某些操作需要等待画布容器加载完成才能执行。 * @param {Object} map - Mapbox GL JS 地图实例对象 @@ -190,32 +281,3 @@ export function waitForMapCanvasReady (map, maxRetry = 5) { check(maxRetry) }) } - -/** - * @description: 把对象保存为 JSON 文件并触发下载 - * @param {*} data - 要保存的对象数据 - * @param {string} filename - 下载的文件名,默认为 data_时间 - */ -export function saveObjectToFile (data, filename = `data_${Date.now()}.json`) { - if (!data || typeof data !== 'object') { - alert('无效的数据对象') - return - } - - // 将对象格式化为 JSON 字符串 - const content = JSON.stringify(data, null, 2) - - // 创建 Blob 对象 - const blob = new Blob([content], { type: 'application/json' }) - - // 创建一个临时下载链接 - const link = document.createElement('a') - link.href = URL.createObjectURL(blob) - link.download = filename - - // 模拟点击以触发下载 - link.click() - - // 释放资源 - URL.revokeObjectURL(link.href) -} diff --git a/src/views/layout/components/main/home/index.vue b/src/views/layout/components/main/home/index.vue index a3ddd24..5ef6c02 100644 --- a/src/views/layout/components/main/home/index.vue +++ b/src/views/layout/components/main/home/index.vue @@ -3,7 +3,7 @@ @@ -27,12 +27,21 @@ export default { Statistics }, computed: { - airList () { + planeList () { return this.$store.state.airList }, // 过滤出所有飞机状态列表s planeStatus () { - return this.airList.map(plane => plane.planeState) + return this.planeList.map(plane => plane.planeState) + }, + /** + * @description: 所有飞机的历史定位数组集合,每个元素是一架飞机的 position 数组 + */ + planePositions () { + return this.planeList.map(plane => { + const posArr = plane.planeState?.position + return Array.isArray(posArr) ? posArr : [] + }) }, /** * @description: 侧边栏显隐 @@ -45,7 +54,7 @@ export default { // 地图组件回调地图加载完成后 执行 onMapReady () { this.mapReady = true // 标记地图加载完成 - this.makePlanes(this.airList) + this.makePlanes(this.planeList) }, /** * @description: 创建飞机图标 @@ -68,43 +77,12 @@ export default { } }, mounted () { - setTimeout(() => { - // 中心点(经纬度) - const centerLng = 100.0000 - const centerLat = 40.0000 - - // 生成随机点数组(10个点) - const testPositions = Array.from({ length: 10 }, () => { - const offsetLng = centerLng + (Math.random() - 0.5) * 0.001 // ±0.0005 - const offsetLat = centerLat + (Math.random() - 0.5) * 0.001 - const alt = 100 + Math.floor(Math.random() * 100) // 高度 100~200 - return [offsetLng, offsetLat, alt] - }) - - // 计算重心点 - const center = testPositions.reduce((acc, pos) => { - acc.lon += pos[0] - acc.lat += pos[1] - acc.alt += pos[2] - return acc - }, { lon: 0, lat: 0, alt: 0 }) - - const count = testPositions.length - const centerPoint = { - lon: center.lon / count, - lat: center.lat / count, - alt: center.alt / count - } - - // 调用 mapbox 地图组件的绘图函数 - this.$refs.mapbox.drawTestPoints(testPositions, centerPoint) - }, 3000) }, watch: { /** * @description: 飞机列表更新时候 更新地图 */ - airList: { + planeList: { async handler () { try { // 等待地图画布准备好 @@ -121,12 +99,36 @@ export default { planeStatus: { handler (val) { val.forEach((stateObj, index) => { - stateObj.name = this.airList[index].name // 保留飞机名称 + stateObj.name = this.planeList[index].name // 保留飞机名称 this.$refs.mapbox.updatePopupContent(stateObj, index) }) }, deep: true }, + // 实时更新所有飞机位置 和轨迹 + planePositions: { + handler (allPositions) { + // allPositions 是一个数组,每个元素是该飞机的 position 数组 + allPositions.forEach((positions, idx) => { + const n = positions.length + if (n > 2) { + const [lon, lat] = positions[n - 1] + // 定期存储 (例如每100次) 当前飞机的位置 + const key = `swarm_plane_${this.planeList[idx].name}` + // 初始化本地计数器 + if (!this._storeCount) this._storeCount = {} + const cnt = (this._storeCount[key] = (this._storeCount[key] || 0) + 1) + if (cnt % 100 === 1) { + localStorage.setItem(key, JSON.stringify({ lon, lat })) + } + + // 更新 MapBox 中第 idx 架飞机的位置与轨迹 + this.$refs.mapbox.setPlaneLonLat({ lon, lat }, idx, positions) + } + }) + }, + deep: true + }, /** * @description: 侧边栏显隐 */ diff --git a/src/views/layout/components/main/planes/index.vue b/src/views/layout/components/main/planes/index.vue index a6e12d0..d94a392 100644 --- a/src/views/layout/components/main/planes/index.vue +++ b/src/views/layout/components/main/planes/index.vue @@ -116,6 +116,7 @@ export default { closeCallback () { if (this.dialogItem === 'guidedBox' && this.isReserveGuidedMaker === false) { // 关闭点飞窗口时 this.$refs.mapbox.delGuidedMarker()// 删除所有点飞的地图标记 + this.$refs.mapbox.clearMapElements(['guided-lines-layer', 'guided-center-point-layer'], ['guided-lines-source'])// 删除引导线 } this.dialogVisible = false this.dialogItem = '' @@ -131,15 +132,27 @@ export default { this.dialogItem = 'guidedBox' this.guidedLonLat = lonLat // 设置点击的经纬度 - // 安全获取飞机当前高度 + // 安全获取当前飞机位置和高度 + let center = { lon: 0, lat: 0 } let height = 0 - if (this.plane && this.plane.planeState && Array.isArray(this.plane.planeState.position)) { - const posLen = this.plane.planeState.position.length - if (posLen > 0 && Array.isArray(this.plane.planeState.position[posLen - 1])) { - height = this.plane.planeState.position[posLen - 1][2] || 0 + if (this.plane?.planeState?.position?.length > 0) { + const lastPos = this.plane.planeState.position.at(-1) + if (Array.isArray(lastPos)) { + center = { lon: lastPos[0], lat: lastPos[1] } + height = lastPos[2] || 0 } } this.guidedAlt = height + + // ✅ 构造单架飞机的位置数组 + const planeCoords = [[center.lon, center.lat]] + + // ✅ 复用现有方法绘制引导线 + this.$refs.mapbox.drawGuidedLines({ + centerPoint: { lng: center.lon, lat: center.lat }, + endPoint: { lng: lonLat.lon, lat: lonLat.lat }, + planeCoords + }) }, // 单架飞机点飞指令,参数:lon, lat, alt flyToSinglePlane (lon, lat, alt) { diff --git a/src/views/layout/components/main/planes/swarm.vue b/src/views/layout/components/main/planes/swarm.vue index 3e05a7a..6c8c4d6 100644 --- a/src/views/layout/components/main/planes/swarm.vue +++ b/src/views/layout/components/main/planes/swarm.vue @@ -1,11 +1,11 @@