| | |
| | | const CLUSTER_HEIGHT = 100000 |
| | | const DETAIL_HEIGHT = 10000 |
| | | const POLYGON_HEIGHT_M = 0.2 |
| | | const DRONE_TRACK_DURATION_S = 10 * 60 |
| | | |
| | | const props = defineProps({ |
| | | allDevices: { |
| | |
| | | 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 |
| | |
| | | entity: null, |
| | | } |
| | | 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), |
| | |
| | | }, |
| | | }) |
| | | 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 => { |
| | |
| | | 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 |
| | |
| | | } |
| | | droneTrackPolylineCollection = null |
| | | } |
| | | if (droneTrackSource) { |
| | | viewer.dataSources.remove(droneTrackSource) |
| | | droneTrackSource = null |
| | | } |
| | | droneTrackRuntime = [] |
| | | if (favoritePulseEntities.length) { |
| | | favoritePulseEntities.forEach(pulse => { |
| | |
| | | } |
| | | } |
| | | |
| | | 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 = () => { |
| | |
| | | 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) |
| | |
| | | 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, |
| | |
| | | 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() |
| | |
| | | |
| | | 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 |