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 @@
-
+
-
+
@@ -38,7 +38,8 @@