| | |
| | | @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> |
| | |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | alarmDrones: { |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | leftCollapsed: { |
| | | type: Boolean, |
| | | default: false, |
| | |
| | | default: true, |
| | | }, |
| | | }) |
| | | const emit = defineEmits(['droneSignal', 'droneCounter']) |
| | | const emit = defineEmits(['droneSignal', 'droneCounter', 'droneFavorite']) |
| | | |
| | | const DEFAULT_ZONE_PAGE_SIZE = 999 |
| | | |
| | |
| | | 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() |
| | |
| | | */ |
| | | |
| | | 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 |
| | |
| | | } |
| | | } |
| | | |
| | | 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 |
| | |
| | | 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() |
| | | } |
| | |
| | | }) |
| | | } |
| | | |
| | | 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) |
| | |
| | | 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() |
| | |
| | | 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() |
| | | } |
| | | |
| | |
| | | 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) |
| | |
| | | () => 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 } |
| | | ) |
| | |
| | | loadPartitions() |
| | | loadAggregation() |
| | | loadCommandPosts() |
| | | renderSimulatedDroneTrack() |
| | | renderSimulatedDroneTrack(props.alarmDrones) |
| | | initDeviceClickHandler() |
| | | } |
| | | |