food/src/views/layout/components/main/planes/swarm.vue

329 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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">
<template #content>
<div v-show="mapReady">
<SwarmControllerTabs :planes="planeList" />
</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">
<el-form-item label="中心纬度" label-width="80px">
<el-input v-model="guidedLonLat.lat" placeholder="请输维度" label="纬度"></el-input>
</el-form-item>
<el-form-item label="中心经度" label-width="80px">
<el-input v-model="guidedLonLat.lon" placeholder="请输经度" label="经度"></el-input>
</el-form-item>
<el-form-item label="中心高度" label-width="80px">
<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>
<el-button size="medium" type="primary" @click="onSwarmFlyTo">飞至</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import MapBox from '@/components/MapBox'
import SwarmControllerTabs from '@/components/SwarmControllerTabs.vue'
import { waitForMapCanvasReady, getBoundingCenter } from '@/utils'
import mqtt from '@/utils/mqtt'
export default {
name: 'Swarm',
data () {
return {
dialogTitle: '', // 弹出框 标题
dialogItem: '', // 弹出框 项目类型判断
dialogVisible: false, // 弹出框 显隐
guidedLonLat: {}, // 点飞 的经纬度
guidedAlt: '', // 点飞的高度
isReserveGuidedMaker: false, // 关闭指点飞行操作窗口时 标记是否删除图标
mapReady: false, // 地图加载完成后 回调时 设置此值 让地图组件插槽内容 滞后显示
swarmReady: false// 判断有没有选中两架以上集群 用于控制地图组件渲染
}
},
components: {
MapBox,
SwarmControllerTabs
},
computed: {
// 选中的集群控制飞机s
planeList () {
const allPlanes = this.$store.state.airList
const idArr = this.$store.state.app.swarmIdArr
return allPlanes.filter(plane => idArr.includes(plane.id))
},
// 过滤出所有飞机状态列表s
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: 侧边栏显隐
*/
isCollapse () {
return this.$store.state.app.isCollapse
}
},
created () {
// 集群控制至少需要2架飞机
if (this.$store.state.app.swarmIdArr.length < 2) {
this.$router.replace('/register/index')// 如果集群控制飞机数量小于2架 则跳转列表页
} 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))
// // ✅ 随机高度4060 米之间
// 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 = ''
},
/** 弹出框 打开事件回调 */
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
// ① 获取所有飞机位置 [[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[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
}
// ③ 获取飞机重心点(使用工具函数 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 () {
const targetLon = Number(this.guidedLonLat.lon)
const targetLat = Number(this.guidedLonLat.lat)
const targetAlt = Number(this.guidedAlt)
if (
isNaN(targetLon) || isNaN(targetLat) || isNaN(targetAlt) ||
targetLon < -180 || targetLon > 180 ||
targetLat < -90 || targetLat > 90
) {
this.$message.warning('请输入有效的经纬度(经度-180~180纬度-90~90和高度')
return
}
// 取所有飞机的最新定位点,组成坐标数组,传入 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('无法获取飞行器中心位置,检查飞机是否都已定位')
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 {
macadd: p.macadd,
cmd: `{guidedMode:{lon:${newLon.toFixed(7)},lat:${newLat.toFixed(7)},alt:${newAlt.toFixed(1)}}}`
}
}).filter(Boolean)
// 发送控制指令
commands.forEach(({ macadd, cmd }) => {
mqtt.publishFun(`cmd/${macadd}`, cmd)
})
this.isReserveGuidedMaker = true
this.dialogVisible = false
},
// 地图组件回调地图加载完成后 执行
onMapReady () {
this.mapReady = true // 标记地图加载完成
this.makePlanes(this.planeList)
},
/**
* @description: 创建飞机图标
*/
makePlanes (planes) {
this.$refs.mapbox.removePlanes()// 先清除画布上现有的飞机
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)
})
}
},
watch: {
/**
* @description: 飞机列表更新时候 更新地图
*/
planeList: {
async handler () {
try {
// 等待地图画布准备好
await waitForMapCanvasReady(this.$refs.mapbox.map)
// 画布准备好后执行你自己的逻辑
this.onMapReady()
} catch (err) {
console.debug('等待地图画布准备超时', err)
}
},
immediate: true
},
// 实时更新所有状态 到对应飞机弹出框
planeStatus: {
handler (val) {
val.forEach((stateObj, index) => {
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: 侧边栏缩进有变化时 地图重新自适应
*/
isCollapse: {
handler (val) {
if (val) {
setTimeout(() => {
this.$nextTick(() => {
this.$refs.mapbox.handleResize()
})
}, 500)
}
},
deep: true
}
}
}
</script>
<style lang="scss" scoped></style>