【类 型】:feat
【原 因】:1集群 点飞功能完善 发命令 规划引导线等 2单体点飞增加 规划引导线功能 3集群飞机轨迹显示bug 【过 程】: 【影 响】: # 类型 包含: # feat:新功能(feature) # fix:修补bug # docs:文档(documentation) # style: 格式(不影响代码运行的变动) # refactor:重构(即不是新增功能,也不是修改bug的代码变动) # test:增加测试 # chore:构建过程或辅助工具的变动
This commit is contained in:
parent
bcffe48c50
commit
b42ea4328d
@ -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 {
|
||||
|
@ -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: 设置商铺列表
|
||||
*/
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
<map-box ref="mapbox" @map-ready="onMapReady">
|
||||
<template #content>
|
||||
<div v-show="mapReady">
|
||||
<Statistics :planes="airList" />
|
||||
<Statistics :planes="planeList" />
|
||||
</div>
|
||||
</template>
|
||||
</map-box>
|
||||
@ -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: 侧边栏显隐
|
||||
*/
|
||||
|
@ -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) {
|
||||
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="h-100">
|
||||
<!-- 地图组件 -->
|
||||
<map-box ref="mapbox" v-if="swarmReady" :enableShowNofly="true" :enableGuided="true" :enblueScale="!$store.state.app.isWideScreen"
|
||||
@longPress="handleLongPress" @map-ready="onMapReady">
|
||||
<map-box ref="mapbox" v-if="swarmReady" :enableShowNofly="true" :enableGuided="true"
|
||||
:enblueScale="!$store.state.app.isWideScreen" @longPress="handleLongPress" @map-ready="onMapReady">
|
||||
<template #content>
|
||||
<div v-show="mapReady">
|
||||
<SwarmControllerTabs :planes="planeList"/>
|
||||
<SwarmControllerTabs :planes="planeList" />
|
||||
</div>
|
||||
</template>
|
||||
</map-box>
|
||||
@ -38,7 +38,8 @@
|
||||
<script>
|
||||
import MapBox from '@/components/MapBox'
|
||||
import SwarmControllerTabs from '@/components/SwarmControllerTabs.vue'
|
||||
import { waitForMapCanvasReady } from '@/utils'
|
||||
import { waitForMapCanvasReady, getBoundingCenter } from '@/utils'
|
||||
import mqtt from '@/utils/mqtt'
|
||||
|
||||
export default {
|
||||
name: 'Swarm',
|
||||
@ -69,6 +70,15 @@ export default {
|
||||
planeStatus () {
|
||||
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: 侧边栏显隐
|
||||
*/
|
||||
@ -83,12 +93,41 @@ export default {
|
||||
} else {
|
||||
this.swarmReady = true // 只有当数据条件满足才渲染 <map-box>
|
||||
}
|
||||
|
||||
// // 测试单元
|
||||
// setInterval(() => {
|
||||
// const centerLng = 116.397 // 中心经度(北京)
|
||||
// const centerLat = 39.908 // 中心纬度
|
||||
// const maxOffset = 0.0002 // 经纬度最大偏移 ≈ ±200 米
|
||||
|
||||
// this.$store.state.airList.forEach((plane) => {
|
||||
// if (!plane.planeState) {
|
||||
// plane.planeState = {}
|
||||
// }
|
||||
|
||||
// // 经纬度偏移
|
||||
// const offsetLng = (Math.random() - 0.5) * 2 * maxOffset
|
||||
// const offsetLat = (Math.random() - 0.5) * 2 * maxOffset
|
||||
// const randomLng = Number((centerLng + offsetLng).toFixed(7))
|
||||
// const randomLat = Number((centerLat + offsetLat).toFixed(7))
|
||||
|
||||
// // ✅ 随机高度:40~60 米之间
|
||||
// const randomAlt = Number((40 + Math.random() * 20).toFixed(2))
|
||||
|
||||
// // 设置随机位置
|
||||
// this.$store.commit('updatePlanePosition', {
|
||||
// planeId: plane.id,
|
||||
// position: [[randomLng, randomLat, randomAlt]]
|
||||
// })
|
||||
// })
|
||||
// }, 15000)
|
||||
},
|
||||
methods: {
|
||||
/** 弹出框 关闭事件回调 */
|
||||
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 = ''
|
||||
@ -96,26 +135,41 @@ export default {
|
||||
/** 弹出框 打开事件回调 */
|
||||
openCallback () {
|
||||
},
|
||||
// 地图长按事件 记录地图经纬度
|
||||
/**
|
||||
* @abstract 地图长按事件处理
|
||||
* @param {Object} lonLat - 点击地图终点经纬度,如 { lng: number, lat: number }
|
||||
*/
|
||||
handleLongPress (lonLat) {
|
||||
this.isReserveGuidedMaker = false
|
||||
this.dialogTitle = '集群指点'
|
||||
this.dialogVisible = true
|
||||
this.dialogItem = 'guidedBox'
|
||||
this.guidedLonLat = lonLat // 点击的新位置中心点
|
||||
this.guidedLonLat = lonLat
|
||||
|
||||
// 获取所有飞机的高度并求平均
|
||||
const validHeights = this.planeList.map(p => {
|
||||
// ① 获取所有飞机位置 [[lng, lat, alt], ...]
|
||||
const allPlaneCoords = this.planeList.map(p => {
|
||||
const pos = p?.planeState?.position
|
||||
const last = Array.isArray(pos) && pos.length > 0 ? pos[pos.length - 1] : null
|
||||
return Array.isArray(last) ? last[2] || 0 : 0
|
||||
}).filter(h => typeof h === 'number')
|
||||
return Array.isArray(last) ? [last[0], last[1], last[2] || 0] : null
|
||||
}).filter(Boolean)
|
||||
|
||||
const avgAlt = validHeights.length
|
||||
? (validHeights.reduce((sum, h) => sum + h, 0) / validHeights.length).toFixed(1)
|
||||
: 0
|
||||
// ② 计算平均高度并更新引导高度
|
||||
if (allPlaneCoords.length > 0) {
|
||||
const totalAlt = allPlaneCoords.reduce((sum, item) => sum + (item[2] || 0), 0)
|
||||
this.guidedAlt = Number((totalAlt / allPlaneCoords.length).toFixed(1))
|
||||
} else {
|
||||
this.guidedAlt = 0
|
||||
}
|
||||
|
||||
this.guidedAlt = Number(avgAlt)
|
||||
// ③ 获取飞机重心点(使用工具函数 getBoundingCenter)
|
||||
const center = getBoundingCenter(allPlaneCoords) // 返回 { lng, lat }
|
||||
|
||||
// ④ 绘制引导线与中心点
|
||||
this.$refs.mapbox.drawGuidedLines({
|
||||
centerPoint: { lng: center.lon, lat: center.lat },
|
||||
endPoint: { lng: lonLat.lon, lat: lonLat.lat },
|
||||
planeCoords: allPlaneCoords.map(([lng, lat]) => [lng, lat]) // 可选:去掉alt
|
||||
})
|
||||
},
|
||||
// 集群指点飞行 保持所有飞机拓扑关系飞到指定位置
|
||||
onSwarmFlyTo () {
|
||||
@ -125,15 +179,23 @@ export default {
|
||||
|
||||
if (
|
||||
isNaN(targetLon) || isNaN(targetLat) || isNaN(targetAlt) ||
|
||||
targetLon < -180 || targetLon > 180 ||
|
||||
targetLat < -90 || targetLat > 90
|
||||
targetLon < -180 || targetLon > 180 ||
|
||||
targetLat < -90 || targetLat > 90
|
||||
) {
|
||||
this.$message.warning('请输入有效的经纬度(经度-180~180,纬度-90~90)和高度')
|
||||
return
|
||||
}
|
||||
|
||||
// 所有飞机当前中心点
|
||||
const currentCenter = this.getSwarmCenter()
|
||||
// 取所有飞机的最新定位点,组成坐标数组,传入 getBoundingCenter,求中心点
|
||||
const positions = this.planeList.map(p => {
|
||||
const pos = p?.planeState?.position
|
||||
// 取最后一个点(最新点),如果没有位置就用 null 过滤掉
|
||||
if (!Array.isArray(pos) || pos.length === 0) return null
|
||||
return pos[pos.length - 1]
|
||||
}).filter(Boolean)
|
||||
|
||||
// 求所有飞机最新定位点的包围盒中心点
|
||||
const currentCenter = getBoundingCenter(positions)
|
||||
|
||||
if (!currentCenter) {
|
||||
this.$message.error('无法获取飞行器中心位置,检查飞机是否都已定位')
|
||||
@ -155,43 +217,19 @@ export default {
|
||||
const newAlt = targetAlt + offsetAlt
|
||||
|
||||
return {
|
||||
id: p.id,
|
||||
macadd: p.macadd,
|
||||
cmd: `{guidedMode:{lon:${newLon.toFixed(7)},lat:${newLat.toFixed(7)},alt:${newAlt.toFixed(1)}}}`
|
||||
}
|
||||
}).filter(Boolean)
|
||||
|
||||
// 发送控制指令
|
||||
commands.forEach(({ id, cmd }) => {
|
||||
this.publishFun(cmd, id)
|
||||
commands.forEach(({ macadd, cmd }) => {
|
||||
mqtt.publishFun(`cmd/${macadd}`, cmd)
|
||||
})
|
||||
|
||||
this.isReserveGuidedMaker = true
|
||||
this.dialogVisible = false
|
||||
},
|
||||
// 获取集群重心点
|
||||
getSwarmCenter () {
|
||||
const positions = this.planeList.map(p => {
|
||||
const pos = p?.planeState?.position
|
||||
const last = Array.isArray(pos) && pos.length > 0 ? pos[pos.length - 1] : null
|
||||
return Array.isArray(last) ? last : null
|
||||
}).filter(Boolean)
|
||||
|
||||
if (!positions.length) return null
|
||||
|
||||
const sum = positions.reduce((acc, pos) => {
|
||||
acc.lon += pos[0]
|
||||
acc.lat += pos[1]
|
||||
acc.alt += pos[2] || 0
|
||||
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
|
||||
}
|
||||
},
|
||||
// 地图组件回调地图加载完成后 执行
|
||||
onMapReady () {
|
||||
this.mapReady = true // 标记地图加载完成
|
||||
@ -244,6 +282,30 @@ export default {
|
||||
},
|
||||
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: 侧边栏显隐
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user