吉安感知网项目-前端
张含笑
2026-01-23 da563dc595339d42e2135f63167906d6fa20e06c
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>