2025-06-22 15:19:51 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="h-100">
|
|
|
|
|
|
<!-- 地图组件 -->
|
2025-06-24 16:12:28 +08:00
|
|
|
|
<map-box ref="mapbox" v-if="swarmReady" :enableShowNofly="true" :enableGuided="true"
|
|
|
|
|
|
:enblueScale="!$store.state.app.isWideScreen" @longPress="handleLongPress" @map-ready="onMapReady">
|
2025-06-22 15:19:51 +08:00
|
|
|
|
<template #content>
|
|
|
|
|
|
<div v-show="mapReady">
|
2025-06-24 16:12:28 +08:00
|
|
|
|
<SwarmControllerTabs :planes="planeList" />
|
2025-06-22 15:19:51 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</map-box>
|
|
|
|
|
|
<!-- 弹出框 -->
|
|
|
|
|
|
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="320px" top="30vh" @close="closeCallback"
|
|
|
|
|
|
@open="openCallback">
|
|
|
|
|
|
<!-- 点飞设置弹出框 -->
|
|
|
|
|
|
<template v-if="dialogItem == 'guidedBox'">
|
|
|
|
|
|
<el-form label-position="left">
|
2025-06-22 20:39:40 +08:00
|
|
|
|
<el-form-item label="中心纬度" label-width="80px">
|
2025-06-22 15:19:51 +08:00
|
|
|
|
<el-input v-model="guidedLonLat.lat" placeholder="请输维度" label="纬度"></el-input>
|
|
|
|
|
|
</el-form-item>
|
2025-06-22 20:39:40 +08:00
|
|
|
|
<el-form-item label="中心经度" label-width="80px">
|
2025-06-22 15:19:51 +08:00
|
|
|
|
<el-input v-model="guidedLonLat.lon" placeholder="请输经度" label="经度"></el-input>
|
|
|
|
|
|
</el-form-item>
|
2025-06-22 20:39:40 +08:00
|
|
|
|
<el-form-item label="中心高度" label-width="80px">
|
2025-06-22 15:19:51 +08:00
|
|
|
|
<el-input-number v-model="guidedAlt" label="高度设置"></el-input-number>
|
|
|
|
|
|
<font class="m-l-5">米</font>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<span slot="footer" class="dialog-footer">
|
|
|
|
|
|
<el-button size="medium" @click="dialogVisible = false">关闭</el-button>
|
2025-06-22 20:39:40 +08:00
|
|
|
|
<el-button size="medium" type="primary" @click="onSwarmFlyTo">飞至</el-button>
|
2025-06-22 15:19:51 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import MapBox from '@/components/MapBox'
|
2025-06-22 20:39:40 +08:00
|
|
|
|
import SwarmControllerTabs from '@/components/SwarmControllerTabs.vue'
|
2025-06-24 16:12:28 +08:00
|
|
|
|
import { waitForMapCanvasReady, getBoundingCenter } from '@/utils'
|
|
|
|
|
|
import mqtt from '@/utils/mqtt'
|
2025-06-22 15:19:51 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
2025-06-22 20:39:40 +08:00
|
|
|
|
name: 'Swarm',
|
2025-06-22 15:19:51 +08:00
|
|
|
|
data () {
|
|
|
|
|
|
return {
|
|
|
|
|
|
dialogTitle: '', // 弹出框 标题
|
|
|
|
|
|
dialogItem: '', // 弹出框 项目类型判断
|
|
|
|
|
|
dialogVisible: false, // 弹出框 显隐
|
|
|
|
|
|
guidedLonLat: {}, // 点飞 的经纬度
|
|
|
|
|
|
guidedAlt: '', // 点飞的高度
|
|
|
|
|
|
isReserveGuidedMaker: false, // 关闭指点飞行操作窗口时 标记是否删除图标
|
2025-06-23 21:13:50 +08:00
|
|
|
|
mapReady: false, // 地图加载完成后 回调时 设置此值 让地图组件插槽内容 滞后显示
|
|
|
|
|
|
swarmReady: false// 判断有没有选中两架以上集群 用于控制地图组件渲染
|
2025-06-22 15:19:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
components: {
|
|
|
|
|
|
MapBox,
|
2025-06-22 20:39:40 +08:00
|
|
|
|
SwarmControllerTabs
|
2025-06-22 15:19:51 +08:00
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
2025-06-22 20:39:40 +08:00
|
|
|
|
// 选中的集群控制飞机s
|
|
|
|
|
|
planeList () {
|
|
|
|
|
|
const allPlanes = this.$store.state.airList
|
|
|
|
|
|
const idArr = this.$store.state.app.swarmIdArr
|
|
|
|
|
|
return allPlanes.filter(plane => idArr.includes(plane.id))
|
2025-06-22 15:19:51 +08:00
|
|
|
|
},
|
2025-06-23 21:13:50 +08:00
|
|
|
|
// 过滤出所有飞机状态列表s
|
|
|
|
|
|
planeStatus () {
|
|
|
|
|
|
return this.planeList.map(plane => plane.planeState)
|
|
|
|
|
|
},
|
2025-06-24 16:12:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @description: 所有飞机的历史定位数组集合,每个元素是一架飞机的 position 数组
|
|
|
|
|
|
*/
|
|
|
|
|
|
planePositions () {
|
|
|
|
|
|
return this.planeList.map(plane => {
|
|
|
|
|
|
const posArr = plane.planeState?.position
|
|
|
|
|
|
return Array.isArray(posArr) ? posArr : []
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
2025-06-22 15:19:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @description: 侧边栏显隐
|
|
|
|
|
|
*/
|
|
|
|
|
|
isCollapse () {
|
|
|
|
|
|
return this.$store.state.app.isCollapse
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-06-22 20:39:40 +08:00
|
|
|
|
created () {
|
2025-06-23 21:13:50 +08:00
|
|
|
|
// 集群控制至少需要2架飞机
|
2025-06-22 20:39:40 +08:00
|
|
|
|
if (this.$store.state.app.swarmIdArr.length < 2) {
|
2025-06-23 21:13:50 +08:00
|
|
|
|
this.$router.replace('/register/index')// 如果集群控制飞机数量小于2架 则跳转列表页
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.swarmReady = true // 只有当数据条件满足才渲染 <map-box>
|
2025-06-22 20:39:40 +08:00
|
|
|
|
}
|
2025-06-24 16:12:28 +08:00
|
|
|
|
|
|
|
|
|
|
// // 测试单元
|
|
|
|
|
|
// 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)
|
2025-06-22 20:39:40 +08:00
|
|
|
|
},
|
2025-06-22 15:19:51 +08:00
|
|
|
|
methods: {
|
|
|
|
|
|
/** 弹出框 关闭事件回调 */
|
|
|
|
|
|
closeCallback () {
|
|
|
|
|
|
if (this.dialogItem === 'guidedBox' && this.isReserveGuidedMaker === false) { // 关闭点飞窗口时
|
|
|
|
|
|
this.$refs.mapbox.delGuidedMarker()// 删除所有点飞的地图标记
|
2025-06-24 16:12:28 +08:00
|
|
|
|
this.$refs.mapbox.clearMapElements(['guided-lines-layer', 'guided-center-point-layer'], ['guided-lines-source'])// 删除引导线
|
2025-06-22 15:19:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
this.dialogVisible = false
|
|
|
|
|
|
this.dialogItem = ''
|
|
|
|
|
|
},
|
|
|
|
|
|
/** 弹出框 打开事件回调 */
|
|
|
|
|
|
openCallback () {
|
|
|
|
|
|
},
|
2025-06-24 16:12:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @abstract 地图长按事件处理
|
|
|
|
|
|
* @param {Object} lonLat - 点击地图终点经纬度,如 { lng: number, lat: number }
|
|
|
|
|
|
*/
|
2025-06-22 15:19:51 +08:00
|
|
|
|
handleLongPress (lonLat) {
|
|
|
|
|
|
this.isReserveGuidedMaker = false
|
2025-06-22 20:39:40 +08:00
|
|
|
|
this.dialogTitle = '集群指点'
|
2025-06-22 15:19:51 +08:00
|
|
|
|
this.dialogVisible = true
|
|
|
|
|
|
this.dialogItem = 'guidedBox'
|
2025-06-24 16:12:28 +08:00
|
|
|
|
this.guidedLonLat = lonLat
|
2025-06-22 20:39:40 +08:00
|
|
|
|
|
2025-06-24 16:12:28 +08:00
|
|
|
|
// ① 获取所有飞机位置 [[lng, lat, alt], ...]
|
|
|
|
|
|
const allPlaneCoords = this.planeList.map(p => {
|
2025-06-22 20:39:40 +08:00
|
|
|
|
const pos = p?.planeState?.position
|
|
|
|
|
|
const last = Array.isArray(pos) && pos.length > 0 ? pos[pos.length - 1] : null
|
2025-06-24 16:12:28 +08:00
|
|
|
|
return Array.isArray(last) ? [last[0], last[1], last[2] || 0] : null
|
|
|
|
|
|
}).filter(Boolean)
|
|
|
|
|
|
|
|
|
|
|
|
// ② 计算平均高度并更新引导高度
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
2025-06-22 20:39:40 +08:00
|
|
|
|
|
2025-06-24 16:12:28 +08:00
|
|
|
|
// ③ 获取飞机重心点(使用工具函数 getBoundingCenter)
|
|
|
|
|
|
const center = getBoundingCenter(allPlaneCoords) // 返回 { lng, lat }
|
2025-06-22 20:39:40 +08:00
|
|
|
|
|
2025-06-24 16:12:28 +08:00
|
|
|
|
// ④ 绘制引导线与中心点
|
|
|
|
|
|
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
|
|
|
|
|
|
})
|
2025-06-22 20:39:40 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 集群指点飞行 保持所有飞机拓扑关系飞到指定位置
|
|
|
|
|
|
onSwarmFlyTo () {
|
|
|
|
|
|
const targetLon = Number(this.guidedLonLat.lon)
|
|
|
|
|
|
const targetLat = Number(this.guidedLonLat.lat)
|
|
|
|
|
|
const targetAlt = Number(this.guidedAlt)
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
isNaN(targetLon) || isNaN(targetLat) || isNaN(targetAlt) ||
|
2025-06-24 16:12:28 +08:00
|
|
|
|
targetLon < -180 || targetLon > 180 ||
|
|
|
|
|
|
targetLat < -90 || targetLat > 90
|
2025-06-22 20:39:40 +08:00
|
|
|
|
) {
|
|
|
|
|
|
this.$message.warning('请输入有效的经纬度(经度-180~180,纬度-90~90)和高度')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-24 16:12:28 +08:00
|
|
|
|
// 取所有飞机的最新定位点,组成坐标数组,传入 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)
|
2025-06-22 20:39:40 +08:00
|
|
|
|
|
|
|
|
|
|
if (!currentCenter) {
|
|
|
|
|
|
this.$message.error('无法获取飞行器中心位置,检查飞机是否都已定位')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算每架飞机相对于当前中心的偏移量
|
|
|
|
|
|
const commands = this.planeList.map(p => {
|
|
|
|
|
|
const pos = p?.planeState?.position
|
|
|
|
|
|
const last = Array.isArray(pos) && pos.length > 0 ? pos[pos.length - 1] : null
|
|
|
|
|
|
if (!Array.isArray(last)) return null
|
|
|
|
|
|
|
|
|
|
|
|
const offsetLon = last[0] - currentCenter.lon
|
|
|
|
|
|
const offsetLat = last[1] - currentCenter.lat
|
|
|
|
|
|
const offsetAlt = last[2] - currentCenter.alt
|
|
|
|
|
|
|
|
|
|
|
|
const newLon = targetLon + offsetLon
|
|
|
|
|
|
const newLat = targetLat + offsetLat
|
|
|
|
|
|
const newAlt = targetAlt + offsetAlt
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
2025-06-24 16:12:28 +08:00
|
|
|
|
macadd: p.macadd,
|
2025-06-22 20:39:40 +08:00
|
|
|
|
cmd: `{guidedMode:{lon:${newLon.toFixed(7)},lat:${newLat.toFixed(7)},alt:${newAlt.toFixed(1)}}}`
|
2025-06-22 15:19:51 +08:00
|
|
|
|
}
|
2025-06-22 20:39:40 +08:00
|
|
|
|
}).filter(Boolean)
|
|
|
|
|
|
|
|
|
|
|
|
// 发送控制指令
|
2025-06-24 16:12:28 +08:00
|
|
|
|
commands.forEach(({ macadd, cmd }) => {
|
|
|
|
|
|
mqtt.publishFun(`cmd/${macadd}`, cmd)
|
2025-06-22 20:39:40 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.isReserveGuidedMaker = true
|
|
|
|
|
|
this.dialogVisible = false
|
|
|
|
|
|
},
|
2025-06-22 15:19:51 +08:00
|
|
|
|
// 地图组件回调地图加载完成后 执行
|
|
|
|
|
|
onMapReady () {
|
2025-06-22 20:39:40 +08:00
|
|
|
|
this.mapReady = true // 标记地图加载完成
|
|
|
|
|
|
this.makePlanes(this.planeList)
|
2025-06-22 15:19:51 +08:00
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @description: 创建飞机图标
|
|
|
|
|
|
*/
|
2025-06-22 20:39:40 +08:00
|
|
|
|
makePlanes (planes) {
|
2025-06-22 15:19:51 +08:00
|
|
|
|
this.$refs.mapbox.removePlanes()// 先清除画布上现有的飞机
|
2025-06-22 20:39:40 +08:00
|
|
|
|
planes.forEach((plane, index) => { // 创建所有飞机
|
|
|
|
|
|
let planeDefaultLonLat
|
|
|
|
|
|
if (localStorage.getItem(plane.name) !== null) { // 从本地缓存 拿飞机得初始位置
|
|
|
|
|
|
planeDefaultLonLat = JSON.parse(localStorage.getItem(plane.name))
|
|
|
|
|
|
plane.lon = planeDefaultLonLat.lon
|
|
|
|
|
|
plane.lat = planeDefaultLonLat.lat
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 缺省 给经纬度 小数点后随机第五位 即有个10米左右的随机距离
|
|
|
|
|
|
plane.lon = 100 + Number((Math.random() * 0.01).toFixed(5))
|
|
|
|
|
|
plane.lat = 40 + Number((Math.random() * 0.01).toFixed(5))
|
|
|
|
|
|
}
|
|
|
|
|
|
this.$refs.mapbox.makePlane(plane, index)
|
|
|
|
|
|
})
|
2025-06-22 15:19:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
watch: {
|
2025-06-22 20:39:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @description: 飞机列表更新时候 更新地图
|
|
|
|
|
|
*/
|
|
|
|
|
|
planeList: {
|
2025-06-22 15:19:51 +08:00
|
|
|
|
async handler () {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 等待地图画布准备好
|
2025-06-22 20:39:40 +08:00
|
|
|
|
await waitForMapCanvasReady(this.$refs.mapbox.map)
|
2025-06-22 15:19:51 +08:00
|
|
|
|
// 画布准备好后执行你自己的逻辑
|
|
|
|
|
|
this.onMapReady()
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.debug('等待地图画布准备超时', err)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
immediate: true
|
|
|
|
|
|
},
|
2025-06-23 21:13:50 +08:00
|
|
|
|
// 实时更新所有状态 到对应飞机弹出框
|
|
|
|
|
|
planeStatus: {
|
|
|
|
|
|
handler (val) {
|
|
|
|
|
|
val.forEach((stateObj, index) => {
|
|
|
|
|
|
stateObj.name = this.planeList[index].name // 保留飞机名称
|
|
|
|
|
|
this.$refs.mapbox.updatePopupContent(stateObj, index)
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
deep: true
|
|
|
|
|
|
},
|
2025-06-24 16:12:28 +08:00
|
|
|
|
// 实时更新所有飞机位置 和轨迹
|
|
|
|
|
|
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
|
|
|
|
|
|
},
|
2025-06-22 15:19:51 +08:00
|
|
|
|
/**
|
2025-06-28 15:17:22 +08:00
|
|
|
|
* @description: 侧边栏缩进有变化时 地图重新自适应
|
2025-06-22 15:19:51 +08:00
|
|
|
|
*/
|
2025-06-28 15:17:22 +08:00
|
|
|
|
isCollapse: {
|
|
|
|
|
|
handler (val) {
|
|
|
|
|
|
if (val) {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
this.$refs.mapbox.handleResize()
|
|
|
|
|
|
})
|
|
|
|
|
|
}, 500)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
deep: true
|
2025-06-22 15:19:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped></style>
|