吉安感知网项目-前端
罗广辉
2026-01-31 a8b8b40b8b849def3a02cd3d655bdfd449d1c44f
applications/drone-command/src/components/map-container/device-map-container.vue
@@ -77,6 +77,7 @@
const CLUSTER_HEIGHT = 100000
const DETAIL_HEIGHT = 10000
const POLYGON_HEIGHT_M = 0.2
const DRONE_TRACK_DURATION_S = 30 * 60
const props = defineProps({
   allDevices: {
@@ -119,8 +120,12 @@
let droneTrackBillboardCollection = null
let droneTrackPolylineCollection = null
let droneTrackTickHandler = null
let droneTrackRafId = null
let droneTrackStartTime = null
let droneTrackAnimStartAt = 0
let droneTrackLastTickAt = 0
let droneTrackRuntime = []
let droneTrackSource = null
let mapReadyHandled = false
let favoritePulseRafId = null
let favoritePulseStartAt = 0
@@ -223,16 +228,32 @@
      offset: Math.random() * PULSE_DURATION_S,
      entity: null,
   }
   const getPulseHeight = () => {
      const pos = getBillboardPosition(billboard)
      if (!pos) return 0
      const carto = Cesium.Cartographic.fromCartesian(pos)
      return carto?.height ?? 0
   }
   pulse.entity = viewer.entities.add({
      position: new Cesium.CallbackProperty(() => billboard.position, false),
      position: new Cesium.CallbackProperty(() => getBillboardPosition(billboard), false),
      ellipse: {
         semiMajorAxis: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false),
         semiMinorAxis: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false),
         material,
         heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
         height: new Cesium.CallbackProperty(() => getPulseHeight(), false),
         heightReference: Cesium.HeightReference.NONE,
      },
   })
   favoritePulseEntities.push(pulse)
}
const getBillboardPosition = billboard => {
   if (!viewer || !billboard) return null
   const position = billboard.position
   if (position?.getValue) {
      return position.getValue(viewer.clock.currentTime)
   }
   return position
}
const getDevicePosition = item => {
@@ -411,6 +432,7 @@
const setDroneVisibility = visible => {
   if (droneTrackBillboardCollection) droneTrackBillboardCollection.show = visible
   if (droneTrackPolylineCollection) droneTrackPolylineCollection.show = visible
   if (droneTrackSource) droneTrackSource.show = visible
   if (favoritePulseEntities.length) {
      favoritePulseEntities.forEach(pulse => {
         if (pulse?.entity) pulse.entity.show = visible
@@ -537,6 +559,10 @@
      }
      droneTrackPolylineCollection = null
   }
   if (droneTrackSource) {
      viewer.dataSources.remove(droneTrackSource)
      droneTrackSource = null
   }
   droneTrackRuntime = []
   if (favoritePulseEntities.length) {
      favoritePulseEntities.forEach(pulse => {
@@ -570,44 +596,96 @@
   }
}
const ensureDroneTrackSource = () => {
   if (!viewer) return
   if (!droneTrackSource) {
      droneTrackSource = new Cesium.CustomDataSource('droneTrackSource')
      viewer.dataSources.add(droneTrackSource)
   }
}
const setupDroneTrackClock = () => {
   if (!viewer) return
   const startTime = Cesium.JulianDate.now()
   const stopTime = Cesium.JulianDate.addSeconds(startTime, DRONE_TRACK_DURATION_S, new Cesium.JulianDate())
   viewer.clock.startTime = startTime
   viewer.clock.stopTime = stopTime
   viewer.clock.currentTime = startTime
   viewer.clock.multiplier = 1
   viewer.clock.clockStep = Cesium.ClockStep.SYSTEM_CLOCK_MULTIPLIER
   viewer.clock.clockRange = Cesium.ClockRange.CLAMPED
}
const updateDroneTrackPositions = elapsed => {
   if (!viewer || !viewer.scene || !droneTrackRuntime.length) return
   droneTrackRuntime.forEach(track => {
      if (!track?.billboard || track.billboard.isDestroyed?.()) return
      if (!track?.polyline) return
      if (!track.positions || track.positions.length < 2) return
      const duration = track.duration
      if (duration <= 0) return
      const t = Math.min(Math.max(elapsed, 0), duration)
      const seg = Math.min(track.positions.length - 2, Math.floor(t / track.segmentDuration))
      const ratio = (t - seg * track.segmentDuration) / track.segmentDuration
      const isEntityPosition = typeof track.billboard?.position?.getValue === 'function'
      const pos = isEntityPosition
         ? getBillboardPosition(track.billboard)
         : Cesium.Cartesian3.lerp(
               track.positions[seg],
               track.positions[seg + 1],
               ratio,
               new Cesium.Cartesian3()
           )
      if (!pos) return
      if (!isEntityPosition) {
         track.billboard.position = pos
      }
      const pathCount = Math.min(track.positions.length - 1, Math.floor(t / track.segmentDuration))
      const pathPositions = track.positions.slice(0, pathCount + 1)
      track.polyline.positions = [...pathPositions, pos]
   })
}
const startDroneTrackAnimation = () => {
   if (!viewer) return
   stopDroneTrackAnimation()
   droneTrackStartTime = Cesium.JulianDate.now()
   droneTrackTickHandler = clock => {
      if (!viewer || viewer.isDestroyed?.() || !viewer.scene) {
         stopDroneTrackAnimation()
   droneTrackStartTime = viewer.clock.startTime
   viewer.clock.shouldAnimate = true
   droneTrackAnimStartAt = performance.now()
   droneTrackLastTickAt = droneTrackAnimStartAt
   const renderTick = now => {
      if (!viewer || viewer.isDestroyed?.()) {
         droneTrackRafId = null
         return
      }
      if (!droneTrackRuntime.length) return
      const current = clock.currentTime
      const elapsed = Cesium.JulianDate.secondsDifference(current, droneTrackStartTime)
      droneTrackRuntime.forEach(track => {
         if (!track?.billboard || track.billboard.isDestroyed?.()) return
         if (!track.billboard._scene) return
         if (!track.positions || track.positions.length < 2) return
         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()
      const deltaSeconds = Math.max(0, (now - droneTrackLastTickAt) / 1000)
      droneTrackLastTickAt = now
      if (viewer.clock?.currentTime && viewer.clock?.stopTime) {
         const nextTime = Cesium.JulianDate.addSeconds(
            viewer.clock.currentTime,
            deltaSeconds * (viewer.clock.multiplier || 1),
            new Cesium.JulianDate()
         )
         track.billboard.position = pos
      })
         if (Cesium.JulianDate.greaterThan(nextTime, viewer.clock.stopTime)) {
            viewer.clock.currentTime = viewer.clock.stopTime
         } else {
            viewer.clock.currentTime = nextTime
         }
      }
      const elapsed = Cesium.JulianDate.secondsDifference(viewer.clock.currentTime, droneTrackStartTime)
      updateDroneTrackPositions(elapsed)
      viewer.scene?.requestRender?.()
      droneTrackRafId = requestAnimationFrame(renderTick)
   }
   viewer.clock.onTick.addEventListener(droneTrackTickHandler)
   viewer.clock.shouldAnimate = true
   droneTrackRafId = requestAnimationFrame(renderTick)
}
const stopDroneTrackAnimation = () => {
   if (!viewer || !droneTrackTickHandler) return
   viewer.clock.onTick.removeEventListener(droneTrackTickHandler)
   droneTrackTickHandler = null
   if (droneTrackRafId) {
      cancelAnimationFrame(droneTrackRafId)
      droneTrackRafId = null
   }
}
const startFavoritePulseAnimation = () => {
@@ -637,10 +715,11 @@
   clearDroneTrackEntities()
   if (!list?.length) return
   ensureDroneTrackCollections()
   ensureDroneTrackSource()
   setupDroneTrackClock()
   droneTrackBillboardCollection.show = detailVisible.value
   droneTrackPolylineCollection.show = detailVisible.value
   droneTrackRuntime = []
   const segmentDuration = 6
   const baseTrackColor = Cesium.Color.fromCssColorString('red')
      ; (list || []).forEach((item, trackIndex) => {
         const position = getDevicePosition(item)
@@ -650,6 +729,7 @@
         const positions = points.map(point =>
            Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
         )
         const segmentDuration = DRONE_TRACK_DURATION_S / Math.max(positions.length - 1, 1)
         let trackMaterial = createDroneTrackMaterial({
            color: baseTrackColor,
            speed: 4.5,
@@ -660,43 +740,67 @@
         if (!trackMaterial) {
            trackMaterial = Cesium.Material.fromType('Color', { color: baseTrackColor })
         }
         droneTrackPolylineCollection.add({
            positions,
         const polyline = droneTrackPolylineCollection.add({
            positions: [positions[0]],
            width: 3,
            material: trackMaterial,
         })
         const droneId = `drone-alarm-${item?.alarmRecordId ?? item?.id ?? trackIndex}`
         const billboard = droneTrackBillboardCollection.add({
            position: positions[positions.length - 1],
            image: droneIcon,
            width: 36,
            height: 36,
            verticalOrigin: Cesium.VerticalOrigin.CENTER,
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
         const startTime = viewer.clock.startTime
         const stopTime = viewer.clock.stopTime
         const positionProperty = new Cesium.SampledPositionProperty()
         positions.forEach((pos, index) => {
            const sampleTime = Cesium.JulianDate.addSeconds(
               startTime,
               index * segmentDuration,
               new Cesium.JulianDate()
            )
            positionProperty.addSample(sampleTime, pos)
         })
         billboard.id = droneId
         positionProperty.setInterpolationOptions({
            interpolationDegree: 1,
            interpolationAlgorithm: Cesium.LinearApproximation,
         })
         const entity = droneTrackSource.entities.add({
            id: droneId,
            availability: new Cesium.TimeIntervalCollection([
               new Cesium.TimeInterval({
                  start: startTime,
                  stop: stopTime,
               }),
            ]),
            position: positionProperty,
            orientation: new Cesium.VelocityOrientationProperty(positionProperty),
            billboard: {
               image: droneIcon,
               width: 36,
               height: 36,
               verticalOrigin: Cesium.VerticalOrigin.CENTER,
               disableDepthTestDistance: Number.POSITIVE_INFINITY,
            },
         })
         const speedMs = Math.round(Cesium.Cartesian3.distance(positions[0], positions[1]) / segmentDuration)
      dronePickMap.set(droneId, {
         data: {
            ...item,
            flightHeightM: item.flightHeightM ?? points[0].height,
            flightSpeedMs: item.flightSpeedMs ?? speedMs,
            longitude: item.longitude ?? points[0].longitude,
            latitude: item.latitude ?? points[0].latitude,
         },
         billboard,
      })
      if (isFavorited(item)) {
         createFavoritePulseEntity(billboard)
      }
         dronePickMap.set(droneId, {
            data: {
               ...item,
               flightHeightM: item.flightHeightM ?? points[0].height,
               flightSpeedMs: item.flightSpeedMs ?? speedMs,
               longitude: item.longitude ?? points[0].longitude,
               latitude: item.latitude ?? points[0].latitude,
            },
            billboard: entity,
         })
         if (isFavorited(item)) {
            createFavoritePulseEntity(entity)
         }
         droneTrackRuntime.push({
            positions,
            billboard,
            polyline,
            billboard: entity,
            segmentDuration,
            duration: (points.length - 1) * segmentDuration,
            duration: DRONE_TRACK_DURATION_S,
         })
      })
   viewer.clock.multiplier = 1
   startDroneTrackAnimation()
   startFavoritePulseAnimation()
   reorderCockpitPrimitives()
@@ -1045,7 +1149,7 @@
const updatePopupPosition = () => {
   if (!viewer || !selectedDeviceBillboard) return
   const cartesian = selectedDeviceBillboard.position
   const cartesian = getBillboardPosition(selectedDeviceBillboard)
   if (!cartesian) return
   const screenPosition = viewer.scene.cartesianToCanvasCoordinates(cartesian)
   if (!screenPosition) return