<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="CLUSTER_HEIGHT" :detail-height="DETAIL_HEIGHT" @ready="handleMapReady"
|
@stage-change="handleStageChange" />
|
<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.stop="toggleLayerPanel">
|
<img :src="layerControlIcon" alt="图层控制" />
|
</div>
|
<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" />
|
|
<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 { 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 {
|
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({
|
allDevices: {
|
type: Array,
|
default: () => [],
|
},
|
alarmDrones: {
|
type: Array,
|
default: () => [],
|
},
|
rightCollapsed: {
|
type: Boolean,
|
default: false,
|
},
|
containerId: {
|
type: String,
|
default: 'device-map-container',
|
},
|
showLayerControl: {
|
type: Boolean,
|
default: true,
|
},
|
})
|
const emit = defineEmits(['droneSignal', 'droneCounter', 'droneFavorite'])
|
|
|
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 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
|
let deviceClickHandler = null
|
let popupRenderHandler = null
|
const layerTreeProps = {
|
label: 'label',
|
children: 'children',
|
}
|
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: 'ja-terrain', label: '吉安地形' },
|
{ key: 'admin', label: '行政区划' },
|
],
|
},
|
|
])
|
|
/**
|
* {
|
key: 'city',
|
label: '城市CIM图层',
|
children: [
|
{ key: 'city-base', label: '皖山白模' },
|
{ key: 'city-grid', label: '皖山白模光栅网格' },
|
{ key: 'city-tilt', label: '皖山倾斜摄影' },
|
{ key: 'city-tilt-grid', label: '皖山倾斜摄影网格' },
|
],
|
},
|
{
|
key: 'sky',
|
label: '空域要素图层',
|
children: [
|
{ key: 'airspace', label: '空域边界' },
|
{ 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
|
const latitudeRaw = item.latitude ?? item.lat
|
if (longitudeRaw == null || latitudeRaw == null) return null
|
const longitude = Number(longitudeRaw)
|
const latitude = Number(latitudeRaw)
|
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return null
|
return { longitude, latitude }
|
}
|
|
const getDeviceRange = item => {
|
if (!item) return null
|
const rawRange = item.effectiveRangeKm ?? item.range ?? item.coverRadiusM
|
const range = Number(rawRange)
|
if (!Number.isFinite(range) || range <= 0) return null
|
return range
|
}
|
|
|
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 (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)
|
}
|
|
const clearDeviceEntities = () => {
|
if (!viewer) return
|
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 clearPartitionEntities = () => {
|
if (!viewer) return
|
if (partitionFillPrimitives.length) {
|
partitionFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
|
partitionFillPrimitives = []
|
}
|
if (partitionOutlinePrimitives.length) {
|
partitionOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
|
partitionOutlinePrimitives = []
|
}
|
}
|
|
const clearAggregationEntities = () => {
|
if (!aggregationSource) return
|
aggregationSource.entities.removeAll()
|
}
|
|
const clearCommandPostEntities = () => {
|
if (!viewer) return
|
if (commandPostBillboardCollection) {
|
commandPostBillboardCollection.removeAll()
|
removeCockpitPrimitive(commandPostBillboardCollection)
|
commandPostBillboardCollection = null
|
}
|
}
|
|
const ensureCommandPostCollection = () => {
|
if (!viewer) return
|
ensureCockpitPrimitiveLayer()
|
if (!commandPostBillboardCollection) {
|
commandPostBillboardCollection = new Cesium.BillboardCollection()
|
addCockpitPrimitive(commandPostBillboardCollection)
|
}
|
}
|
|
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 (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) {
|
deviceRingFillPrimitives.forEach(primitive => {
|
primitive.show = visible
|
})
|
}
|
if (deviceRingOutlinePrimitives.length) {
|
deviceRingOutlinePrimitives.forEach(primitive => {
|
primitive.show = visible
|
})
|
}
|
if (!visible) closePopup()
|
}
|
|
const setClusterVisibility = visible => {
|
clusterVisible.value = 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 getStageByHeight = height => {
|
if (height == null) return 'detail'
|
if (height >= CLUSTER_HEIGHT) return 'cluster'
|
if (height <= DETAIL_HEIGHT) return 'detail'
|
return 'mid'
|
}
|
|
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 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)
|
}
|
droneTrackBillboardCollection = null
|
}
|
if (droneTrackPolylineCollection) {
|
if (!droneTrackPolylineCollection.isDestroyed?.()) {
|
removeCockpitPrimitive(droneTrackPolylineCollection)
|
}
|
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 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 ensureDroneTrackSource = () => {
|
if (!viewer) return
|
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
|
}
|
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 = () => {
|
droneTrackTickHandler = null
|
if (droneTrackRafId) {
|
cancelAnimationFrame(droneTrackRafId)
|
droneTrackRafId = null
|
}
|
}
|
|
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 baseTrackColor = Cesium.Color.fromCssColorString('red')
|
; (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: {
|
...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,
|
})
|
})
|
startDroneTrackAnimation()
|
startFavoritePulseAnimation()
|
reorderCockpitPrimitives()
|
}
|
|
const renderDeviceEntities = async devices => {
|
if (!viewer) return
|
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 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)
|
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()
|
}
|
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),
|
}
|
})
|
.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 getAreaTypeStyle = areaType => {
|
return AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE
|
}
|
|
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),
|
},
|
})
|
)
|
})
|
)
|
})
|
|
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 primitive = new Cesium.Primitive({
|
geometryInstances: group.polygonInstances,
|
appearance: new Cesium.MaterialAppearance({
|
material,
|
translucent: true,
|
}),
|
})
|
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)
|
}
|
})
|
|
return { primitives, outlinePrimitives }
|
}
|
|
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 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 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 newAreaDivideList({
|
isSetSceneManage: 1,
|
flyTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
})
|
await renderPartitions(res?.data?.data ?? [])
|
} catch (error) {
|
await renderPartitions([])
|
}
|
}
|
|
const renderAggregation = list => {
|
if (!viewer) return
|
ensureCountyCenterMap()
|
if (!aggregationSource) {
|
aggregationSource = new Cesium.CustomDataSource('aggregationSource')
|
viewer.dataSources.add(aggregationSource)
|
}
|
clearAggregationEntities()
|
updateAggregationVisibility()
|
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,
|
// disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
},
|
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,
|
},
|
})
|
})
|
}
|
|
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 = getBillboardPosition(selectedDeviceBillboard)
|
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 handleDroneFavorite = () => {
|
if (!selectedDevice.value) return
|
emit('droneFavorite', selectedDevice.value)
|
}
|
const handleDroneSignal = () => emit('droneSignal', selectedDevice.value)
|
const handleDroneCounter = () => emit('droneCounter', selectedDevice.value)
|
|
const renderCommandPosts = async list => {
|
if (!viewer) return
|
ensureCommandPostCollection()
|
commandPostBillboardCollection.removeAll()
|
commandPostBillboardCollection.show = detailVisible.value
|
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 newCockpitAggregationApi({
|
effectiveRangeKmIsNotNull: 1
|
})
|
renderAggregation(res?.data?.data ?? [])
|
} catch (error) {
|
renderAggregation([])
|
}
|
}
|
|
const loadCommandPosts = async () => {
|
try {
|
const res = await newDefenseSceneManageList({
|
time: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
})
|
await renderCommandPosts(res?.data?.data ?? [])
|
} catch (error) {
|
await renderCommandPosts([])
|
}
|
}
|
|
|
watch(
|
() => props.allDevices,
|
devices => {
|
renderDeviceEntities(devices || [])
|
},
|
{ deep: true }
|
)
|
|
watch(
|
() => props.alarmDrones,
|
list => {
|
renderSimulatedDroneTrack(list || [])
|
if (selectedTargetType.value === 'drone' && selectedDevice.value) {
|
const selectedId = selectedDevice.value.alarmRecordId ?? selectedDevice.value.id
|
const match = (list || []).find(item => (item?.alarmRecordId ?? item?.id) === selectedId)
|
if (match) selectedDevice.value = { ...selectedDevice.value, ...match }
|
}
|
},
|
{ deep: true }
|
)
|
|
watch(
|
() => props.rightCollapsed,
|
isCollapsed => {
|
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 => {
|
const showCluster = stage === 'cluster'
|
setClusterVisibility(showCluster)
|
setDetailVisibility(!showCluster)
|
setDroneVisibility(!showCluster)
|
}
|
|
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)
|
await loadCommandPosts()
|
renderDeviceEntities(props.allDevices)
|
loadPartitions()
|
loadAggregation()
|
renderSimulatedDroneTrack(props.alarmDrones)
|
initDeviceClickHandler()
|
}
|
|
const handleStageChange = stage => {
|
updateStageDisplay(stage)
|
}
|
|
const handleLayerClickOutside = event => {
|
if (!showLayerPanel.value) return
|
const target = event.target
|
if (layerWrapRef.value?.contains(target)) return
|
showLayerPanel.value = false
|
}
|
|
onMounted(() => {
|
document.addEventListener('click', handleLayerClickOutside)
|
const map = mapRef.value?.getMap()
|
if (map?.viewer) handleMapReady(map)
|
})
|
|
onBeforeUnmount(() => {
|
document.removeEventListener('click', handleLayerClickOutside)
|
clearDeviceEntities()
|
clearPartitionEntities()
|
clearAggregationEntities()
|
clearCommandPostEntities()
|
clearDroneTrackEntities()
|
closePopup()
|
destroyDeviceClickHandler()
|
if (cockpitPrimitiveLayer && viewer?.scene?.primitives) {
|
viewer.scene.primitives.remove(cockpitPrimitiveLayer)
|
}
|
cockpitPrimitiveLayer = null
|
viewer = null
|
publicCesium = null
|
})
|
|
const getMap = () => {
|
return mapRef.value?.getMap()
|
}
|
|
// 暴露给父组件调用
|
const flyTo = (options) => {
|
mapRef.value?.flyToPoint(options)
|
}
|
|
defineExpose({
|
getMap,
|
flyTo
|
})
|
</script>
|
|
<style lang="scss" scoped>
|
.map-shell {
|
position: relative;
|
width: 100%;
|
height: 100%;
|
}
|
|
.map-container {
|
position: absolute;
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 100%;
|
}
|
|
.layer-control-root {
|
position: absolute;
|
right: 337px;
|
top: 95px;
|
z-index: 11;
|
transition: transform 0.3s ease-in-out;
|
pointer-events: none;
|
|
&.collapsed {
|
transform: translateX(317px);
|
}
|
}
|
|
.layer-control-wrap {
|
position: relative;
|
display: flex;
|
align-items: flex-end;
|
pointer-events: auto;
|
}
|
|
.layer-control {
|
width: 46px;
|
height: 46px;
|
cursor: pointer;
|
|
img {
|
width: 100%;
|
height: 100%;
|
display: block;
|
}
|
}
|
|
.layer-panel {
|
display: flex;
|
flex-direction: column;
|
position: absolute;
|
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;
|
line-height: 42px;
|
font-family: 'Open Sans', Open Sans;
|
font-weight: 400;
|
font-size: 12px;
|
color: #ffffff;
|
text-align: left;
|
font-style: normal;
|
text-transform: none;
|
border-bottom: 1px solid rgba(70, 70, 100, 0.5);
|
box-sizing: border-box;
|
}
|
|
.panel-content {
|
padding: 0 16px;
|
height: 0;
|
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>
|