From 7319ec49da9f1490ad35f47e0da62546d1cc7ff4 Mon Sep 17 00:00:00 2001
From: 罗广辉 <guanghui.luo@foxmail.com>
Date: Tue, 27 Jan 2026 18:05:34 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master'
---
applications/drone-command/src/components/map-container/device-map-container.vue | 343 +++++++++++++++++++++++++++++++++++++++++---------------
1 files changed, 251 insertions(+), 92 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 49bd52b..f7fb7e9 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
@@ -39,7 +39,7 @@
@close="closePopup" />
<DronePopup :visible="popupVisible && isDronePopup" :position="popupPosition" :drone="selectedDevice"
- :favorite="Boolean(selectedDevice?.isFavorite)" @close="closePopup" @toggle-favorite="toggleDroneFavorite"
+ :favorite="Boolean(selectedDevice?.isFavorite)" @close="closePopup" @toggle-favorite="handleDroneFavorite"
@signal="handleDroneSignal" @counter="handleDroneCounter" />
</div>
</template>
@@ -78,6 +78,10 @@
type: Array,
default: () => [],
},
+ alarmDrones: {
+ type: Array,
+ default: () => [],
+ },
leftCollapsed: {
type: Boolean,
default: false,
@@ -91,7 +95,7 @@
default: true,
},
})
-const emit = defineEmits(['droneSignal', 'droneCounter'])
+const emit = defineEmits(['droneSignal', 'droneCounter', 'droneFavorite'])
const DEFAULT_ZONE_PAGE_SIZE = 999
@@ -115,6 +119,14 @@
let droneTrackTickHandler = null
let droneTrackStartTime = null
let droneTrackRuntime = []
+let favoritePulseRafId = null
+let favoritePulseStartAt = 0
+let favoritePulseElapsed = 0
+const favoritePulseEntities = []
+const pulseBaseColor = Cesium.Color.fromCssColorString('#FF3B30')
+const PULSE_MIN_RADIUS_M = 60
+const PULSE_MAX_RADIUS_M = 180
+const PULSE_DURATION_S = 1.6
const detailVisible = ref(true)
const clusterVisible = ref(false)
const countyCenterMap = new Map()
@@ -168,6 +180,57 @@
*/
const adminBoundaryVisible = ref(treeCheckedKeys.value.includes('admin'))
+
+const isFavorited = item => {
+ const value = item?.favorited ?? item?.isFavorite
+ return value === 1 || value === '1' || value === true
+}
+
+let pulseCanvas = null
+const getPulseCanvas = () => {
+ if (pulseCanvas) return pulseCanvas
+ const size = 256
+ const canvas = document.createElement('canvas')
+ canvas.width = size
+ canvas.height = size
+ const ctx = canvas.getContext('2d')
+ const center = size / 2
+ const radius = center - 2
+ const gradient = ctx.createRadialGradient(center, center, radius * 0.1, center, center, radius)
+ gradient.addColorStop(0, 'rgba(255,59,48,0.55)')
+ gradient.addColorStop(0.35, 'rgba(255,59,48,0.35)')
+ gradient.addColorStop(0.7, 'rgba(255,59,48,0.15)')
+ gradient.addColorStop(1, 'rgba(255,59,48,0)')
+ ctx.fillStyle = gradient
+ ctx.beginPath()
+ ctx.arc(center, center, radius, 0, Math.PI * 2)
+ ctx.fill()
+ pulseCanvas = canvas
+ return canvas
+}
+
+const createFavoritePulseEntity = (billboard) => {
+ if (!viewer || !billboard) return
+ const material = new Cesium.ImageMaterialProperty({
+ image: getPulseCanvas(),
+ transparent: true,
+ })
+ const pulse = {
+ material,
+ offset: Math.random() * PULSE_DURATION_S,
+ entity: null,
+ }
+ pulse.entity = viewer.entities.add({
+ position: new Cesium.CallbackProperty(() => billboard.position, false),
+ ellipse: {
+ semiMajorAxis: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false),
+ semiMinorAxis: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false),
+ material,
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ },
+ })
+ favoritePulseEntities.push(pulse)
+}
const getDevicePosition = item => {
const longitudeRaw = item.longitude ?? item.lng ?? item.lon
@@ -307,6 +370,12 @@
}
}
+const getPulseRadius = pulse => {
+ const phase = ((favoritePulseElapsed + pulse.offset) % PULSE_DURATION_S) / PULSE_DURATION_S
+ const wave = phase < 0.5 ? phase * 2 : (1 - phase) * 2
+ return PULSE_MIN_RADIUS_M + (PULSE_MAX_RADIUS_M - PULSE_MIN_RADIUS_M) * wave
+}
+
const setDetailVisibility = visible => {
detailVisible.value = visible
if (defenseZonePrimitive) defenseZonePrimitive.show = visible
@@ -341,6 +410,11 @@
const setDroneVisibility = visible => {
if (droneTrackBillboardCollection) droneTrackBillboardCollection.show = visible
if (droneTrackPolylineCollection) droneTrackPolylineCollection.show = visible
+ if (favoritePulseEntities.length) {
+ favoritePulseEntities.forEach(pulse => {
+ if (pulse?.entity) pulse.entity.show = visible
+ })
+ }
if (!visible && selectedTargetType.value === 'drone') {
closePopup()
}
@@ -363,50 +437,93 @@
})
}
-const buildSimulatedTrackPoints = () => {
- return [
- {
- longitude: 114.963191,
- latitude: 27.136716,
- height: 120
- },
- {
- longitude: 114.957308,
- latitude: 27.138452,
- height: 120
- },
- {
- longitude: 114.952,
- latitude: 27.136317,
- height: 120
- },
- {
- longitude: 114.949293,
- latitude: 27.133864,
- height: 120
- },
- {
- longitude: 114.944666,
- latitude: 27.130526,
- height: 120
- },
- {
- longitude: 114.945909,
- latitude: 27.127845,
- height: 120
- },
- {
- longitude: 114.962974,
- latitude: 27.136242,
- height: 120
- },
+const buildSimulatedTrackPoints = center => {
+ const basePoints = [
+ [
+ {
+ longitude: 114.963191,
+ latitude: 27.136716,
+ height: 120
+ },
+ {
+ longitude: 114.957308,
+ latitude: 27.138452,
+ height: 120
+ },
+ {
+ longitude: 114.952,
+ latitude: 27.136317,
+ height: 120
+ },
+ {
+ longitude: 114.949293,
+ latitude: 27.133864,
+ height: 120
+ },
+ {
+ longitude: 114.944666,
+ latitude: 27.130526,
+ height: 120
+ },
+ {
+ longitude: 114.945909,
+ latitude: 27.127845,
+ height: 120
+ },
+ {
+ longitude: 114.962974,
+ latitude: 27.136242,
+ height: 120
+ },
+ ],
+
+ [
+ {
+ longitude: 114.931063,
+ latitude: 27.095052,
+ height: 120
+ },
+ {
+ longitude: 114.928653,
+ latitude: 27.096307,
+ height: 120
+ },
+ {
+ longitude: 114.925958,
+ latitude: 27.096963,
+ height: 120
+ },
+ {
+ longitude: 114.925836,
+ latitude: 27.096102,
+ height: 120
+ },
+ {
+ longitude: 114.928384,
+ latitude: 27.095866,
+ height: 120
+ },
+ {
+ longitude: 114.929874,
+ latitude: 27.096095,
+ height: 120
+ },
+ {
+ longitude: 114.931048,
+ latitude: 27.097666,
+ height: 120
+ },
+ ]
]
+
+ return basePoints[center.trackIndex]
}
const clearDroneTrackEntities = () => {
if (!viewer) return
stopDroneTrackAnimation()
+ stopFavoritePulseAnimation()
if (droneTrackBillboardCollection) {
if (!droneTrackBillboardCollection.isDestroyed?.()) {
removeCockpitPrimitive(droneTrackBillboardCollection)
@@ -420,6 +537,12 @@
droneTrackPolylineCollection = null
}
droneTrackRuntime = []
+ if (favoritePulseEntities.length) {
+ favoritePulseEntities.forEach(pulse => {
+ if (pulse?.entity) viewer?.entities?.remove(pulse.entity)
+ })
+ favoritePulseEntities.length = 0
+ }
dronePickMap.clear()
if (selectedTargetType.value === 'drone') {
closePopup()
@@ -486,72 +609,95 @@
droneTrackTickHandler = null
}
-const renderSimulatedDroneTrack = () => {
+const startFavoritePulseAnimation = () => {
+ if (!viewer || favoritePulseRafId) return
+ favoritePulseStartAt = performance.now()
+ const tick = now => {
+ if (!viewer || viewer.isDestroyed?.() || !favoritePulseEntities.length) {
+ favoritePulseRafId = null
+ return
+ }
+ favoritePulseElapsed = (now - favoritePulseStartAt) / 1000
+ viewer.scene.requestRender()
+ favoritePulseRafId = requestAnimationFrame(tick)
+ }
+ favoritePulseRafId = requestAnimationFrame(tick)
+}
+
+const stopFavoritePulseAnimation = () => {
+ if (favoritePulseRafId) {
+ cancelAnimationFrame(favoritePulseRafId)
+ favoritePulseRafId = null
+ }
+}
+
+const renderSimulatedDroneTrack = (list) => {
if (!viewer) return
clearDroneTrackEntities()
+ if (!list?.length) return
ensureDroneTrackCollections()
droneTrackBillboardCollection.show = detailVisible.value
droneTrackPolylineCollection.show = detailVisible.value
droneTrackRuntime = []
- const baseCenters = [['默认区域', { longitude: 114.958541, latitude: 27.121917 }]]
const segmentDuration = 6
const baseTrackColor = Cesium.Color.fromCssColorString('red')
- baseCenters.forEach(([centerName, 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)
- )
- let trackMaterial = createDroneTrackMaterial({
- color: baseTrackColor,
- speed: 4.5,
- headWidth: 0.2,
- glowPower: 1.8,
- backgroundAlpha: 0.34,
- })
- if (!trackMaterial) {
- trackMaterial = Cesium.Material.fromType('Color', { color: baseTrackColor })
- }
- droneTrackPolylineCollection.add({
- positions,
- width: 3,
- material: trackMaterial,
- })
- const droneId = `drone-track-${trackIndex}`
- const billboard = droneTrackBillboardCollection.add({
- position: positions[positions.length - 1],
- image: droneIcon,
- width: 36,
- height: 36,
- verticalOrigin: Cesium.VerticalOrigin.CENTER,
- disableDepthTestDistance: Number.POSITIVE_INFINITY,
- })
- billboard.id = droneId
- const speedMs = Math.round(Cesium.Cartesian3.distance(positions[0], positions[1]) / segmentDuration)
+ ; (list || []).forEach((item, trackIndex) => {
+ const position = getDevicePosition(item)
+ if (!position) return
+ const points = buildSimulatedTrackPoints({ ...position, height: item.flightHeightM, trackIndex })
+ if (points.length < 2) return
+ const positions = points.map(point =>
+ Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
+ )
+ let trackMaterial = createDroneTrackMaterial({
+ color: baseTrackColor,
+ speed: 4.5,
+ headWidth: 0.2,
+ glowPower: 1.8,
+ backgroundAlpha: 0.34,
+ })
+ if (!trackMaterial) {
+ trackMaterial = Cesium.Material.fromType('Color', { color: baseTrackColor })
+ }
+ droneTrackPolylineCollection.add({
+ positions,
+ 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,
+ })
+ billboard.id = droneId
+ const speedMs = Math.round(Cesium.Cartesian3.distance(positions[0], positions[1]) / segmentDuration)
dronePickMap.set(droneId, {
data: {
- droneName: `无人机-${trackIndex + 1}`,
- droneSerialNo: `UAV-${trackIndex + 1}`,
- flightHeightM: points[0].height,
- flightSpeedMs: speedMs,
- longitude: points[0].longitude,
- latitude: points[0].latitude,
- flightStatus: 1,
- deviceName: centerName,
- dataSource: centerName,
- counterDeviceName: centerName,
+ ...item,
+ flightHeightM: item.flightHeightM ?? points[0].height,
+ flightSpeedMs: item.flightSpeedMs ?? speedMs,
+ longitude: item.longitude ?? points[0].longitude,
+ latitude: item.latitude ?? points[0].latitude,
},
billboard,
})
- droneTrackRuntime.push({
- positions,
- billboard,
- segmentDuration,
- duration: (points.length - 1) * segmentDuration,
+ if (isFavorited(item)) {
+ createFavoritePulseEntity(billboard)
+ }
+ droneTrackRuntime.push({
+ positions,
+ billboard,
+ segmentDuration,
+ duration: (points.length - 1) * segmentDuration,
+ })
})
- })
viewer.clock.multiplier = 1
startDroneTrackAnimation()
+ startFavoritePulseAnimation()
reorderCockpitPrimitives()
}
@@ -860,9 +1006,9 @@
const popupPosition = ref({ x: 0, y: 0 })
const popupVisible = computed(() => Boolean(selectedDevice.value))
const isDronePopup = computed(() => selectedTargetType.value === 'drone')
-const toggleDroneFavorite = () => {
+const handleDroneFavorite = () => {
if (!selectedDevice.value) return
- selectedDevice.value.isFavorite = !selectedDevice.value.isFavorite
+ emit('droneFavorite', selectedDevice.value)
}
const handleDroneSignal = () => emit('droneSignal', selectedDevice.value)
const handleDroneCounter = () => emit('droneCounter', selectedDevice.value)
@@ -911,6 +1057,19 @@
() => props.onlineDevices,
devices => {
renderDeviceEntities(devices || [])
+ },
+ { deep: true }
+)
+
+watch(
+ () => props.alarmDrones,
+ list => {
+ renderSimulatedDroneTrack(list || [])
+ if (selectedTargetType.value === 'drone' && selectedDevice.value) {
+ const selectedId = selectedDevice.value.alarmRecordId ?? selectedDevice.value.id
+ const match = (list || []).find(item => (item?.alarmRecordId ?? item?.id) === selectedId)
+ if (match) selectedDevice.value = { ...selectedDevice.value, ...match }
+ }
},
{ deep: true }
)
@@ -970,7 +1129,7 @@
loadPartitions()
loadAggregation()
loadCommandPosts()
- renderSimulatedDroneTrack()
+ renderSimulatedDroneTrack(props.alarmDrones)
initDeviceClickHandler()
}
--
Gitblit v1.9.3