| | |
| | | :show-admin-boundary="true" :zoom-to-boundary="true" :enable-stage-emit="true" |
| | | :cluster-height="CLUSTER_HEIGHT" :detail-height="DETAIL_HEIGHT" @ready="handleMapReady" |
| | | @stage-change="handleStageChange" /> |
| | | <div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.leftCollapsed }"> |
| | | <div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.rightCollapsed }"> |
| | | <div class="layer-control-wrap" ref="layerWrapRef"> |
| | | <div class="layer-control" @click="toggleLayerPanel"> |
| | | <div class="layer-control" @click.stop="toggleLayerPanel"> |
| | | <img :src="layerControlIcon" alt="图层控制" /> |
| | | </div> |
| | | <div v-if="showLayerPanel" class="layer-panel"> |
| | | <div class="panel-title">图层管理</div> |
| | | |
| | | <div class="panel-content"> |
| | | <el-tree class="command-tree" :data="layerTree" show-checkbox default-expand-all node-key="key" |
| | | :props="layerTreeProps" :default-checked-keys="defaultCheckedKeys" /> |
| | | <transition name="layer-panel-slide"> |
| | | <div v-if="showLayerPanel" class="layer-panel"> |
| | | <div class="panel-title">图层管理</div> |
| | | <div class="panel-content"> |
| | | <el-tree ref="layerTreeRef" class="command-tree map-layer-tree" :data="layerTree" |
| | | icon="el-icon-arrow-down-bold" |
| | | show-checkbox default-expand-all node-key="key" :props="layerTreeProps" |
| | | :default-checked-keys="treeCheckedKeys" @check="handleLayerCheck" /> |
| | | </div> |
| | | </div> |
| | | </transition> |
| | | </div> |
| | | </div> |
| | | <div v-if="props.showLayerControl" class="base-layer-switch-root" :class="{ collapsed: props.rightCollapsed }"> |
| | | <div class="base-layer-switch-wrap" @mouseenter="showBaseLayerPanel = true" |
| | | @mouseleave="showBaseLayerPanel = false"> |
| | | <div class="base-layer-trigger"> |
| | | <img class="base-layer-trigger-thumb" :src="currentBaseLayer.icon" :alt="currentBaseLayer.label"> |
| | | <div class="base-layer-trigger-label">{{ currentBaseLayer.shortLabel }}</div> |
| | | </div> |
| | | <transition name="layer-panel-slide"> |
| | | <div v-if="showBaseLayerPanel" class="base-layer-panel"> |
| | | <div class="base-map-options"> |
| | | <div class="base-map-card" :class="{ active: baseLayerKey === 'base-satellite' }" |
| | | @click="handleBaseLayerSelect('base-satellite')"> |
| | | <img class="base-map-thumb" :src="yxIcon" alt=""> |
| | | <div class="base-map-label">卫星地图</div> |
| | | </div> |
| | | <div class="base-map-card" :class="{ active: baseLayerKey === 'base-standard' }" |
| | | @click="handleBaseLayerSelect('base-standard')"> |
| | | <img class="base-map-thumb" :src="dzIcon" alt=""> |
| | | <div class="base-map-label">标准地图</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </transition> |
| | | </div> |
| | | </div> |
| | | |
| | | <DevicePopup |
| | | :visible="popupVisible && !isDronePopup" |
| | | :position="popupPosition" |
| | | :device="selectedDevice" |
| | | @close="closePopup" |
| | | /> |
| | | <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" |
| | | /> |
| | | <DronePopup :visible="popupVisible && isDronePopup" :position="popupPosition" :drone="selectedDevice" |
| | | :favorite="Boolean(selectedDevice?.isFavorite)" @close="closePopup" @toggle-favorite="handleDroneFavorite" |
| | | @signal="handleDroneSignal" @counter="handleDroneCounter" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | |
| | | import * as Cesium from 'cesium' |
| | | import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue' |
| | | 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 { buildEllipsePositions } from '@/utils/cesium/shapeTools' |
| | | import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants' |
| | | |
| | | import { newAreaDivideList, newDefenseSceneManageList } from '@/api/dataCockpit/index' |
| | | |
| | | import { newCockpitAggregationApi } from '@/api/dataCockpit' |
| | | import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png' |
| | | import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png' |
| | | import offlineEquipmentIcon from '@/assets/images/dataCockpit/map/offline-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 dayjs from 'dayjs' |
| | | import { |
| | | RING_STYLES, |
| | | addDeviceRings, |
| | | createDroneTrackMaterial, |
| | | createRadialGradientMaterial, |
| | | getTexturedVertexFormat, |
| | | } from './device-map-materials' |
| | | import { getPointPositionsHeight } from '@/utils/cesium/mapUtil' |
| | | import { createDeviceRangePrimitiveWithHeight } from '@/utils/cesium/deviceRange' |
| | | import dzIcon from '@/assets/images/dataCockpit/map/dz-map-layer.png' |
| | | import yxIcon from '@/assets/images/dataCockpit/map/yx-map-layer.png' |
| | | |
| | | const CLUSTER_HEIGHT = 100000 |
| | | const DETAIL_HEIGHT = 10000 |
| | | const POLYGON_HEIGHT_M = 0.2 |
| | | const DRONE_TRACK_DURATION_S = 30 * 60 |
| | | const FIXED_DRONE_TRACK_IDS = ['6000000000609', '6000000000608'] |
| | | |
| | | const props = defineProps({ |
| | | onlineDevices: { |
| | | allDevices: { |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | leftCollapsed: { |
| | | alarmDrones: { |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | rightCollapsed: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | |
| | | default: true, |
| | | }, |
| | | }) |
| | | const emit = defineEmits(['droneSignal', 'droneCounter']) |
| | | const emit = defineEmits(['droneSignal', 'droneCounter', 'droneFavorite']) |
| | | |
| | | const DEFAULT_ZONE_PAGE_SIZE = 999 |
| | | |
| | | const mapRef = ref(null) |
| | | let viewer = null |
| | | let publicCesium = 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 partitionFillPrimitives = [] |
| | | let partitionOutlinePrimitives = [] |
| | | let aggregationSource = null |
| | | let commandPostBillboardCollection = null |
| | | 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 |
| | | let favoritePulseElapsed = 0 |
| | | const favoritePulseEntities = [] |
| | | const pulseBaseColor = Cesium.Color.fromCssColorString('#FF3B30') |
| | | const PULSE_MIN_RADIUS_M = 20 |
| | | const PULSE_MAX_RADIUS_M = 80 |
| | | const PULSE_DURATION_S = 1.4 |
| | | const detailVisible = ref(true) |
| | | const clusterVisible = ref(false) |
| | | const countyCenterMap = new Map() |
| | | const showLayerPanel = ref(false) |
| | | const showBaseLayerPanel = ref(false) |
| | | const layerWrapRef = ref(null) |
| | | const layerTreeRef = ref(null) |
| | | const selectedDevice = ref(null) |
| | | const selectedTargetType = ref('device') |
| | | let selectedDeviceBillboard = null |
| | |
| | | label: 'label', |
| | | children: 'children', |
| | | } |
| | | const defaultCheckedKeys = ['global', 'city-base'] |
| | | const baseLayerKeys = ['base-standard', 'base-satellite'] |
| | | const defaultCheckedKeys = ['ja-terrain', 'admin', 'city-base'] |
| | | const baseLayerKey = ref('base-satellite') |
| | | const baseLayerMeta = { |
| | | 'base-standard': { icon: dzIcon, label: '标准地图', shortLabel: '标准' }, |
| | | 'base-satellite': { icon: yxIcon, label: '卫星地图', shortLabel: '影像' }, |
| | | } |
| | | const currentBaseLayer = computed(() => baseLayerMeta[baseLayerKey.value] || baseLayerMeta['base-satellite']) |
| | | const treeCheckedKeys = ref([...defaultCheckedKeys]) |
| | | const layerTree = ref([ |
| | | { |
| | | key: 'base', |
| | | label: '地理信息图层', |
| | | children: [ |
| | | { key: 'global', label: '全球地形' }, |
| | | { key: 'ja-terrain', label: '吉安地形' }, |
| | | { key: 'admin', label: '行政区划' }, |
| | | ], |
| | | }, |
| | | { |
| | | |
| | | ]) |
| | | |
| | | /** |
| | | * { |
| | | key: 'city', |
| | | label: '城市CIM图层', |
| | | children: [ |
| | |
| | | { key: 'route', label: '飞行航路' }, |
| | | ], |
| | | }, |
| | | ]) |
| | | */ |
| | | |
| | | 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 pulse = { |
| | | offset: Math.random() * PULSE_DURATION_S, |
| | | entity: null, |
| | | } |
| | | pulse.entity = viewer.entities.add({ |
| | | position: new Cesium.CallbackProperty(() => getBillboardPosition(billboard), false), |
| | | point: { |
| | | pixelSize: new Cesium.CallbackProperty(() => getPulseRadius(pulse), false), |
| | | color: pulseBaseColor.withAlpha(0.4), |
| | | }, |
| | | }) |
| | | 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 longitudeRaw = item.longitude ?? item.lng ?? item.lon |
| | |
| | | if (!Number.isFinite(range) || range <= 0) return null |
| | | return range |
| | | } |
| | | |
| | | |
| | | const ensureCockpitPrimitiveLayer = () => { |
| | | if (!viewer) return |
| | |
| | | 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 (partitionFillPrimitives.length) { |
| | | partitionFillPrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive)) |
| | | } |
| | | if (partitionOutlinePrimitives.length) { |
| | | partitionOutlinePrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive)) |
| | | } |
| | | if (deviceRingFillPrimitives.length) { |
| | | deviceRingFillPrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive)) |
| | | } |
| | | if (deviceRingOutlinePrimitives.length) { |
| | | deviceRingOutlinePrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive)) |
| | | } |
| | | if (commandPostBillboardCollection) cockpitPrimitiveLayer.add(commandPostBillboardCollection) |
| | | if (deviceBillboardCollection) cockpitPrimitiveLayer.add(deviceBillboardCollection) |
| | | if (droneTrackPolylineCollection) cockpitPrimitiveLayer.add(droneTrackPolylineCollection) |
| | | if (droneTrackBillboardCollection) cockpitPrimitiveLayer.add(droneTrackBillboardCollection) |
| | | if (deviceBillboardCollection) cockpitPrimitiveLayer.add(deviceBillboardCollection) |
| | | if (commandPostBillboardCollection) cockpitPrimitiveLayer.add(commandPostBillboardCollection) |
| | | } |
| | | |
| | | const clearDeviceEntities = () => { |
| | |
| | | } |
| | | } |
| | | |
| | | const clearDefenseZoneEntities = () => { |
| | | if (!viewer) return |
| | | if (defenseZonePrimitive) { |
| | | removeCockpitPrimitive(defenseZonePrimitive) |
| | | defenseZonePrimitive = null |
| | | } |
| | | if (defenseZoneOutlinePrimitive) { |
| | | removeCockpitPrimitive(defenseZoneOutlinePrimitive) |
| | | defenseZoneOutlinePrimitive = null |
| | | } |
| | | } |
| | | |
| | | const clearPartitionEntities = () => { |
| | | if (!viewer) return |
| | | if (partitionPrimitive) { |
| | | removeCockpitPrimitive(partitionPrimitive) |
| | | partitionPrimitive = null |
| | | if (partitionFillPrimitives.length) { |
| | | partitionFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive)) |
| | | partitionFillPrimitives = [] |
| | | } |
| | | if (partitionOutlinePrimitive) { |
| | | removeCockpitPrimitive(partitionOutlinePrimitive) |
| | | partitionOutlinePrimitive = null |
| | | if (partitionOutlinePrimitives.length) { |
| | | partitionOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive)) |
| | | partitionOutlinePrimitives = [] |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | 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 |
| | | if (defenseZoneOutlinePrimitive) defenseZoneOutlinePrimitive.show = visible |
| | | if (partitionPrimitive) partitionPrimitive.show = visible |
| | | if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = visible |
| | | if (partitionFillPrimitives.length) { |
| | | partitionFillPrimitives.forEach(primitive => { |
| | | primitive.show = visible |
| | | }) |
| | | } |
| | | if (partitionOutlinePrimitives.length) { |
| | | partitionOutlinePrimitives.forEach(primitive => { |
| | | primitive.show = visible |
| | | }) |
| | | } |
| | | if (commandPostBillboardCollection) commandPostBillboardCollection.show = visible |
| | | if (deviceBillboardCollection) deviceBillboardCollection.show = visible |
| | | if (deviceRingFillPrimitives.length) { |
| | |
| | | |
| | | const setClusterVisibility = visible => { |
| | | clusterVisible.value = visible |
| | | if (aggregationSource) aggregationSource.show = visible |
| | | updateAggregationVisibility() |
| | | } |
| | | |
| | | const updateAggregationVisibility = () => { |
| | | if (!aggregationSource) return |
| | | aggregationSource.show = clusterVisible.value && adminBoundaryVisible.value |
| | | } |
| | | |
| | | 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 |
| | | }) |
| | | } |
| | | 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.929475, |
| | | latitude: 27.136575, |
| | | height: 200 |
| | | }, |
| | | { |
| | | longitude: 114.928466, |
| | | latitude: 27.134556, |
| | | height: 200 |
| | | }, |
| | | { |
| | | longitude: 114.928466, |
| | | latitude: 27.134556, |
| | | height: 320 |
| | | }, |
| | | { |
| | | longitude: 114.928474, |
| | | latitude: 27.134397, |
| | | height: 320 |
| | | }, |
| | | { |
| | | longitude: 114.927932, |
| | | latitude: 27.133385, |
| | | height: 320 |
| | | }, |
| | | { |
| | | longitude: 114.927365, |
| | | latitude: 27.132097, |
| | | height: 320 |
| | | }, |
| | | { |
| | | longitude: 114.929818, |
| | | latitude: 27.131645, |
| | | height: 320 |
| | | }, |
| | | { |
| | | longitude: 114.924644, |
| | | latitude: 27.136622, |
| | | height: 120 |
| | | }, |
| | | ], |
| | | |
| | | [ |
| | | { |
| | | longitude: 114.936386, |
| | | latitude: 26.825890, |
| | | height: 120 |
| | | }, |
| | | { |
| | | longitude: 114.933976, |
| | | latitude: 26.827145, |
| | | height: 120 |
| | | }, |
| | | { |
| | | longitude: 114.931281, |
| | | latitude: 26.827801, |
| | | height: 120 |
| | | }, |
| | | { |
| | | longitude: 114.931281, |
| | | latitude: 26.827801, |
| | | height: 400 |
| | | }, |
| | | { |
| | | longitude: 114.931159, |
| | | latitude: 26.826940, |
| | | height: 400 |
| | | }, |
| | | { |
| | | longitude: 114.933707, |
| | | latitude: 26.826704, |
| | | height: 400 |
| | | }, |
| | | { |
| | | longitude: 114.935197, |
| | | latitude: 26.826933, |
| | | height: 400 |
| | | }, |
| | | { |
| | | longitude: 114.936371, |
| | | latitude: 26.828504, |
| | | height: 400 |
| | | }, |
| | | ], |
| | | ] |
| | | |
| | | return basePoints[center.trackIndex] || basePoints[1] |
| | | } |
| | | |
| | | |
| | | const clearDroneTrackEntities = () => { |
| | | if (!viewer) return |
| | | stopDroneTrackAnimation() |
| | | stopFavoritePulseAnimation() |
| | | if (droneTrackBillboardCollection) { |
| | | if (!droneTrackBillboardCollection.isDestroyed?.()) { |
| | | removeCockpitPrimitive(droneTrackBillboardCollection) |
| | |
| | | } |
| | | droneTrackPolylineCollection = null |
| | | } |
| | | if (droneTrackSource) { |
| | | viewer.dataSources.remove(droneTrackSource) |
| | | droneTrackSource = 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() |
| | |
| | | } |
| | | } |
| | | |
| | | const startDroneTrackAnimation = () => { |
| | | const ensureDroneTrackSource = () => { |
| | | 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( |
| | | if (!droneTrackSource) { |
| | | droneTrackSource = new Cesium.CustomDataSource('droneTrackSource') |
| | | viewer.dataSources.add(droneTrackSource) |
| | | } |
| | | } |
| | | |
| | | const setupDroneTrackClock = () => { |
| | | if (!viewer) return |
| | | const now = new Date() |
| | | const noon = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 12, 0, 0, 0) |
| | | const startTime = Cesium.JulianDate.fromDate(noon) |
| | | 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 |
| | | }) |
| | | } |
| | | viewer.clock.onTick.addEventListener(droneTrackTickHandler) |
| | | } |
| | | 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 = viewer.clock.startTime |
| | | viewer.clock.shouldAnimate = true |
| | | droneTrackAnimStartAt = performance.now() |
| | | droneTrackLastTickAt = droneTrackAnimStartAt |
| | | const renderTick = now => { |
| | | if (!viewer || viewer.isDestroyed?.()) { |
| | | droneTrackRafId = null |
| | | return |
| | | } |
| | | 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() |
| | | ) |
| | | 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) |
| | | } |
| | | droneTrackRafId = requestAnimationFrame(renderTick) |
| | | } |
| | | |
| | | const stopDroneTrackAnimation = () => { |
| | | if (!viewer || !droneTrackTickHandler) return |
| | | viewer.clock.onTick.removeEventListener(droneTrackTickHandler) |
| | | droneTrackTickHandler = null |
| | | if (droneTrackRafId) { |
| | | cancelAnimationFrame(droneTrackRafId) |
| | | droneTrackRafId = 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() |
| | | ensureDroneTrackSource() |
| | | setupDroneTrackClock() |
| | | 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) |
| | | ; (FIXED_DRONE_TRACK_IDS || []).forEach((trackId, trackIndex) => { |
| | | const item = (list || []).find(entry => String(entry?.id) === String(trackId)) |
| | | if (!item) return |
| | | 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) |
| | | ) |
| | | const segmentDuration = DRONE_TRACK_DURATION_S / Math.max(positions.length - 1, 1) |
| | | 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 }) |
| | | } |
| | | const polyline = droneTrackPolylineCollection.add({ |
| | | positions: [positions[0]], |
| | | width: 3, |
| | | material: trackMaterial, |
| | | }) |
| | | const droneId = `drone-alarm-${trackId}` |
| | | 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) |
| | | }) |
| | | 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: { |
| | | 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, |
| | | 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, |
| | | polyline, |
| | | billboard: entity, |
| | | segmentDuration, |
| | | duration: DRONE_TRACK_DURATION_S, |
| | | }) |
| | | }) |
| | | droneTrackRuntime.push({ |
| | | positions, |
| | | billboard, |
| | | segmentDuration, |
| | | duration: (points.length - 1) * segmentDuration, |
| | | }) |
| | | }) |
| | | viewer.clock.multiplier = 1 |
| | | startDroneTrackAnimation() |
| | | startFavoritePulseAnimation() |
| | | reorderCockpitPrimitives() |
| | | } |
| | | |
| | | const renderDeviceEntities = devices => { |
| | | const renderDeviceEntities = async devices => { |
| | | if (!viewer) return |
| | | ensureCockpitPrimitiveLayer() |
| | | ensureDeviceCollections() |
| | |
| | | } |
| | | devicePickMap.clear() |
| | | deviceBillboardCollection.show = detailVisible.value |
| | | const ringFillInstancesByStyle = RING_STYLES.map(() => []) |
| | | const ringOutlineInstancesByStyle = RING_STYLES.map(() => []) |
| | | const deviceEntries = [] |
| | | const devicePositions = [] |
| | | devices.forEach((item, index) => { |
| | | const isOnline = String(item.status) === '0' |
| | | |
| | | const position = getDevicePosition(item) |
| | | if (!position) return |
| | | const entityId = `online-device-${item.id ?? index}-${index}` |
| | | const rangeMeters = getDeviceRange(item) |
| | | addDeviceRings(position, ringFillInstancesByStyle, ringOutlineInstancesByStyle, rangeMeters) |
| | | const billboard = deviceBillboardCollection.add({ |
| | | position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0), |
| | | image: equipmentIcon, |
| | | width: 40, |
| | | height: 56, |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, |
| | | }) |
| | | billboard.id = entityId |
| | | devicePickMap.set(entityId, { data: item, billboard }) |
| | | devicePositions.push({ lng: position.longitude, lat: position.latitude }) |
| | | deviceEntries.push({ position, rangeMeters, isOnline, entityId, data: item }) |
| | | }) |
| | | if (deviceEntries.length) { |
| | | const heights = await getPointPositionsHeight(devicePositions, viewer) |
| | | deviceEntries.forEach((entry, index) => { |
| | | const height = Number(heights?.[index]?.ASL) |
| | | const centerHeight = Number.isFinite(height) ? height : 0 |
| | | const billboard = deviceBillboardCollection.add({ |
| | | position: Cesium.Cartesian3.fromDegrees( |
| | | entry.position.longitude, |
| | | entry.position.latitude, |
| | | centerHeight |
| | | ), |
| | | image: entry.isOnline ? equipmentIcon : offlineEquipmentIcon, |
| | | width: 40, |
| | | height: 56, |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }) |
| | | billboard.id = entry.entityId |
| | | devicePickMap.set(entry.entityId, { data: entry.data, billboard }) |
| | | if (!entry.isOnline) return |
| | | if (!Number.isFinite(entry.rangeMeters) || entry.rangeMeters <= 0) return |
| | | const primitive = createDeviceRangePrimitiveWithHeight(entry.position, entry.rangeMeters, centerHeight) |
| | | if (!primitive) return |
| | | primitive.show = detailVisible.value |
| | | addCockpitPrimitive(primitive) |
| | | deviceRingFillPrimitives.push(primitive) |
| | | }) |
| | | } |
| | | if (selectedTargetType.value === 'device' && selectedDeviceBillboard && !devicePickMap.has(selectedDeviceBillboard.id)) { |
| | | closePopup() |
| | | } |
| | | ringFillInstancesByStyle.forEach((instances, index) => { |
| | | if (!instances.length) return |
| | | const ring = RING_STYLES[index] |
| | | const alpha = ring.alpha ?? 0.64 |
| | | const innerRatio = |
| | | typeof ring.innerRatio === 'number' |
| | | ? ring.innerRatio |
| | | : ring.inner && ring.outer |
| | | ? Math.min(Math.max(ring.inner / ring.outer, 0), 0.9) |
| | | : 0 |
| | | const material = createRadialGradientMaterial( |
| | | Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(alpha), |
| | | Cesium.Color.fromCssColorString(ring.gradient[1]).withAlpha(alpha), |
| | | { |
| | | gamma: 1.7, |
| | | innerCutoff: innerRatio, |
| | | reorderCockpitPrimitives() |
| | | } |
| | | |
| | | const elevatePositions = async (positions, height = POLYGON_HEIGHT_M) => { |
| | | if (!Array.isArray(positions) || !positions.length) return [] |
| | | if (!viewer) return positions |
| | | const points = positions |
| | | .map(pos => { |
| | | const carto = Cesium.Cartographic.fromCartesian(pos) |
| | | if (!carto) return null |
| | | return { |
| | | lng: Cesium.Math.toDegrees(carto.longitude), |
| | | lat: Cesium.Math.toDegrees(carto.latitude), |
| | | } |
| | | ) |
| | | 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() |
| | | .filter(Boolean) |
| | | if (!points.length) return positions |
| | | const heights = await getPointPositionsHeight(points, viewer) |
| | | return heights.map(item => Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude, (item.ASL ?? 0) + height)) |
| | | } |
| | | |
| | | const getDefenseZonePositions = geom => { |
| | | const points = geomAnalysis(geom) |
| | | if (points.length < 3) return [] |
| | | const first = points[0] |
| | | const last = points[points.length - 1] |
| | | const list = |
| | | first && last && first.longitude === last.longitude && first.latitude === last.latitude |
| | | ? points.slice(0, -1) |
| | | : points |
| | | return list.map(item => Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude)) |
| | | const getAreaTypeStyle = areaType => { |
| | | return AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE |
| | | } |
| | | |
| | | 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 polygon = new Cesium.PolygonGeometry({ |
| | | polygonHierarchy: new Cesium.PolygonHierarchy(positions), |
| | | vertexFormat: texturedVertexFormat, |
| | | const buildPartitionPrimitives = async shapes => { |
| | | if (!viewer) return { primitives: [], outlinePrimitives: [] } |
| | | const vertexFormat = getTexturedVertexFormat() |
| | | const groups = new Map() |
| | | const tasks = [] |
| | | ; (shapes || []).forEach(shape => { |
| | | if (!shape?.positions || shape.positions.length < 3) return |
| | | const outlineColor = shape.outlineColor |
| | | const fillColor = shape.fillColor |
| | | const key = `${outlineColor.toCssColorString()}|${fillColor.toCssColorString()}` |
| | | if (!groups.has(key)) { |
| | | groups.set(key, { outlineColor, fillColor, polygonInstances: [], lineInstances: [] }) |
| | | } |
| | | const group = groups.get(key) |
| | | const positions = shape.positions |
| | | tasks.push( |
| | | elevatePositions(positions).then(elevatedPositions => { |
| | | if (!elevatedPositions.length) return |
| | | const polygon = new Cesium.PolygonGeometry({ |
| | | polygonHierarchy: new Cesium.PolygonHierarchy(elevatedPositions), |
| | | perPositionHeight: true, |
| | | vertexFormat, |
| | | }) |
| | | group.polygonInstances.push( |
| | | new Cesium.GeometryInstance({ |
| | | geometry: polygon, |
| | | }) |
| | | ) |
| | | const linePositions = |
| | | elevatedPositions.length > 1 |
| | | ? [...elevatedPositions, elevatedPositions[0]] |
| | | : elevatedPositions |
| | | group.lineInstances.push( |
| | | new Cesium.GeometryInstance({ |
| | | geometry: new Cesium.PolylineGeometry({ |
| | | positions: linePositions, |
| | | width: 2, |
| | | }), |
| | | attributes: { |
| | | color: Cesium.ColorGeometryInstanceAttribute.fromColor(outlineColor), |
| | | }, |
| | | }) |
| | | ) |
| | | }) |
| | | ) |
| | | }) |
| | | polygonInstances.push( |
| | | new Cesium.GeometryInstance({ |
| | | geometry: polygon, |
| | | |
| | | if (tasks.length) await Promise.all(tasks) |
| | | |
| | | const primitives = [] |
| | | const outlinePrimitives = [] |
| | | groups.forEach(group => { |
| | | if (group.polygonInstances.length) { |
| | | const material = createRadialGradientMaterial(group.outlineColor, group.fillColor, { |
| | | gamma: 1.7, |
| | | innerCutoff: 0, |
| | | }) |
| | | ) |
| | | const linePositions = positions.length > 1 ? [...positions, positions[0]] : positions |
| | | lineInstances.push( |
| | | new Cesium.GeometryInstance({ |
| | | geometry: new Cesium.GroundPolylineGeometry({ |
| | | positions: linePositions, |
| | | width: 2, |
| | | const primitive = new Cesium.Primitive({ |
| | | geometryInstances: group.polygonInstances, |
| | | appearance: new Cesium.MaterialAppearance({ |
| | | material, |
| | | translucent: true, |
| | | }), |
| | | attributes: { |
| | | color: Cesium.ColorGeometryInstanceAttribute.fromColor( |
| | | Cesium.Color.fromCssColorString(lineColor) |
| | | ), |
| | | }, |
| | | }) |
| | | ) |
| | | addCockpitPrimitive(primitive) |
| | | primitives.push(primitive) |
| | | } |
| | | if (group.lineInstances.length) { |
| | | const outlinePrimitive = new Cesium.Primitive({ |
| | | geometryInstances: group.lineInstances, |
| | | appearance: new Cesium.PolylineColorAppearance(), |
| | | }) |
| | | addCockpitPrimitive(outlinePrimitive) |
| | | outlinePrimitives.push(outlinePrimitive) |
| | | } |
| | | }) |
| | | 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 } |
| | | |
| | | return { primitives, outlinePrimitives } |
| | | } |
| | | |
| | | const renderDefenseZones = zones => { |
| | | if (!viewer) return |
| | | clearDefenseZoneEntities() |
| | | const result = buildZonePrimitives(zones, '#72F3FF', '#72F3FF', '#0A7C88') |
| | | defenseZonePrimitive = result.primitive |
| | | defenseZoneOutlinePrimitive = result.outlinePrimitive |
| | | if (defenseZonePrimitive) defenseZonePrimitive.show = detailVisible.value |
| | | if (defenseZoneOutlinePrimitive) defenseZoneOutlinePrimitive.show = detailVisible.value |
| | | reorderCockpitPrimitives() |
| | | const parseGeomJson = geomJson => { |
| | | if (!geomJson) return null |
| | | if (typeof geomJson === 'object') return geomJson |
| | | if (typeof geomJson !== 'string') return null |
| | | const trimmed = geomJson.trim() |
| | | if (!trimmed) return null |
| | | try { |
| | | return JSON.parse(trimmed) |
| | | } catch (error) { } |
| | | return null |
| | | } |
| | | |
| | | const renderPartitions = zones => { |
| | | const normalizeShapePoint = point => { |
| | | if (!point) return null |
| | | const lng = point?.lng ?? point?.longitude |
| | | const lat = point?.lat ?? point?.latitude |
| | | const height = Number.isFinite(Number(point?.height)) ? Number(point.height) : 0 |
| | | if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null |
| | | return { lng: Number(lng), lat: Number(lat), height } |
| | | } |
| | | |
| | | const buildShapePositions = (points = []) => { |
| | | const normalized = points.map(normalizeShapePoint).filter(Boolean) |
| | | return normalized.map(point => Cesium.Cartesian3.fromDegrees(point.lng, point.lat, point.height)) |
| | | } |
| | | |
| | | const getShapeDisplayPoints = shape => { |
| | | if (Array.isArray(shape?.displayPoints) && shape.displayPoints.length) { |
| | | return shape.displayPoints |
| | | } |
| | | return shape?.points || [] |
| | | } |
| | | |
| | | const resolvePartitionShapes = areas => { |
| | | const shapes = [] |
| | | ; (areas || []).forEach(area => { |
| | | const extList = Array.isArray(area?.fwAreaDivideExtList) ? area.fwAreaDivideExtList : [] |
| | | extList.forEach((item, index) => { |
| | | const isShapePayload = item?.drawType || item?.points |
| | | const parsed = isShapePayload ? item : parseGeomJson(item?.geomJson) |
| | | if (!parsed) return |
| | | const shape = { |
| | | id: parsed?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`, |
| | | drawType: parsed?.drawType ?? 'polygon', |
| | | areaType: parsed?.areaType ?? item?.areaTypeKey ?? item?.areaType ?? '', |
| | | points: Array.isArray(parsed?.points) ? parsed.points : [], |
| | | displayPoints: Array.isArray(parsed?.displayPoints) ? parsed.displayPoints : null, |
| | | meta: parsed?.meta ?? null, |
| | | } |
| | | if (shape.drawType === 'buffer' && shape.meta?.bufferRadii?.length && shape.meta?.center) { |
| | | const center = shape.meta.center |
| | | const centerCartesian = Cesium.Cartesian3.fromDegrees( |
| | | center.lng, |
| | | center.lat, |
| | | center.height || 0 |
| | | ) |
| | | const radii = shape.meta.bufferRadii |
| | | .map(radius => Number(radius)) |
| | | .filter(radius => Number.isFinite(radius) && radius > 0) |
| | | radii.forEach((radius, levelIndex) => { |
| | | const positions = buildEllipsePositions(centerCartesian, radius, radius) |
| | | const style = BUFFER_LEVEL_STYLES[levelIndex] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1] |
| | | if (positions.length >= 3) { |
| | | shapes.push({ |
| | | positions, |
| | | fillColor: style.fill, |
| | | outlineColor: style.outline, |
| | | }) |
| | | } |
| | | }) |
| | | return |
| | | } |
| | | const positions = buildShapePositions(getShapeDisplayPoints(shape)) |
| | | if (positions.length >= 3) { |
| | | const style = getAreaTypeStyle(shape.areaType) |
| | | shapes.push({ |
| | | positions, |
| | | fillColor: style.fill, |
| | | outlineColor: style.outline, |
| | | }) |
| | | } |
| | | }) |
| | | }) |
| | | return shapes |
| | | } |
| | | |
| | | const renderPartitions = async zones => { |
| | | if (!viewer) return |
| | | clearPartitionEntities() |
| | | const result = buildZonePrimitives(zones, '#FFD772', '#FFD772', '#A86B00') |
| | | partitionPrimitive = result.primitive |
| | | partitionOutlinePrimitive = result.outlinePrimitive |
| | | if (partitionPrimitive) partitionPrimitive.show = detailVisible.value |
| | | if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = detailVisible.value |
| | | reorderCockpitPrimitives() |
| | | } |
| | | |
| | | const loadDefenseZones = async () => { |
| | | if (!viewer) return |
| | | try { |
| | | const res = await fwDefenseZonePageApi({ current: 1, size: DEFAULT_ZONE_PAGE_SIZE }) |
| | | renderDefenseZones(res?.data?.data?.records ?? []) |
| | | } catch (error) { |
| | | renderDefenseZones([]) |
| | | const shapes = resolvePartitionShapes(zones) |
| | | const result = await buildPartitionPrimitives(shapes) |
| | | partitionFillPrimitives = result.primitives |
| | | partitionOutlinePrimitives = result.outlinePrimitives |
| | | if (partitionFillPrimitives.length) { |
| | | partitionFillPrimitives.forEach(primitive => { |
| | | primitive.show = detailVisible.value |
| | | }) |
| | | } |
| | | if (partitionOutlinePrimitives.length) { |
| | | partitionOutlinePrimitives.forEach(primitive => { |
| | | primitive.show = detailVisible.value |
| | | }) |
| | | } |
| | | reorderCockpitPrimitives() |
| | | } |
| | | |
| | | const loadPartitions = async () => { |
| | | if (!viewer) return |
| | | try { |
| | | const res = await fwAreaDividePageApi({ current: 1, size: DEFAULT_ZONE_PAGE_SIZE }) |
| | | renderPartitions(res?.data?.data?.records ?? []) |
| | | const res = await newAreaDivideList({ |
| | | isSetSceneManage: 1, |
| | | flyTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), |
| | | }) |
| | | await renderPartitions(res?.data?.data ?? []) |
| | | } catch (error) { |
| | | renderPartitions([]) |
| | | await renderPartitions([]) |
| | | } |
| | | } |
| | | |
| | |
| | | viewer.dataSources.add(aggregationSource) |
| | | } |
| | | clearAggregationEntities() |
| | | aggregationSource.show = clusterVisible.value |
| | | updateAggregationVisibility() |
| | | const countMap = new Map() |
| | | ; (list || []).forEach(item => { |
| | | if (!item?.type) return |
| | |
| | | |
| | | 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 |
| | |
| | | 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) |
| | | |
| | | const renderCommandPosts = list => { |
| | | const renderCommandPosts = async 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, |
| | | const entries = [] |
| | | const positions = [] |
| | | ; (list || []).forEach((item, index) => { |
| | | const position = getDevicePosition(item) |
| | | if (!position) return |
| | | entries.push({ position, index }) |
| | | positions.push({ lng: position.longitude, lat: position.latitude }) |
| | | }) |
| | | }) |
| | | if (positions.length) { |
| | | const heights = await getPointPositionsHeight(positions, viewer) |
| | | entries.forEach((entry, idx) => { |
| | | const height = Number(heights?.[idx]?.ASL) |
| | | const centerHeight = Number.isFinite(height) ? height : 0 |
| | | commandPostBillboardCollection.add({ |
| | | position: Cesium.Cartesian3.fromDegrees( |
| | | entry.position.longitude, |
| | | entry.position.latitude, |
| | | centerHeight |
| | | ), |
| | | image: commandPostIcon, |
| | | width: 40, |
| | | height: 56, |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }) |
| | | }) |
| | | } |
| | | reorderCockpitPrimitives() |
| | | } |
| | | |
| | | const loadAggregation = async () => { |
| | | try { |
| | | const res = await cockpitAggregationApi({ |
| | | const res = await newCockpitAggregationApi({ |
| | | effectiveRangeKmIsNotNull: 1 |
| | | }) |
| | | renderAggregation(res?.data?.data ?? []) |
| | |
| | | |
| | | const loadCommandPosts = async () => { |
| | | try { |
| | | const res = await fwDefenseSceneListApi() |
| | | renderCommandPosts(res?.data?.data ?? []) |
| | | const res = await newDefenseSceneManageList({ |
| | | time: dayjs().format('YYYY-MM-DD HH:mm:ss') |
| | | }) |
| | | await renderCommandPosts(res?.data?.data ?? []) |
| | | } catch (error) { |
| | | renderCommandPosts([]) |
| | | await renderCommandPosts([]) |
| | | } |
| | | } |
| | | |
| | | |
| | | watch( |
| | | () => props.onlineDevices, |
| | | () => props.allDevices, |
| | | devices => { |
| | | renderDeviceEntities(devices || []) |
| | | }, |
| | |
| | | ) |
| | | |
| | | watch( |
| | | () => props.leftCollapsed, |
| | | () => 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 } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.rightCollapsed, |
| | | isCollapsed => { |
| | | if (isCollapsed) showLayerPanel.value = false |
| | | if (!isCollapsed) return |
| | | showLayerPanel.value = false |
| | | showBaseLayerPanel.value = false |
| | | } |
| | | ) |
| | | |
| | | const toggleLayerPanel = () => { |
| | | if (!props.showLayerControl) return |
| | | showLayerPanel.value = !showLayerPanel.value |
| | | } |
| | | |
| | | const applyLayerVisibility = checkedKeys => { |
| | | publicCesium?.switchLayers?.(baseLayerKey.value === 'base-standard' ? 0 : 4) |
| | | const showTerrain = checkedKeys.includes('ja-terrain') |
| | | publicCesium?.setTerrainVisible?.(showTerrain) |
| | | const showAdmin = checkedKeys.includes('admin') |
| | | adminBoundaryVisible.value = showAdmin |
| | | mapRef.value?.setAdminBoundaryVisible?.(showAdmin) |
| | | updateAggregationVisibility() |
| | | } |
| | | |
| | | const handleLayerCheck = (_data, state) => { |
| | | const checkedKeys = state?.checkedKeys ?? layerTreeRef.value?.getCheckedKeys?.() ?? [] |
| | | treeCheckedKeys.value = checkedKeys |
| | | applyLayerVisibility(checkedKeys) |
| | | } |
| | | |
| | | const handleBaseLayerSelect = key => { |
| | | if (!baseLayerKeys.includes(key)) return |
| | | if (baseLayerKey.value === key) return |
| | | baseLayerKey.value = key |
| | | applyLayerVisibility(treeCheckedKeys.value) |
| | | } |
| | | |
| | | const updateStageDisplay = stage => { |
| | |
| | | setDroneVisibility(!showCluster) |
| | | } |
| | | |
| | | const handleMapReady = ({ viewer: mapViewer }) => { |
| | | const handleMapReady = async ({ viewer: mapViewer, publicCesium: mapPublic }) => { |
| | | if (mapReadyHandled) return |
| | | mapReadyHandled = true |
| | | viewer = mapViewer |
| | | publicCesium = mapPublic |
| | | ensureCockpitPrimitiveLayer() |
| | | applyLayerVisibility(treeCheckedKeys.value) |
| | | const height = viewer?.camera?.positionCartographic?.height |
| | | const stage = getStageByHeight(height) |
| | | updateStageDisplay(stage) |
| | | renderDeviceEntities(props.onlineDevices) |
| | | loadDefenseZones() |
| | | await loadCommandPosts() |
| | | renderDeviceEntities(props.allDevices) |
| | | loadPartitions() |
| | | loadAggregation() |
| | | loadCommandPosts() |
| | | renderSimulatedDroneTrack() |
| | | renderSimulatedDroneTrack(props.alarmDrones) |
| | | initDeviceClickHandler() |
| | | } |
| | | |
| | |
| | | updateStageDisplay(stage) |
| | | } |
| | | |
| | | const handleClickOutside = event => { |
| | | const handleLayerClickOutside = event => { |
| | | if (!showLayerPanel.value) return |
| | | const target = event.target |
| | | if (layerWrapRef.value?.contains(target)) return |
| | |
| | | } |
| | | |
| | | onMounted(() => { |
| | | document.addEventListener('click', handleClickOutside) |
| | | document.addEventListener('click', handleLayerClickOutside) |
| | | const map = mapRef.value?.getMap() |
| | | if (map?.viewer) handleMapReady(map) |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | document.removeEventListener('click', handleClickOutside) |
| | | document.removeEventListener('click', handleLayerClickOutside) |
| | | clearDeviceEntities() |
| | | clearDefenseZoneEntities() |
| | | clearPartitionEntities() |
| | | clearAggregationEntities() |
| | | clearCommandPostEntities() |
| | |
| | | } |
| | | cockpitPrimitiveLayer = null |
| | | viewer = null |
| | | publicCesium = null |
| | | }) |
| | | |
| | | const getMap = () => { |
| | | return mapRef.value?.getMap() |
| | | } |
| | | |
| | | // 暴露给父组件调用 |
| | | const flyTo = (options) => { |
| | | mapRef.value?.flyToPoint(options) |
| | | } |
| | | |
| | | defineExpose({ |
| | | getMap, |
| | | flyTo |
| | | }) |
| | | </script> |
| | | |
| | |
| | | |
| | | .layer-control-root { |
| | | position: absolute; |
| | | left: 337px; |
| | | bottom: 22px; |
| | | z-index: 9; |
| | | right: 337px; |
| | | top: 95px; |
| | | z-index: 11; |
| | | transition: transform 0.3s ease-in-out; |
| | | pointer-events: none; |
| | | |
| | | &.collapsed { |
| | | transform: translateX(-317px); |
| | | transform: translateX(317px); |
| | | } |
| | | } |
| | | |
| | |
| | | display: flex; |
| | | flex-direction: column; |
| | | position: absolute; |
| | | left: 66px; |
| | | bottom: 0; |
| | | right: 66px; |
| | | top: 0; |
| | | width: 160px; |
| | | max-height: 442px; |
| | | background: #191932; |
| | | border-radius: 8px 8px 8px 8px; |
| | | z-index: 99; |
| | | |
| | | .panel-title { |
| | | padding: 0 16px; |
| | |
| | | flex: 1; |
| | | overflow: auto; |
| | | } |
| | | |
| | | } |
| | | |
| | | .base-layer-switch-root { |
| | | position: absolute; |
| | | right: 337px; |
| | | bottom: 78px; |
| | | z-index: 11; |
| | | transition: transform 0.3s ease-in-out; |
| | | pointer-events: none; |
| | | |
| | | &.collapsed { |
| | | transform: translateX(317px); |
| | | } |
| | | } |
| | | |
| | | .base-layer-switch-wrap { |
| | | position: relative; |
| | | pointer-events: auto; |
| | | } |
| | | |
| | | .base-layer-trigger { |
| | | width: 55px; |
| | | padding: 8px 0; |
| | | border-radius: 8px; |
| | | background: #191932; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 6px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .base-layer-trigger-thumb { |
| | | width: 36px; |
| | | height: 36px; |
| | | border-radius: 6px; |
| | | display: block; |
| | | // border: 2px solid #fff; |
| | | background-size: cover; |
| | | background-position: center; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .base-layer-trigger-label { |
| | | margin-top: 8px; |
| | | font-size: 12px; |
| | | line-height: 1; |
| | | color: #ffffff; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .base-layer-panel { |
| | | position: absolute; |
| | | right: calc(100% + 10px); |
| | | bottom: 0; |
| | | margin-bottom: 0; |
| | | width: 160px; |
| | | background: #191932; |
| | | border-radius: 8px; |
| | | z-index: 99; |
| | | |
| | | &::after { |
| | | content: ''; |
| | | position: absolute; |
| | | right: -10px; |
| | | top: 0; |
| | | width: 10px; |
| | | height: 100%; |
| | | background: transparent; |
| | | } |
| | | } |
| | | |
| | | .base-map-options { |
| | | padding: 8px; |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 8px; |
| | | } |
| | | |
| | | .base-map-card { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 6px; |
| | | border-radius: 6px; |
| | | color: #d8e6ff; |
| | | font-size: 12px; |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | |
| | | .base-map-thumb { |
| | | width: 36px; |
| | | height: 36px; |
| | | border-radius: 6px; |
| | | // border: 2px solid #fff; |
| | | background-size: cover; |
| | | background-position: center; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .base-map-label { |
| | | margin-top: 8px; |
| | | font-size: 12px; |
| | | line-height: 1; |
| | | color: #ffffff; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | |
| | | // &.active { |
| | | // .base-map-thumb { |
| | | // border: 2px solid #2ea8ff; |
| | | // } |
| | | |
| | | // .base-map-label { |
| | | // color: #2ea8ff; |
| | | // } |
| | | // } |
| | | } |
| | | |
| | | .layer-panel-slide-enter-active, |
| | | .layer-panel-slide-leave-active { |
| | | transition: transform 0.24s ease, opacity 0.24s ease; |
| | | transform-origin: right center; |
| | | } |
| | | |
| | | .layer-panel-slide-enter-from, |
| | | .layer-panel-slide-leave-to { |
| | | transform: translateX(24px); |
| | | opacity: 0; |
| | | } |
| | | </style> |
| | | |