吉安感知网项目-前端
shuishen
2026-01-23 5f53dd8801d2553fcb8af0b2cac96c2c990e9f16
feat:数据驾驶舱地图调整
1 files modified
580 ■■■■ changed files
applications/drone-command/src/components/map-container/device-map-container.vue 580 ●●●● patch | view | raw | blame | history
applications/drone-command/src/components/map-container/device-map-container.vue
@@ -1,4 +1,4 @@
<template>
<template>
    <div class="map-shell">
        <CommonCesiumMap ref="mapRef" class="command-cesium map-container" :dom-id="props.containerId" :active="true"
            :flat-mode="false" :terrain="true" :layer-mode="4" :contour="false" :boundary="false"
@@ -93,6 +93,7 @@
import { cockpitAggregationApi } from '@/api/dataCockpit'
import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
import droneIcon from '@/assets/images/dataCockpit/map/drone.png'
import aggregationIcon from '@/assets/images/dataCockpit/map/aggregation.png'
import commandPostIcon from '@/assets/images/dataCockpit/map/command-post.png'
import popupClose from '@/assets/images/dataCockpit/map/popup-close.png'
@@ -121,19 +122,29 @@
const mapRef = ref(null)
let viewer = null
const deviceEntityIds = new Set()
const deviceEntityMap = new Map()
let defenseZoneSource = null
let partitionSource = null
let cockpitPrimitiveLayer = null
const devicePickMap = new Map()
let deviceBillboardCollection = null
let deviceRingOutlinePrimitives = []
let deviceRingFillPrimitives = []
let defenseZonePrimitive = null
let defenseZoneOutlinePrimitive = null
let partitionPrimitive = null
let partitionOutlinePrimitive = null
let aggregationSource = null
let commandPostSource = null
let commandPostBillboardCollection = null
let droneTrackBillboardCollection = null
let droneTrackPolylineCollection = null
let droneTrackTickHandler = null
let droneTrackStartTime = null
let droneTrackRuntime = []
const detailVisible = ref(true)
const clusterVisible = ref(false)
const countyCenterMap = new Map()
const showLayerPanel = ref(false)
const layerWrapRef = ref(null)
const selectedDevice = ref(null)
let selectedDeviceEntity = null
let selectedDeviceBillboard = null
let deviceClickHandler = null
let popupRenderHandler = null
const layerTreeProps = {
@@ -180,23 +191,101 @@
    return { longitude, latitude }
}
const ensureCockpitPrimitiveLayer = () => {
    if (!viewer) return
    if (!cockpitPrimitiveLayer) {
        cockpitPrimitiveLayer = new Cesium.PrimitiveCollection({ destroyPrimitives: false })
        viewer.scene.primitives.add(cockpitPrimitiveLayer)
    }
}
const addCockpitPrimitive = primitive => {
    if (!primitive) return
    if (cockpitPrimitiveLayer) {
        cockpitPrimitiveLayer.add(primitive)
        return
    }
    viewer?.scene?.primitives?.add(primitive)
}
const removeCockpitPrimitive = (primitive, destroy = true) => {
    if (!primitive) return
    if (cockpitPrimitiveLayer) {
        cockpitPrimitiveLayer.remove(primitive, destroy)
        return
    }
    viewer?.scene?.primitives?.remove(primitive)
}
const reorderCockpitPrimitives = () => {
    if (!viewer) return
    ensureCockpitPrimitiveLayer()
    cockpitPrimitiveLayer.removeAll(false)
    if (defenseZonePrimitive) cockpitPrimitiveLayer.add(defenseZonePrimitive)
    if (defenseZoneOutlinePrimitive) cockpitPrimitiveLayer.add(defenseZoneOutlinePrimitive)
    if (partitionPrimitive) cockpitPrimitiveLayer.add(partitionPrimitive)
    if (partitionOutlinePrimitive) cockpitPrimitiveLayer.add(partitionOutlinePrimitive)
    if (deviceRingFillPrimitives.length) {
        deviceRingFillPrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
    }
    if (deviceRingOutlinePrimitives.length) {
        deviceRingOutlinePrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
    }
    if (droneTrackPolylineCollection) cockpitPrimitiveLayer.add(droneTrackPolylineCollection)
    if (droneTrackBillboardCollection) cockpitPrimitiveLayer.add(droneTrackBillboardCollection)
    if (deviceBillboardCollection) cockpitPrimitiveLayer.add(deviceBillboardCollection)
    if (commandPostBillboardCollection) cockpitPrimitiveLayer.add(commandPostBillboardCollection)
}
const clearDeviceEntities = () => {
    if (!viewer) return
    deviceEntityIds.forEach(id => {
        viewer.entities.removeById(id)
    })
    deviceEntityIds.clear()
    deviceEntityMap.clear()
    if (deviceBillboardCollection) {
        deviceBillboardCollection.removeAll()
        removeCockpitPrimitive(deviceBillboardCollection)
        deviceBillboardCollection = null
    }
    if (deviceRingFillPrimitives.length) {
        deviceRingFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
        deviceRingFillPrimitives = []
    }
    if (deviceRingOutlinePrimitives.length) {
        deviceRingOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
        deviceRingOutlinePrimitives = []
    }
    devicePickMap.clear()
}
const ensureDeviceCollections = () => {
    if (!viewer) return
    ensureCockpitPrimitiveLayer()
    if (!deviceBillboardCollection) {
        deviceBillboardCollection = new Cesium.BillboardCollection()
        addCockpitPrimitive(deviceBillboardCollection)
    }
}
const clearDefenseZoneEntities = () => {
    if (!defenseZoneSource) return
    defenseZoneSource.entities.removeAll()
    if (!viewer) return
    if (defenseZonePrimitive) {
        removeCockpitPrimitive(defenseZonePrimitive)
        defenseZonePrimitive = null
    }
    if (defenseZoneOutlinePrimitive) {
        removeCockpitPrimitive(defenseZoneOutlinePrimitive)
        defenseZoneOutlinePrimitive = null
    }
}
const clearPartitionEntities = () => {
    if (!partitionSource) return
    partitionSource.entities.removeAll()
    if (!viewer) return
    if (partitionPrimitive) {
        removeCockpitPrimitive(partitionPrimitive)
        partitionPrimitive = null
    }
    if (partitionOutlinePrimitive) {
        removeCockpitPrimitive(partitionOutlinePrimitive)
        partitionOutlinePrimitive = null
    }
}
const clearAggregationEntities = () => {
@@ -205,20 +294,42 @@
}
const clearCommandPostEntities = () => {
    if (!commandPostSource) return
    commandPostSource.entities.removeAll()
    if (!viewer) return
    if (commandPostBillboardCollection) {
        commandPostBillboardCollection.removeAll()
        removeCockpitPrimitive(commandPostBillboardCollection)
        commandPostBillboardCollection = null
    }
}
const ensureCommandPostCollection = () => {
    if (!viewer) return
    ensureCockpitPrimitiveLayer()
    if (!commandPostBillboardCollection) {
        commandPostBillboardCollection = new Cesium.BillboardCollection()
        addCockpitPrimitive(commandPostBillboardCollection)
    }
}
const setDetailVisibility = visible => {
    detailVisible.value = visible
    if (defenseZoneSource) defenseZoneSource.show = visible
    if (partitionSource) partitionSource.show = visible
    if (commandPostSource) commandPostSource.show = visible
    if (defenseZonePrimitive) defenseZonePrimitive.show = visible
    if (defenseZoneOutlinePrimitive) defenseZoneOutlinePrimitive.show = visible
    if (partitionPrimitive) partitionPrimitive.show = visible
    if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = visible
    if (commandPostBillboardCollection) commandPostBillboardCollection.show = visible
    if (deviceBillboardCollection) deviceBillboardCollection.show = visible
    if (deviceRingFillPrimitives.length) {
        deviceRingFillPrimitives.forEach(primitive => {
            primitive.show = visible
        })
    }
    if (deviceRingOutlinePrimitives.length) {
        deviceRingOutlinePrimitives.forEach(primitive => {
            primitive.show = visible
        })
    }
    if (!visible) closePopup()
    deviceEntityIds.forEach(id => {
        const entity = viewer?.entities?.getById(id)
        if (entity) entity.show = visible
    })
}
const setClusterVisibility = visible => {
@@ -237,12 +348,139 @@
    })
}
const buildSimulatedTrackPoints = base => {
    ensureCountyCenterMap()
    const anchor = base || { longitude: 116.397, latitude: 39.908 }
    const offsets = [
        [0, 0, 120],
        [0.02, 0.012, 150],
        [0.04, 0.006, 180],
        [0.06, -0.008, 200],
        [0.03, -0.02, 160],
        [0.0, -0.015, 140],
        [-0.02, -0.005, 120],
        [-0.01, 0.01, 130],
    ]
    return offsets.map(item => ({
        longitude: anchor.longitude + item[0],
        latitude: anchor.latitude + item[1],
        height: item[2],
    }))
}
const clearDroneTrackEntities = () => {
    if (!viewer) return
    if (droneTrackBillboardCollection) {
        droneTrackBillboardCollection.removeAll()
        removeCockpitPrimitive(droneTrackBillboardCollection)
        droneTrackBillboardCollection = null
    }
    if (droneTrackPolylineCollection) {
        droneTrackPolylineCollection.removeAll()
        removeCockpitPrimitive(droneTrackPolylineCollection)
        droneTrackPolylineCollection = null
    }
    droneTrackRuntime = []
    stopDroneTrackAnimation()
}
const ensureDroneTrackCollections = () => {
    if (!viewer) return
    ensureCockpitPrimitiveLayer()
    if (!droneTrackBillboardCollection) {
        droneTrackBillboardCollection = new Cesium.BillboardCollection()
        addCockpitPrimitive(droneTrackBillboardCollection)
    }
    if (!droneTrackPolylineCollection) {
        droneTrackPolylineCollection = new Cesium.PolylineCollection()
        addCockpitPrimitive(droneTrackPolylineCollection)
    }
}
const startDroneTrackAnimation = () => {
    if (!viewer) return
    stopDroneTrackAnimation()
    droneTrackStartTime = Cesium.JulianDate.now()
    droneTrackTickHandler = clock => {
        if (!droneTrackRuntime.length) return
        const current = clock.currentTime
        const elapsed = Cesium.JulianDate.secondsDifference(current, droneTrackStartTime)
        droneTrackRuntime.forEach(track => {
            const duration = track.duration
            if (duration <= 0) return
            const t = ((elapsed % duration) + duration) % duration
            const seg = Math.min(track.positions.length - 2, Math.floor(t / track.segmentDuration))
            const ratio = (t - seg * track.segmentDuration) / track.segmentDuration
            const pos = Cesium.Cartesian3.lerp(
                track.positions[seg],
                track.positions[seg + 1],
                ratio,
                new Cesium.Cartesian3()
            )
            track.billboard.position = pos
        })
    }
    viewer.clock.onTick.addEventListener(droneTrackTickHandler)
    viewer.clock.shouldAnimate = true
}
const stopDroneTrackAnimation = () => {
    if (!viewer || !droneTrackTickHandler) return
    viewer.clock.onTick.removeEventListener(droneTrackTickHandler)
    droneTrackTickHandler = null
}
const renderSimulatedDroneTrack = () => {
    if (!viewer) return
    ensureCountyCenterMap()
    ensureDroneTrackCollections()
    droneTrackBillboardCollection.removeAll()
    droneTrackPolylineCollection.removeAll()
    droneTrackRuntime = []
    const centers = Array.from(countyCenterMap.values())
    const baseCenters = centers.length ? centers.slice(0, 4) : [{ longitude: 116.397, latitude: 39.908 }]
    const segmentDuration = 6
    baseCenters.forEach((center, trackIndex) => {
        const points = buildSimulatedTrackPoints(center)
        if (points.length < 2) return
        const positions = points.map(point =>
            Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
        )
        droneTrackPolylineCollection.add({
            positions,
            width: 2,
            material: Cesium.Material.fromType('Color', {
                color: Cesium.Color.fromCssColorString('#2AEDBF'),
            }),
        })
        const billboard = droneTrackBillboardCollection.add({
            position: positions[0],
            image: droneIcon,
            width: 36,
            height: 36,
            verticalOrigin: Cesium.VerticalOrigin.CENTER,
        })
        droneTrackRuntime.push({
            positions,
            billboard,
            segmentDuration,
            duration: (points.length - 1) * segmentDuration,
        })
    })
    viewer.clock.multiplier = 1
    startDroneTrackAnimation()
    reorderCockpitPrimitives()
}
const RING_STYLES = [
    { inner: 0, outer: 2000, gradient: ['#FF361C', '#360B00'] }
]
const MATERIAL_TYPE = 'RadialGradientMaterial'
const TEXTURED_VERTEX_FORMAT =
    Cesium.MaterialAppearance?.MaterialSupport?.TEXTURED?.vertexFormat || Cesium.VertexFormat.POSITION_AND_ST
let materialRegistered = false
const registerRadialGradientMaterial = () => {
@@ -287,27 +525,39 @@
    return positions
}
const addDeviceRings = (center, baseId, visible) => {
const addDeviceRings = (center, ringFillInstancesByStyle, ringOutlineInstancesByStyle) => {
    if (!ringFillInstancesByStyle) return
    const centerPosition = Cesium.Cartesian3.fromDegrees(center.longitude, center.latitude, 0)
    RING_STYLES.forEach((ring, index) => {
        const outerPositions = buildCirclePositions(center, ring.outer)
        const holes = ring.inner
            ? [new Cesium.PolygonHierarchy(buildCirclePositions(center, ring.inner))]
            : []
        const entityId = `${baseId}-ring-${index}`
        deviceEntityIds.add(entityId)
        registerRadialGradientMaterial()
        const [startColor, endColor] = ring.gradient
        const color1 = Cesium.Color.fromCssColorString(startColor).withAlpha(0.34)
        const color2 = Cesium.Color.fromCssColorString(endColor).withAlpha(0.34)
        const material = new RadialGradientMaterialProperty(color1, color2)
        viewer.entities.add({
            id: entityId,
            show: visible,
            polygon: {
                hierarchy: new Cesium.PolygonHierarchy(outerPositions, holes),
                material,
            },
        })
        if (!ringFillInstancesByStyle[index]) return
        ringFillInstancesByStyle[index].push(
            new Cesium.GeometryInstance({
                geometry: new Cesium.EllipseGeometry({
                    center: centerPosition,
                    semiMajorAxis: ring.outer,
                    semiMinorAxis: ring.outer,
                    vertexFormat: TEXTURED_VERTEX_FORMAT,
                }),
            })
        )
    })
    if (!ringOutlineInstancesByStyle) return
    RING_STYLES.forEach((ring, index) => {
        if (!ringOutlineInstancesByStyle[index]) return
        const positions = buildCirclePositions(center, ring.outer)
        ringOutlineInstancesByStyle[index].push(
            new Cesium.GeometryInstance({
                geometry: new Cesium.GroundPolylineGeometry({
                    positions,
                    width: 2,
                }),
                attributes: {
                    color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                        Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(0.8)
                    ),
                },
            })
        )
    })
}
@@ -348,29 +598,69 @@
const renderDeviceEntities = devices => {
    if (!viewer) return
    clearDeviceEntities()
    ensureCockpitPrimitiveLayer()
    ensureDeviceCollections()
    deviceBillboardCollection.removeAll()
    if (deviceRingFillPrimitives.length) {
        deviceRingFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
        deviceRingFillPrimitives = []
    }
    if (deviceRingOutlinePrimitives.length) {
        deviceRingOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
        deviceRingOutlinePrimitives = []
    }
    devicePickMap.clear()
    deviceBillboardCollection.show = detailVisible.value
    const ringFillInstancesByStyle = RING_STYLES.map(() => [])
    const ringOutlineInstancesByStyle = RING_STYLES.map(() => [])
    devices.forEach((item, index) => {
        const position = getDevicePosition(item)
        if (!position) return
        const entityId = `online-device-${item.id ?? index}-${index}`
        deviceEntityIds.add(entityId)
        addDeviceRings(position, entityId, detailVisible.value)
        const entity = viewer.entities.add({
            id: entityId,
            show: detailVisible.value,
        addDeviceRings(position, ringFillInstancesByStyle, ringOutlineInstancesByStyle)
        const billboard = deviceBillboardCollection.add({
            position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
            billboard: {
                image: equipmentIcon,
                width: 40,
                height: 56,
                verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
            },
            image: equipmentIcon,
            width: 40,
            height: 56,
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
        })
        deviceEntityMap.set(entityId, { data: item, entity })
        billboard.id = entityId
        devicePickMap.set(entityId, { data: item, billboard })
    })
    if (selectedDeviceEntity && !deviceEntityMap.has(selectedDeviceEntity.id)) {
    if (selectedDeviceBillboard && !devicePickMap.has(selectedDeviceBillboard.id)) {
        closePopup()
    }
    registerRadialGradientMaterial()
    ringFillInstancesByStyle.forEach((instances, index) => {
        if (!instances.length) return
        const ring = RING_STYLES[index]
        const material = Cesium.Material.fromType(MATERIAL_TYPE, {
            color1: Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(0.64),
            color2: Cesium.Color.fromCssColorString(ring.gradient[1]).withAlpha(0.64),
        })
        const primitive = new Cesium.GroundPrimitive({
            geometryInstances: instances,
            appearance: new Cesium.MaterialAppearance({
                material,
                translucent: true,
            }),
        })
        primitive.show = detailVisible.value
        addCockpitPrimitive(primitive)
        deviceRingFillPrimitives.push(primitive)
    })
    ringOutlineInstancesByStyle.forEach(instances => {
        if (!instances.length) return
        const primitive = new Cesium.GroundPolylinePrimitive({
            geometryInstances: instances,
            appearance: new Cesium.PolylineColorAppearance(),
        })
        primitive.show = detailVisible.value
        addCockpitPrimitive(primitive)
        deviceRingOutlinePrimitives.push(primitive)
    })
    reorderCockpitPrimitives()
}
const getDefenseZonePositions = geom => {
@@ -385,70 +675,87 @@
    return list.map(item => Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude))
}
const renderZonePolygons = ({ zones, source, idPrefix, lineColor, fillGradient }) => {
    if (!viewer || !source) return
const buildZonePrimitives = (zones, lineColor, fillColor1, fillColor2) => {
    if (!viewer) return { primitive: null, outlinePrimitive: null }
    const polygonInstances = []
    const lineInstances = []
    registerRadialGradientMaterial()
    const borderColor = Cesium.Color.fromCssColorString(lineColor)
    const fillColorStart = Cesium.Color.fromCssColorString(fillGradient[0]).withAlpha(0.34)
    const fillColorEnd = Cesium.Color.fromCssColorString(fillGradient[1]).withAlpha(0.34)
    const fillMaterial = new RadialGradientMaterialProperty(fillColorStart, fillColorEnd)
    zones.forEach((zone, index) => {
    ; (zones || []).forEach((zone, index) => {
        if (!zone?.geom) return
        const positions = getDefenseZonePositions(zone.geom)
        if (!positions.length) return
        const entityId = `${idPrefix}-${zone.id ?? index}`
        const linePositions = positions.length > 1 ? [...positions, positions[0]] : positions
        source.entities.add({
            id: entityId,
            polygon: {
                hierarchy: new Cesium.PolygonHierarchy(positions),
                material: fillMaterial,
                outline: true,
                outlineColor: borderColor,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
            polyline: {
                positions: linePositions,
                clampToGround: true,
                width: 2,
                material: borderColor,
            },
        const polygon = new Cesium.PolygonGeometry({
            polygonHierarchy: new Cesium.PolygonHierarchy(positions),
            vertexFormat: TEXTURED_VERTEX_FORMAT,
        })
        polygonInstances.push(
            new Cesium.GeometryInstance({
                geometry: polygon,
            })
        )
        const linePositions = positions.length > 1 ? [...positions, positions[0]] : positions
        lineInstances.push(
            new Cesium.GeometryInstance({
                geometry: new Cesium.GroundPolylineGeometry({
                    positions: linePositions,
                    width: 2,
                }),
                attributes: {
                    color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                        Cesium.Color.fromCssColorString(lineColor)
                    ),
                },
            })
        )
    })
    let primitive = null
    if (polygonInstances.length) {
        const baseColor1 = Cesium.Color.fromCssColorString(fillColor1)
        const baseColor2 = Cesium.Color.fromCssColorString(fillColor2)
        const material = Cesium.Material.fromType(MATERIAL_TYPE, {
            color1: baseColor1.withAlpha(0.64),
            color2: baseColor2.withAlpha(0.64),
        })
        primitive = new Cesium.GroundPrimitive({
            geometryInstances: polygonInstances,
            appearance: new Cesium.MaterialAppearance({
                material,
                translucent: true,
            }),
        })
        addCockpitPrimitive(primitive)
    }
    let outlinePrimitive = null
    if (lineInstances.length) {
        outlinePrimitive = new Cesium.GroundPolylinePrimitive({
            geometryInstances: lineInstances,
            appearance: new Cesium.PolylineColorAppearance(),
        })
        addCockpitPrimitive(outlinePrimitive)
    }
    return { primitive, outlinePrimitive }
}
const renderDefenseZones = zones => {
    if (!viewer) return
    if (!defenseZoneSource) {
        defenseZoneSource = new Cesium.CustomDataSource('defenseZoneSource')
        viewer.dataSources.add(defenseZoneSource)
    }
    clearDefenseZoneEntities()
    defenseZoneSource.show = detailVisible.value
    renderZonePolygons({
        zones,
        source: defenseZoneSource,
        idPrefix: 'defense-zone',
        lineColor: '#19D266',
        fillGradient: ['#2AEDBF', '#012B11'],
    })
    const result = buildZonePrimitives(zones, '#19D266', '#2AEDBF', '#012B11')
    defenseZonePrimitive = result.primitive
    defenseZoneOutlinePrimitive = result.outlinePrimitive
    if (defenseZonePrimitive) defenseZonePrimitive.show = detailVisible.value
    if (defenseZoneOutlinePrimitive) defenseZoneOutlinePrimitive.show = detailVisible.value
    reorderCockpitPrimitives()
}
const renderPartitions = zones => {
    if (!viewer) return
    if (!partitionSource) {
        partitionSource = new Cesium.CustomDataSource('partitionSource')
        viewer.dataSources.add(partitionSource)
    }
    clearPartitionEntities()
    partitionSource.show = detailVisible.value
    renderZonePolygons({
        zones,
        source: partitionSource,
        idPrefix: 'partition-zone',
        lineColor: '#FFCD2A',
        fillGradient: ['#FFC609', '#583300'],
    })
    const result = buildZonePrimitives(zones, '#FFCD2A', '#FFC609', '#583300')
    partitionPrimitive = result.primitive
    partitionOutlinePrimitive = result.outlinePrimitive
    if (partitionPrimitive) partitionPrimitive.show = detailVisible.value
    if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = detailVisible.value
    reorderCockpitPrimitives()
}
const loadDefenseZones = async () => {
@@ -517,19 +824,18 @@
const getPickedDevice = picks => {
    for (const pick of picks) {
        const entityId = typeof pick?.id === 'string' ? pick.id : pick?.id?.id
        if (entityId && deviceEntityMap.has(entityId)) {
            return deviceEntityMap.get(entityId)
        const pickId = pick?.id
        const resolvedId = typeof pickId === 'string' ? pickId : pickId?.id
        if (resolvedId && devicePickMap.has(resolvedId)) {
            return devicePickMap.get(resolvedId)
        }
    }
    return null
}
const updatePopupPosition = () => {
    if (!viewer || !selectedDeviceEntity) return
    const cartesian =
        selectedDeviceEntity.position?.getValue?.(Cesium.JulianDate.now()) ||
        selectedDeviceEntity.position?._value
    if (!viewer || !selectedDeviceBillboard) return
    const cartesian = selectedDeviceBillboard.position
    if (!cartesian) return
    const screenPosition = viewer.scene.cartesianToCanvasCoordinates(cartesian)
    if (!screenPosition) return
@@ -558,7 +864,7 @@
        return
    }
    selectedDevice.value = pickedDevice.data
    selectedDeviceEntity = pickedDevice.entity
    selectedDeviceBillboard = pickedDevice.billboard
    startPopupRender()
}
@@ -576,7 +882,7 @@
function closePopup () {
    selectedDevice.value = null
    selectedDeviceEntity = null
    selectedDeviceBillboard = null
    stopPopupRender()
}
@@ -654,27 +960,21 @@
const renderCommandPosts = list => {
    if (!viewer) return
    if (!commandPostSource) {
        commandPostSource = new Cesium.CustomDataSource('commandPostSource')
        viewer.dataSources.add(commandPostSource)
    }
    clearCommandPostEntities()
    commandPostSource.show = detailVisible.value
        ; (list || []).forEach((item, index) => {
            const position = getDevicePosition(item)
            if (!position) return
            commandPostSource.entities.add({
                id: `command-post-${item.id ?? index}`,
                position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
                billboard: {
                    image: commandPostIcon,
                    width: 40,
                    height: 56,
                    verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
                    heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                },
            })
    ensureCommandPostCollection()
    commandPostBillboardCollection.removeAll()
    commandPostBillboardCollection.show = detailVisible.value
    ; (list || []).forEach((item, index) => {
        const position = getDevicePosition(item)
        if (!position) return
        commandPostBillboardCollection.add({
            position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
            image: commandPostIcon,
            width: 40,
            height: 56,
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
        })
    })
    reorderCockpitPrimitives()
}
const loadAggregation = async () => {
@@ -724,6 +1024,7 @@
const handleMapReady = ({ viewer: mapViewer }) => {
    viewer = mapViewer
    ensureCockpitPrimitiveLayer()
    const height = viewer?.camera?.positionCartographic?.height
    const stage = height != null && height >= 100000 ? 'cluster' : 'detail'
    updateStageDisplay(stage)
@@ -732,6 +1033,7 @@
    loadPartitions()
    loadAggregation()
    loadCommandPosts()
    renderSimulatedDroneTrack()
    initDeviceClickHandler()
}
@@ -759,8 +1061,13 @@
    clearPartitionEntities()
    clearAggregationEntities()
    clearCommandPostEntities()
    clearDroneTrackEntities()
    closePopup()
    destroyDeviceClickHandler()
    if (cockpitPrimitiveLayer && viewer?.scene?.primitives) {
        viewer.scene.primitives.remove(cockpitPrimitiveLayer)
    }
    cockpitPrimitiveLayer = null
    viewer = null
})
</script>
@@ -1092,3 +1399,4 @@
    border-color: #a1a3d4;
}
</style>