| | |
| | | <template> |
| | | <CommonCesiumMap |
| | | ref="mapRef" |
| | | class="command-cesium map-container" |
| | | :dom-id="props.containerId" |
| | | :active="true" |
| | | :flat-mode="false" |
| | | :terrain="false" |
| | | :layer-mode="4" |
| | | :contour="false" |
| | | :boundary="false" |
| | | :show-admin-boundary="true" |
| | | :zoom-to-boundary="true" |
| | | @ready="handleMapReady" |
| | | /> |
| | | <div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.leftCollapsed }"> |
| | | <div class="layer-control-wrap" ref="layerWrapRef"> |
| | | <div class="layer-control" @click="toggleLayerPanel"> |
| | | <img :src="layerControlIcon" alt="图层控制" /> |
| | | </div> |
| | | <div v-if="showLayerPanel" class="layer-panel"> |
| | | <div class="panel-title">图层管理</div> |
| | | <template> |
| | | <div class="map-shell"> |
| | | <CommonCesiumMap ref="mapRef" class="command-cesium map-container" :dom-id="props.containerId" :active="true" |
| | | :flat-mode="false" :terrain="true" :layer-mode="4" :contour="false" :boundary="false" |
| | | :show-admin-boundary="true" :zoom-to-boundary="true" :enable-stage-emit="true" :cluster-height="100000" |
| | | @ready="handleMapReady" @stage-change="handleStageChange" /> |
| | | <div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.leftCollapsed }"> |
| | | <div class="layer-control-wrap" ref="layerWrapRef"> |
| | | <div class="layer-control" @click="toggleLayerPanel"> |
| | | <img :src="layerControlIcon" alt="图层控制" /> |
| | | </div> |
| | | <div v-if="showLayerPanel" class="layer-panel"> |
| | | <div class="panel-title">图层管理</div> |
| | | |
| | | <div class="panel-content"> |
| | | <el-tree |
| | | :data="layerTree" |
| | | show-checkbox |
| | | default-expand-all |
| | | node-key="key" |
| | | :props="layerTreeProps" |
| | | :default-checked-keys="defaultCheckedKeys" |
| | | /> |
| | | <div class="panel-content"> |
| | | <el-tree :data="layerTree" show-checkbox default-expand-all node-key="key" |
| | | :props="layerTreeProps" :default-checked-keys="defaultCheckedKeys" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <DevicePopup |
| | | :visible="popupVisible && !isDronePopup" |
| | | :position="popupPosition" |
| | | :device="selectedDevice" |
| | | @close="closePopup" |
| | | /> |
| | | |
| | | <DronePopup |
| | | :visible="popupVisible && isDronePopup" |
| | | :position="popupPosition" |
| | | :drone="selectedDevice" |
| | | :favorite="Boolean(selectedDevice?.isFavorite)" |
| | | @close="closePopup" |
| | | @toggle-favorite="toggleDroneFavorite" |
| | | @signal="handleDroneSignal" |
| | | @counter="handleDroneCounter" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import { geomAnalysis } from '@ztzf/utils' |
| | | import { fwDefenseZonePageApi } from '@/views/areaManage/defenseZone/defenseZoneApi' |
| | | import { fwAreaDividePageApi } from '@/views/areaManage/partition/partitionApi' |
| | | import { fwDefenseSceneListApi } from '@/views/areaManage/sceneConfig/sceneConfigApi' |
| | | import { cockpitAggregationApi } from '@/api/dataCockpit' |
| | | import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png' |
| | | import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png' |
| | | import droneIcon from '@/assets/images/dataCockpit/map/drone.png' |
| | | import aggregationIcon from '@/assets/images/dataCockpit/map/aggregation.png' |
| | | import commandPostIcon from '@/assets/images/dataCockpit/map/command-post.png' |
| | | import jaGeojsonRaw from '@/assets/geojson/ja.geojson?raw' |
| | | import DevicePopup from './components/DevicePopup.vue' |
| | | import DronePopup from './components/DronePopup.vue' |
| | | import { |
| | | RING_STYLES, |
| | | addDeviceRings, |
| | | createDroneTrackMaterial, |
| | | createRadialGradientMaterial, |
| | | getTexturedVertexFormat, |
| | | } from './device-map-materials' |
| | | |
| | | const props = defineProps({ |
| | | onlineDevices: { |
| | |
| | | default: true, |
| | | }, |
| | | }) |
| | | const emit = defineEmits(['droneSignal', 'droneCounter']) |
| | | |
| | | const DEFAULT_ZONE_PAGE_SIZE = 999 |
| | | |
| | | const mapRef = ref(null) |
| | | let viewer = null |
| | | const deviceEntityIds = new Set() |
| | | let defenseZoneSource = null |
| | | let partitionSource = null |
| | | let cockpitPrimitiveLayer = null |
| | | const devicePickMap = new Map() |
| | | const dronePickMap = new Map() |
| | | let deviceBillboardCollection = null |
| | | let deviceRingOutlinePrimitives = [] |
| | | let deviceRingFillPrimitives = [] |
| | | let defenseZonePrimitive = null |
| | | let defenseZoneOutlinePrimitive = null |
| | | let partitionPrimitive = null |
| | | let partitionOutlinePrimitive = null |
| | | let aggregationSource = null |
| | | let commandPostBillboardCollection = null |
| | | let droneTrackBillboardCollection = null |
| | | let droneTrackPolylineCollection = null |
| | | let droneTrackTickHandler = null |
| | | let droneTrackStartTime = null |
| | | let droneTrackRuntime = [] |
| | | const detailVisible = ref(true) |
| | | const clusterVisible = ref(false) |
| | | const countyCenterMap = new Map() |
| | | const showLayerPanel = ref(false) |
| | | const layerWrapRef = ref(null) |
| | | const selectedDevice = ref(null) |
| | | const selectedTargetType = ref('device') |
| | | let selectedDeviceBillboard = null |
| | | let deviceClickHandler = null |
| | | let popupRenderHandler = null |
| | | const layerTreeProps = { |
| | | label: 'label', |
| | | children: 'children', |
| | |
| | | return { longitude, latitude } |
| | | } |
| | | |
| | | const ensureCockpitPrimitiveLayer = () => { |
| | | if (!viewer) return |
| | | if (!cockpitPrimitiveLayer) { |
| | | cockpitPrimitiveLayer = new Cesium.PrimitiveCollection({ destroyPrimitives: false }) |
| | | viewer.scene.primitives.add(cockpitPrimitiveLayer) |
| | | } |
| | | } |
| | | |
| | | const addCockpitPrimitive = primitive => { |
| | | if (!primitive) return |
| | | if (cockpitPrimitiveLayer) { |
| | | cockpitPrimitiveLayer.add(primitive) |
| | | return |
| | | } |
| | | viewer?.scene?.primitives?.add(primitive) |
| | | } |
| | | |
| | | const removeCockpitPrimitive = (primitive, destroy = true) => { |
| | | if (!primitive) return |
| | | if (cockpitPrimitiveLayer) { |
| | | cockpitPrimitiveLayer.remove(primitive, destroy) |
| | | return |
| | | } |
| | | viewer?.scene?.primitives?.remove(primitive) |
| | | } |
| | | |
| | | const reorderCockpitPrimitives = () => { |
| | | if (!viewer) return |
| | | ensureCockpitPrimitiveLayer() |
| | | cockpitPrimitiveLayer.removeAll(false) |
| | | if (defenseZonePrimitive) cockpitPrimitiveLayer.add(defenseZonePrimitive) |
| | | if (defenseZoneOutlinePrimitive) cockpitPrimitiveLayer.add(defenseZoneOutlinePrimitive) |
| | | if (partitionPrimitive) cockpitPrimitiveLayer.add(partitionPrimitive) |
| | | if (partitionOutlinePrimitive) cockpitPrimitiveLayer.add(partitionOutlinePrimitive) |
| | | if (deviceRingFillPrimitives.length) { |
| | | deviceRingFillPrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive)) |
| | | } |
| | | if (deviceRingOutlinePrimitives.length) { |
| | | deviceRingOutlinePrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive)) |
| | | } |
| | | if (droneTrackPolylineCollection) cockpitPrimitiveLayer.add(droneTrackPolylineCollection) |
| | | if (droneTrackBillboardCollection) cockpitPrimitiveLayer.add(droneTrackBillboardCollection) |
| | | if (deviceBillboardCollection) cockpitPrimitiveLayer.add(deviceBillboardCollection) |
| | | if (commandPostBillboardCollection) cockpitPrimitiveLayer.add(commandPostBillboardCollection) |
| | | } |
| | | |
| | | const clearDeviceEntities = () => { |
| | | if (!viewer) return |
| | | deviceEntityIds.forEach(id => { |
| | | viewer.entities.removeById(id) |
| | | }) |
| | | deviceEntityIds.clear() |
| | | if (deviceBillboardCollection) { |
| | | deviceBillboardCollection.removeAll() |
| | | removeCockpitPrimitive(deviceBillboardCollection) |
| | | deviceBillboardCollection = null |
| | | } |
| | | if (deviceRingFillPrimitives.length) { |
| | | deviceRingFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive)) |
| | | deviceRingFillPrimitives = [] |
| | | } |
| | | if (deviceRingOutlinePrimitives.length) { |
| | | deviceRingOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive)) |
| | | deviceRingOutlinePrimitives = [] |
| | | } |
| | | devicePickMap.clear() |
| | | } |
| | | |
| | | const ensureDeviceCollections = () => { |
| | | if (!viewer) return |
| | | ensureCockpitPrimitiveLayer() |
| | | if (!deviceBillboardCollection) { |
| | | deviceBillboardCollection = new Cesium.BillboardCollection() |
| | | addCockpitPrimitive(deviceBillboardCollection) |
| | | } |
| | | } |
| | | |
| | | const clearDefenseZoneEntities = () => { |
| | | if (!defenseZoneSource) return |
| | | defenseZoneSource.entities.removeAll() |
| | | if (!viewer) return |
| | | if (defenseZonePrimitive) { |
| | | removeCockpitPrimitive(defenseZonePrimitive) |
| | | defenseZonePrimitive = null |
| | | } |
| | | if (defenseZoneOutlinePrimitive) { |
| | | removeCockpitPrimitive(defenseZoneOutlinePrimitive) |
| | | defenseZoneOutlinePrimitive = null |
| | | } |
| | | } |
| | | |
| | | const clearPartitionEntities = () => { |
| | | if (!partitionSource) return |
| | | partitionSource.entities.removeAll() |
| | | if (!viewer) return |
| | | if (partitionPrimitive) { |
| | | removeCockpitPrimitive(partitionPrimitive) |
| | | partitionPrimitive = null |
| | | } |
| | | if (partitionOutlinePrimitive) { |
| | | removeCockpitPrimitive(partitionOutlinePrimitive) |
| | | partitionOutlinePrimitive = null |
| | | } |
| | | } |
| | | |
| | | const clearAggregationEntities = () => { |
| | | if (!aggregationSource) return |
| | | aggregationSource.entities.removeAll() |
| | | } |
| | | |
| | | const RING_STYLES = [ |
| | | { inner: 0, outer: 2000, gradient: ['#FF361C', '#360B00'] } |
| | | ] |
| | | const clearCommandPostEntities = () => { |
| | | if (!viewer) return |
| | | if (commandPostBillboardCollection) { |
| | | commandPostBillboardCollection.removeAll() |
| | | removeCockpitPrimitive(commandPostBillboardCollection) |
| | | commandPostBillboardCollection = null |
| | | } |
| | | } |
| | | |
| | | const MATERIAL_TYPE = 'RadialGradientMaterial' |
| | | let materialRegistered = false |
| | | const ensureCommandPostCollection = () => { |
| | | if (!viewer) return |
| | | ensureCockpitPrimitiveLayer() |
| | | if (!commandPostBillboardCollection) { |
| | | commandPostBillboardCollection = new Cesium.BillboardCollection() |
| | | addCockpitPrimitive(commandPostBillboardCollection) |
| | | } |
| | | } |
| | | |
| | | const registerRadialGradientMaterial = () => { |
| | | if (materialRegistered || !Cesium?.Material) return |
| | | materialRegistered = true |
| | | Cesium.Material._materialCache.addMaterial(MATERIAL_TYPE, { |
| | | fabric: { |
| | | type: MATERIAL_TYPE, |
| | | uniforms: { |
| | | color1: new Cesium.Color(1.0, 1.0, 1.0, 1.0), |
| | | color2: new Cesium.Color(0.0, 0.0, 0.0, 1.0), |
| | | }, |
| | | source: ` |
| | | czm_material czm_getMaterial(czm_materialInput materialInput) { |
| | | czm_material material = czm_getDefaultMaterial(materialInput); |
| | | vec2 st = materialInput.st - vec2(0.5); |
| | | float t = clamp(length(st) * 2.0, 0.0, 1.0); |
| | | vec4 color = mix(color1, color2, t); |
| | | material.diffuse = color.rgb; |
| | | material.alpha = color.a; |
| | | return material; |
| | | } |
| | | `, |
| | | }, |
| | | translucent: () => true, |
| | | const setDetailVisibility = visible => { |
| | | detailVisible.value = visible |
| | | if (defenseZonePrimitive) defenseZonePrimitive.show = visible |
| | | if (defenseZoneOutlinePrimitive) defenseZoneOutlinePrimitive.show = visible |
| | | if (partitionPrimitive) partitionPrimitive.show = visible |
| | | if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = visible |
| | | if (commandPostBillboardCollection) commandPostBillboardCollection.show = visible |
| | | if (deviceBillboardCollection) deviceBillboardCollection.show = visible |
| | | if (deviceRingFillPrimitives.length) { |
| | | deviceRingFillPrimitives.forEach(primitive => { |
| | | primitive.show = visible |
| | | }) |
| | | } |
| | | if (deviceRingOutlinePrimitives.length) { |
| | | deviceRingOutlinePrimitives.forEach(primitive => { |
| | | primitive.show = visible |
| | | }) |
| | | } |
| | | if (!visible) closePopup() |
| | | } |
| | | |
| | | const setClusterVisibility = visible => { |
| | | clusterVisible.value = visible |
| | | if (aggregationSource) aggregationSource.show = visible |
| | | } |
| | | |
| | | const ensureCountyCenterMap = () => { |
| | | if (countyCenterMap.size) return |
| | | const geojson = JSON.parse(jaGeojsonRaw) |
| | | geojson.features?.forEach(feature => { |
| | | const name = feature?.properties?.name |
| | | const center = feature?.properties?.centroid || feature?.properties?.center |
| | | if (!name || !Array.isArray(center) || center.length < 2) return |
| | | countyCenterMap.set(name, { longitude: center[0], latitude: center[1] }) |
| | | }) |
| | | } |
| | | |
| | | const buildCirclePositions = (center, radiusMeters, steps = 64) => { |
| | | const positions = [] |
| | | const latRad = Cesium.Math.toRadians(center.latitude) |
| | | const metersToLat = 1 / 111320 |
| | | const metersToLon = 1 / (111320 * Math.cos(latRad)) |
| | | for (let i = 0; i <= steps; i += 1) { |
| | | const angle = Cesium.Math.toRadians((i / steps) * 360) |
| | | const dx = radiusMeters * Math.cos(angle) |
| | | const dy = radiusMeters * Math.sin(angle) |
| | | const lon = center.longitude + dx * metersToLon |
| | | const lat = center.latitude + dy * metersToLat |
| | | positions.push(Cesium.Cartesian3.fromDegrees(lon, lat)) |
| | | } |
| | | return positions |
| | | const buildSimulatedTrackPoints = base => { |
| | | ensureCountyCenterMap() |
| | | const anchor = base || { longitude: 116.397, latitude: 39.908 } |
| | | const offsets = [ |
| | | [0, 0, 120], |
| | | [0.02, 0.012, 150], |
| | | [0.04, 0.006, 180], |
| | | [0.06, -0.008, 200], |
| | | [0.03, -0.02, 160], |
| | | [0.0, -0.015, 140], |
| | | [-0.02, -0.005, 120], |
| | | [-0.01, 0.01, 130], |
| | | ] |
| | | return offsets.map(item => ({ |
| | | longitude: anchor.longitude + item[0], |
| | | latitude: anchor.latitude + item[1], |
| | | height: item[2], |
| | | })) |
| | | } |
| | | |
| | | const addDeviceRings = (center, baseId) => { |
| | | RING_STYLES.forEach((ring, index) => { |
| | | const outerPositions = buildCirclePositions(center, ring.outer) |
| | | const holes = ring.inner |
| | | ? [new Cesium.PolygonHierarchy(buildCirclePositions(center, ring.inner))] |
| | | : [] |
| | | const entityId = `${baseId}-ring-${index}` |
| | | deviceEntityIds.add(entityId) |
| | | registerRadialGradientMaterial() |
| | | const [startColor, endColor] = ring.gradient |
| | | const color1 = Cesium.Color.fromCssColorString(startColor).withAlpha(0.34) |
| | | const color2 = Cesium.Color.fromCssColorString(endColor).withAlpha(0.34) |
| | | const material = new RadialGradientMaterialProperty(color1, color2) |
| | | viewer.entities.add({ |
| | | id: entityId, |
| | | polygon: { |
| | | hierarchy: new Cesium.PolygonHierarchy(outerPositions, holes), |
| | | material, |
| | | |
| | | const clearDroneTrackEntities = () => { |
| | | if (!viewer) return |
| | | stopDroneTrackAnimation() |
| | | if (droneTrackBillboardCollection) { |
| | | if (!droneTrackBillboardCollection.isDestroyed?.()) { |
| | | removeCockpitPrimitive(droneTrackBillboardCollection) |
| | | } |
| | | droneTrackBillboardCollection = null |
| | | } |
| | | if (droneTrackPolylineCollection) { |
| | | if (!droneTrackPolylineCollection.isDestroyed?.()) { |
| | | removeCockpitPrimitive(droneTrackPolylineCollection) |
| | | } |
| | | droneTrackPolylineCollection = null |
| | | } |
| | | droneTrackRuntime = [] |
| | | dronePickMap.clear() |
| | | if (selectedTargetType.value === 'drone') { |
| | | closePopup() |
| | | } |
| | | } |
| | | |
| | | |
| | | const ensureDroneTrackCollections = () => { |
| | | if (!viewer) return |
| | | ensureCockpitPrimitiveLayer() |
| | | if (droneTrackBillboardCollection?.isDestroyed?.()) { |
| | | droneTrackBillboardCollection = null |
| | | } |
| | | if (droneTrackPolylineCollection?.isDestroyed?.()) { |
| | | droneTrackPolylineCollection = null |
| | | } |
| | | if (!droneTrackBillboardCollection) { |
| | | droneTrackBillboardCollection = new Cesium.BillboardCollection() |
| | | addCockpitPrimitive(droneTrackBillboardCollection) |
| | | } |
| | | if (!droneTrackPolylineCollection) { |
| | | droneTrackPolylineCollection = new Cesium.PolylineCollection() |
| | | addCockpitPrimitive(droneTrackPolylineCollection) |
| | | } |
| | | } |
| | | |
| | | const startDroneTrackAnimation = () => { |
| | | if (!viewer) return |
| | | stopDroneTrackAnimation() |
| | | droneTrackStartTime = Cesium.JulianDate.now() |
| | | droneTrackTickHandler = clock => { |
| | | if (!viewer || viewer.isDestroyed?.() || !viewer.scene) { |
| | | stopDroneTrackAnimation() |
| | | 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() |
| | | ) |
| | | track.billboard.position = pos |
| | | }) |
| | | } |
| | | viewer.clock.onTick.addEventListener(droneTrackTickHandler) |
| | | viewer.clock.shouldAnimate = true |
| | | } |
| | | |
| | | const stopDroneTrackAnimation = () => { |
| | | if (!viewer || !droneTrackTickHandler) return |
| | | viewer.clock.onTick.removeEventListener(droneTrackTickHandler) |
| | | droneTrackTickHandler = null |
| | | } |
| | | |
| | | const renderSimulatedDroneTrack = () => { |
| | | if (!viewer) return |
| | | clearDroneTrackEntities() |
| | | ensureCountyCenterMap() |
| | | ensureDroneTrackCollections() |
| | | droneTrackRuntime = [] |
| | | const centers = Array.from(countyCenterMap.entries()) |
| | | const baseCenters = centers.length |
| | | ? centers.slice(0, 4) |
| | | : [['默认区域', { longitude: 116.397, latitude: 39.908 }]] |
| | | 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[0], |
| | | 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, |
| | | }, |
| | | billboard, |
| | | }) |
| | | droneTrackRuntime.push({ |
| | | positions, |
| | | billboard, |
| | | segmentDuration, |
| | | duration: (points.length - 1) * segmentDuration, |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | class RadialGradientMaterialProperty { |
| | | constructor(color1, color2) { |
| | | this._definitionChanged = new Cesium.Event() |
| | | this.color1 = color1 |
| | | this.color2 = color2 |
| | | } |
| | | |
| | | get isConstant() { |
| | | return true |
| | | } |
| | | |
| | | get definitionChanged() { |
| | | return this._definitionChanged |
| | | } |
| | | |
| | | getType() { |
| | | return MATERIAL_TYPE |
| | | } |
| | | |
| | | getValue(time, result) { |
| | | const target = result || {} |
| | | target.color1 = this.color1 |
| | | target.color2 = this.color2 |
| | | return target |
| | | } |
| | | |
| | | equals(other) { |
| | | return ( |
| | | other instanceof RadialGradientMaterialProperty && |
| | | Cesium.Color.equals(this.color1, other.color1) && |
| | | Cesium.Color.equals(this.color2, other.color2) |
| | | ) |
| | | } |
| | | viewer.clock.multiplier = 1 |
| | | startDroneTrackAnimation() |
| | | reorderCockpitPrimitives() |
| | | } |
| | | |
| | | const renderDeviceEntities = devices => { |
| | | if (!viewer) return |
| | | clearDeviceEntities() |
| | | ensureCockpitPrimitiveLayer() |
| | | ensureDeviceCollections() |
| | | deviceBillboardCollection.removeAll() |
| | | if (deviceRingFillPrimitives.length) { |
| | | deviceRingFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive)) |
| | | deviceRingFillPrimitives = [] |
| | | } |
| | | if (deviceRingOutlinePrimitives.length) { |
| | | deviceRingOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive)) |
| | | deviceRingOutlinePrimitives = [] |
| | | } |
| | | devicePickMap.clear() |
| | | deviceBillboardCollection.show = detailVisible.value |
| | | const ringFillInstancesByStyle = RING_STYLES.map(() => []) |
| | | const ringOutlineInstancesByStyle = RING_STYLES.map(() => []) |
| | | devices.forEach((item, index) => { |
| | | const position = getDevicePosition(item) |
| | | if (!position) return |
| | | const entityId = `online-device-${item.id ?? index}` |
| | | deviceEntityIds.add(entityId) |
| | | addDeviceRings(position, entityId) |
| | | viewer.entities.add({ |
| | | id: entityId, |
| | | const entityId = `online-device-${item.id ?? index}-${index}` |
| | | addDeviceRings(position, ringFillInstancesByStyle, ringOutlineInstancesByStyle) |
| | | const billboard = deviceBillboardCollection.add({ |
| | | position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0), |
| | | billboard: { |
| | | image: equipmentIcon, |
| | | width: 40.34, |
| | | height: 40.34, |
| | | }, |
| | | image: equipmentIcon, |
| | | width: 40, |
| | | height: 56, |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, |
| | | }) |
| | | billboard.id = entityId |
| | | devicePickMap.set(entityId, { data: item, billboard }) |
| | | }) |
| | | if (selectedTargetType.value === 'device' && selectedDeviceBillboard && !devicePickMap.has(selectedDeviceBillboard.id)) { |
| | | closePopup() |
| | | } |
| | | ringFillInstancesByStyle.forEach((instances, index) => { |
| | | if (!instances.length) return |
| | | const ring = RING_STYLES[index] |
| | | const material = createRadialGradientMaterial( |
| | | Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(0.64), |
| | | Cesium.Color.fromCssColorString(ring.gradient[1]).withAlpha(0.64) |
| | | ) |
| | | const primitive = new Cesium.GroundPrimitive({ |
| | | geometryInstances: instances, |
| | | appearance: new Cesium.MaterialAppearance({ |
| | | material, |
| | | translucent: true, |
| | | }), |
| | | }) |
| | | primitive.show = detailVisible.value |
| | | addCockpitPrimitive(primitive) |
| | | deviceRingFillPrimitives.push(primitive) |
| | | }) |
| | | ringOutlineInstancesByStyle.forEach(instances => { |
| | | if (!instances.length) return |
| | | const primitive = new Cesium.GroundPolylinePrimitive({ |
| | | geometryInstances: instances, |
| | | appearance: new Cesium.PolylineColorAppearance(), |
| | | }) |
| | | primitive.show = detailVisible.value |
| | | addCockpitPrimitive(primitive) |
| | | deviceRingOutlinePrimitives.push(primitive) |
| | | }) |
| | | reorderCockpitPrimitives() |
| | | } |
| | | |
| | | const getDefenseZonePositions = geom => { |
| | |
| | | return list.map(item => Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude)) |
| | | } |
| | | |
| | | const renderZonePolygons = ({ zones, source, idPrefix, lineColor, fillGradient }) => { |
| | | if (!viewer || !source) return |
| | | registerRadialGradientMaterial() |
| | | const borderColor = Cesium.Color.fromCssColorString(lineColor) |
| | | const fillColorStart = Cesium.Color.fromCssColorString(fillGradient[0]).withAlpha(0.34) |
| | | const fillColorEnd = Cesium.Color.fromCssColorString(fillGradient[1]).withAlpha(0.34) |
| | | const fillMaterial = new RadialGradientMaterialProperty(fillColorStart, fillColorEnd) |
| | | zones.forEach((zone, index) => { |
| | | const buildZonePrimitives = (zones, lineColor, fillColor1, fillColor2) => { |
| | | if (!viewer) return { primitive: null, outlinePrimitive: null } |
| | | const polygonInstances = [] |
| | | const lineInstances = [] |
| | | const texturedVertexFormat = getTexturedVertexFormat() |
| | | ; (zones || []).forEach((zone, index) => { |
| | | if (!zone?.geom) return |
| | | const positions = getDefenseZonePositions(zone.geom) |
| | | if (!positions.length) return |
| | | const entityId = `${idPrefix}-${zone.id ?? index}` |
| | | const linePositions = positions.length > 1 ? [...positions, positions[0]] : positions |
| | | source.entities.add({ |
| | | id: entityId, |
| | | polygon: { |
| | | hierarchy: new Cesium.PolygonHierarchy(positions), |
| | | material: fillMaterial, |
| | | outline: true, |
| | | outlineColor: borderColor, |
| | | heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, |
| | | }, |
| | | polyline: { |
| | | positions: linePositions, |
| | | clampToGround: true, |
| | | width: 2, |
| | | material: borderColor, |
| | | }, |
| | | const polygon = new Cesium.PolygonGeometry({ |
| | | polygonHierarchy: new Cesium.PolygonHierarchy(positions), |
| | | vertexFormat: texturedVertexFormat, |
| | | }) |
| | | polygonInstances.push( |
| | | new Cesium.GeometryInstance({ |
| | | geometry: polygon, |
| | | }) |
| | | ) |
| | | const linePositions = positions.length > 1 ? [...positions, positions[0]] : positions |
| | | lineInstances.push( |
| | | new Cesium.GeometryInstance({ |
| | | geometry: new Cesium.GroundPolylineGeometry({ |
| | | positions: linePositions, |
| | | width: 2, |
| | | }), |
| | | attributes: { |
| | | color: Cesium.ColorGeometryInstanceAttribute.fromColor( |
| | | Cesium.Color.fromCssColorString(lineColor) |
| | | ), |
| | | }, |
| | | }) |
| | | ) |
| | | }) |
| | | let primitive = null |
| | | if (polygonInstances.length) { |
| | | const baseColor1 = Cesium.Color.fromCssColorString(fillColor1) |
| | | const baseColor2 = Cesium.Color.fromCssColorString(fillColor2) |
| | | const material = createRadialGradientMaterial(baseColor1.withAlpha(0.64), baseColor2.withAlpha(0.64)) |
| | | primitive = new Cesium.GroundPrimitive({ |
| | | geometryInstances: polygonInstances, |
| | | appearance: new Cesium.MaterialAppearance({ |
| | | material, |
| | | translucent: true, |
| | | }), |
| | | }) |
| | | addCockpitPrimitive(primitive) |
| | | } |
| | | let outlinePrimitive = null |
| | | if (lineInstances.length) { |
| | | outlinePrimitive = new Cesium.GroundPolylinePrimitive({ |
| | | geometryInstances: lineInstances, |
| | | appearance: new Cesium.PolylineColorAppearance(), |
| | | }) |
| | | addCockpitPrimitive(outlinePrimitive) |
| | | } |
| | | return { primitive, outlinePrimitive } |
| | | } |
| | | |
| | | const renderDefenseZones = zones => { |
| | | if (!viewer) return |
| | | if (!defenseZoneSource) { |
| | | defenseZoneSource = new Cesium.CustomDataSource('defenseZoneSource') |
| | | viewer.dataSources.add(defenseZoneSource) |
| | | } |
| | | clearDefenseZoneEntities() |
| | | renderZonePolygons({ |
| | | zones, |
| | | source: defenseZoneSource, |
| | | idPrefix: 'defense-zone', |
| | | lineColor: '#19D266', |
| | | fillGradient: ['#2AEDBF', '#012B11'], |
| | | }) |
| | | const result = buildZonePrimitives(zones, '#19D266', '#2AEDBF', '#012B11') |
| | | defenseZonePrimitive = result.primitive |
| | | defenseZoneOutlinePrimitive = result.outlinePrimitive |
| | | if (defenseZonePrimitive) defenseZonePrimitive.show = detailVisible.value |
| | | if (defenseZoneOutlinePrimitive) defenseZoneOutlinePrimitive.show = detailVisible.value |
| | | reorderCockpitPrimitives() |
| | | } |
| | | |
| | | const renderPartitions = zones => { |
| | | if (!viewer) return |
| | | if (!partitionSource) { |
| | | partitionSource = new Cesium.CustomDataSource('partitionSource') |
| | | viewer.dataSources.add(partitionSource) |
| | | } |
| | | clearPartitionEntities() |
| | | renderZonePolygons({ |
| | | zones, |
| | | source: partitionSource, |
| | | idPrefix: 'partition-zone', |
| | | lineColor: '#FFCD2A', |
| | | fillGradient: ['#FFC609', '#583300'], |
| | | }) |
| | | const result = buildZonePrimitives(zones, '#FFCD2A', '#FFC609', '#583300') |
| | | partitionPrimitive = result.primitive |
| | | partitionOutlinePrimitive = result.outlinePrimitive |
| | | if (partitionPrimitive) partitionPrimitive.show = detailVisible.value |
| | | if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = detailVisible.value |
| | | reorderCockpitPrimitives() |
| | | } |
| | | |
| | | const loadDefenseZones = async () => { |
| | |
| | | renderPartitions(res?.data?.data?.records ?? []) |
| | | } catch (error) { |
| | | renderPartitions([]) |
| | | } |
| | | } |
| | | |
| | | const renderAggregation = list => { |
| | | if (!viewer) return |
| | | ensureCountyCenterMap() |
| | | if (!aggregationSource) { |
| | | aggregationSource = new Cesium.CustomDataSource('aggregationSource') |
| | | viewer.dataSources.add(aggregationSource) |
| | | } |
| | | clearAggregationEntities() |
| | | aggregationSource.show = clusterVisible.value |
| | | const countMap = new Map() |
| | | ; (list || []).forEach(item => { |
| | | if (!item?.type) return |
| | | countMap.set(item.type, Number(item.count ?? 0)) |
| | | }) |
| | | Array.from(countyCenterMap.entries()).forEach(([name, center], index) => { |
| | | const position = Cesium.Cartesian3.fromDegrees(center.longitude, center.latitude, 0) |
| | | const count = countMap.get(name) ?? 0 |
| | | aggregationSource.entities.add({ |
| | | id: `aggregation-${name}-${index}`, |
| | | position, |
| | | billboard: { |
| | | image: aggregationIcon, |
| | | width: 66.73, |
| | | height: 43, |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, |
| | | heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, |
| | | }, |
| | | label: { |
| | | text: `${count}`, |
| | | fillColor: Cesium.Color.WHITE, |
| | | style: Cesium.LabelStyle.FILL_AND_OUTLINE, |
| | | verticalOrigin: Cesium.VerticalOrigin.CENTER, |
| | | horizontalOrigin: Cesium.HorizontalOrigin.CENTER, |
| | | font: '11pt Source Han Sans CN', |
| | | eyeOffset: new Cesium.Cartesian3(0, 0, -20), // 让label "浮" 在广告牌前面 |
| | | |
| | | pixelOffset: new Cesium.Cartesian2(0, -35), |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, |
| | | }, |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | const getPickedTarget = picks => { |
| | | for (const pick of picks) { |
| | | const pickId = pick?.id |
| | | const resolvedId = typeof pickId === 'string' ? pickId : pickId?.id |
| | | if (resolvedId && devicePickMap.has(resolvedId)) { |
| | | return { type: 'device', ...devicePickMap.get(resolvedId) } |
| | | } |
| | | if (resolvedId && dronePickMap.has(resolvedId)) { |
| | | return { type: 'drone', ...dronePickMap.get(resolvedId) } |
| | | } |
| | | } |
| | | return null |
| | | } |
| | | |
| | | const updatePopupPosition = () => { |
| | | if (!viewer || !selectedDeviceBillboard) return |
| | | const cartesian = selectedDeviceBillboard.position |
| | | if (!cartesian) return |
| | | const screenPosition = viewer.scene.cartesianToCanvasCoordinates(cartesian) |
| | | if (!screenPosition) return |
| | | popupPosition.value = { x: screenPosition.x, y: screenPosition.y } |
| | | } |
| | | |
| | | const startPopupRender = () => { |
| | | if (!viewer || popupRenderHandler) return |
| | | popupRenderHandler = () => updatePopupPosition() |
| | | viewer.scene.postRender.addEventListener(popupRenderHandler) |
| | | updatePopupPosition() |
| | | } |
| | | |
| | | const stopPopupRender = () => { |
| | | if (!viewer || !popupRenderHandler) return |
| | | viewer.scene.postRender.removeEventListener(popupRenderHandler) |
| | | popupRenderHandler = null |
| | | } |
| | | |
| | | const handleDeviceClick = movement => { |
| | | if (!viewer) return |
| | | const picks = viewer.scene.drillPick(movement.position) || [] |
| | | const pickedTarget = getPickedTarget(picks) |
| | | if (!pickedTarget) { |
| | | closePopup() |
| | | return |
| | | } |
| | | selectedTargetType.value = pickedTarget.type |
| | | selectedDevice.value = pickedTarget.data |
| | | selectedDeviceBillboard = pickedTarget.billboard |
| | | startPopupRender() |
| | | } |
| | | |
| | | const initDeviceClickHandler = () => { |
| | | if (deviceClickHandler || !viewer) return |
| | | deviceClickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas) |
| | | deviceClickHandler.setInputAction(handleDeviceClick, Cesium.ScreenSpaceEventType.LEFT_CLICK) |
| | | } |
| | | |
| | | const destroyDeviceClickHandler = () => { |
| | | deviceClickHandler?.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK) |
| | | deviceClickHandler?.destroy() |
| | | deviceClickHandler = null |
| | | } |
| | | |
| | | function closePopup () { |
| | | selectedDevice.value = null |
| | | selectedDeviceBillboard = null |
| | | selectedTargetType.value = 'device' |
| | | stopPopupRender() |
| | | } |
| | | |
| | | const popupPosition = ref({ x: 0, y: 0 }) |
| | | const popupVisible = computed(() => Boolean(selectedDevice.value)) |
| | | const isDronePopup = computed(() => selectedTargetType.value === 'drone') |
| | | const toggleDroneFavorite = () => { |
| | | if (!selectedDevice.value) return |
| | | selectedDevice.value.isFavorite = !selectedDevice.value.isFavorite |
| | | } |
| | | const handleDroneSignal = () => emit('droneSignal', selectedDevice.value) |
| | | const handleDroneCounter = () => emit('droneCounter', selectedDevice.value) |
| | | |
| | | const renderCommandPosts = list => { |
| | | if (!viewer) return |
| | | ensureCommandPostCollection() |
| | | commandPostBillboardCollection.removeAll() |
| | | commandPostBillboardCollection.show = detailVisible.value |
| | | ; (list || []).forEach((item, index) => { |
| | | const position = getDevicePosition(item) |
| | | if (!position) return |
| | | commandPostBillboardCollection.add({ |
| | | position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0), |
| | | image: commandPostIcon, |
| | | width: 40, |
| | | height: 56, |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, |
| | | }) |
| | | }) |
| | | reorderCockpitPrimitives() |
| | | } |
| | | |
| | | const loadAggregation = async () => { |
| | | try { |
| | | const res = await cockpitAggregationApi() |
| | | renderAggregation(res?.data?.data ?? []) |
| | | } catch (error) { |
| | | renderAggregation([]) |
| | | } |
| | | } |
| | | |
| | | const loadCommandPosts = async () => { |
| | | try { |
| | | const res = await fwDefenseSceneListApi() |
| | | renderCommandPosts(res?.data?.data ?? []) |
| | | } catch (error) { |
| | | renderCommandPosts([]) |
| | | } |
| | | } |
| | | |
| | |
| | | showLayerPanel.value = !showLayerPanel.value |
| | | } |
| | | |
| | | const updateStageDisplay = stage => { |
| | | const showCluster = stage === 'cluster' |
| | | setClusterVisibility(showCluster) |
| | | setDetailVisibility(!showCluster) |
| | | } |
| | | |
| | | const handleMapReady = ({ viewer: mapViewer }) => { |
| | | viewer = mapViewer |
| | | ensureCockpitPrimitiveLayer() |
| | | const height = viewer?.camera?.positionCartographic?.height |
| | | const stage = height != null && height >= 100000 ? 'cluster' : 'detail' |
| | | updateStageDisplay(stage) |
| | | renderDeviceEntities(props.onlineDevices) |
| | | loadDefenseZones() |
| | | loadPartitions() |
| | | loadAggregation() |
| | | loadCommandPosts() |
| | | renderSimulatedDroneTrack() |
| | | initDeviceClickHandler() |
| | | } |
| | | |
| | | const handleStageChange = stage => { |
| | | updateStageDisplay(stage) |
| | | } |
| | | |
| | | const handleClickOutside = event => { |
| | |
| | | clearDeviceEntities() |
| | | clearDefenseZoneEntities() |
| | | clearPartitionEntities() |
| | | clearAggregationEntities() |
| | | clearCommandPostEntities() |
| | | clearDroneTrackEntities() |
| | | closePopup() |
| | | destroyDeviceClickHandler() |
| | | if (cockpitPrimitiveLayer && viewer?.scene?.primitives) { |
| | | viewer.scene.primitives.remove(cockpitPrimitiveLayer) |
| | | } |
| | | cockpitPrimitiveLayer = null |
| | | viewer = null |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .map-shell { |
| | | position: relative; |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .map-container { |
| | | position: absolute; |
| | | top: 0; |
| | |
| | | border-color: #a1a3d4; |
| | | } |
| | | </style> |
| | | |