From a8b8b40b8b849def3a02cd3d655bdfd449d1c44f Mon Sep 17 00:00:00 2001
From: 罗广辉 <guanghui.luo@foxmail.com>
Date: Sat, 31 Jan 2026 17:21:50 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master'

---
 applications/drone-command/src/components/map-container/device-map-container.vue |  218 ++++++++++++++++++++++++++++++++++++++++--------------
 1 files changed, 161 insertions(+), 57 deletions(-)

diff --git a/applications/drone-command/src/components/map-container/device-map-container.vue b/applications/drone-command/src/components/map-container/device-map-container.vue
index 829f2e9..650d950 100644
--- a/applications/drone-command/src/components/map-container/device-map-container.vue
+++ b/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

--
Gitblit v1.9.3