<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.leftCollapsed }">
|
<div class="layer-control-wrap" ref="layerWrapRef">
|
<div class="layer-control" @click="toggleLayerPanel">
|
<img :src="layerControlIcon" alt="图层控制" />
|
</div>
|
<div v-if="showLayerPanel" class="layer-panel">
|
<div class="panel-title">图层管理</div>
|
|
<div class="panel-content">
|
<el-tree class="command-tree" :data="layerTree" show-checkbox default-expand-all node-key="key"
|
:props="layerTreeProps" :default-checked-keys="defaultCheckedKeys" />
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<DevicePopup
|
:visible="popupVisible && !isDronePopup"
|
:position="popupPosition"
|
:device="selectedDevice"
|
@close="closePopup"
|
/>
|
|
<DronePopup
|
:visible="popupVisible && isDronePopup"
|
:position="popupPosition"
|
:drone="selectedDevice"
|
:favorite="Boolean(selectedDevice?.isFavorite)"
|
@close="closePopup"
|
@toggle-favorite="toggleDroneFavorite"
|
@signal="handleDroneSignal"
|
@counter="handleDroneCounter"
|
/>
|
</div>
|
</template>
|
|
<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 layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
|
import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
|
import droneIcon from '@/assets/images/dataCockpit/map/drone.png'
|
import aggregationIcon from '@/assets/images/dataCockpit/map/aggregation.png'
|
import commandPostIcon from '@/assets/images/dataCockpit/map/command-post.png'
|
import jaGeojsonRaw from '@/assets/geojson/ja.geojson?raw'
|
import DevicePopup from './components/DevicePopup.vue'
|
import DronePopup from './components/DronePopup.vue'
|
import {
|
RING_STYLES,
|
addDeviceRings,
|
createDroneTrackMaterial,
|
createRadialGradientMaterial,
|
getTexturedVertexFormat,
|
} from './device-map-materials'
|
|
const CLUSTER_HEIGHT = 100000
|
const DETAIL_HEIGHT = 10000
|
|
const props = defineProps({
|
onlineDevices: {
|
type: Array,
|
default: () => [],
|
},
|
leftCollapsed: {
|
type: Boolean,
|
default: false,
|
},
|
containerId: {
|
type: String,
|
default: 'device-map-container',
|
},
|
showLayerControl: {
|
type: Boolean,
|
default: true,
|
},
|
})
|
const emit = defineEmits(['droneSignal', 'droneCounter'])
|
|
const DEFAULT_ZONE_PAGE_SIZE = 999
|
|
const mapRef = ref(null)
|
let viewer = null
|
let cockpitPrimitiveLayer = null
|
const devicePickMap = new Map()
|
const dronePickMap = new Map()
|
let deviceBillboardCollection = null
|
let deviceRingOutlinePrimitives = []
|
let deviceRingFillPrimitives = []
|
let defenseZonePrimitive = null
|
let defenseZoneOutlinePrimitive = null
|
let partitionPrimitive = null
|
let partitionOutlinePrimitive = null
|
let aggregationSource = null
|
let commandPostBillboardCollection = null
|
let droneTrackBillboardCollection = null
|
let droneTrackPolylineCollection = null
|
let droneTrackTickHandler = null
|
let droneTrackStartTime = null
|
let droneTrackRuntime = []
|
const detailVisible = ref(true)
|
const clusterVisible = ref(false)
|
const countyCenterMap = new Map()
|
const showLayerPanel = ref(false)
|
const layerWrapRef = ref(null)
|
const selectedDevice = ref(null)
|
const selectedTargetType = ref('device')
|
let selectedDeviceBillboard = null
|
let deviceClickHandler = null
|
let popupRenderHandler = null
|
const layerTreeProps = {
|
label: 'label',
|
children: 'children',
|
}
|
const defaultCheckedKeys = ['global', 'city-base']
|
const layerTree = ref([
|
{
|
key: 'base',
|
label: '地理信息图层',
|
children: [
|
{ key: 'global', 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 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 (defenseZonePrimitive) cockpitPrimitiveLayer.add(defenseZonePrimitive)
|
if (defenseZoneOutlinePrimitive) cockpitPrimitiveLayer.add(defenseZoneOutlinePrimitive)
|
if (partitionPrimitive) cockpitPrimitiveLayer.add(partitionPrimitive)
|
if (partitionOutlinePrimitive) cockpitPrimitiveLayer.add(partitionOutlinePrimitive)
|
if (deviceRingFillPrimitives.length) {
|
deviceRingFillPrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
|
}
|
if (deviceRingOutlinePrimitives.length) {
|
deviceRingOutlinePrimitives.forEach(primitive => cockpitPrimitiveLayer.add(primitive))
|
}
|
if (droneTrackPolylineCollection) cockpitPrimitiveLayer.add(droneTrackPolylineCollection)
|
if (droneTrackBillboardCollection) cockpitPrimitiveLayer.add(droneTrackBillboardCollection)
|
if (deviceBillboardCollection) cockpitPrimitiveLayer.add(deviceBillboardCollection)
|
if (commandPostBillboardCollection) cockpitPrimitiveLayer.add(commandPostBillboardCollection)
|
}
|
|
const clearDeviceEntities = () => {
|
if (!viewer) return
|
if (deviceBillboardCollection) {
|
deviceBillboardCollection.removeAll()
|
removeCockpitPrimitive(deviceBillboardCollection)
|
deviceBillboardCollection = null
|
}
|
if (deviceRingFillPrimitives.length) {
|
deviceRingFillPrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
|
deviceRingFillPrimitives = []
|
}
|
if (deviceRingOutlinePrimitives.length) {
|
deviceRingOutlinePrimitives.forEach(primitive => removeCockpitPrimitive(primitive))
|
deviceRingOutlinePrimitives = []
|
}
|
devicePickMap.clear()
|
}
|
|
const ensureDeviceCollections = () => {
|
if (!viewer) return
|
ensureCockpitPrimitiveLayer()
|
if (!deviceBillboardCollection) {
|
deviceBillboardCollection = new Cesium.BillboardCollection()
|
addCockpitPrimitive(deviceBillboardCollection)
|
}
|
}
|
|
const clearDefenseZoneEntities = () => {
|
if (!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 (partitionOutlinePrimitive) {
|
removeCockpitPrimitive(partitionOutlinePrimitive)
|
partitionOutlinePrimitive = null
|
}
|
}
|
|
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 setDetailVisibility = visible => {
|
detailVisible.value = visible
|
if (defenseZonePrimitive) defenseZonePrimitive.show = visible
|
if (defenseZoneOutlinePrimitive) defenseZoneOutlinePrimitive.show = visible
|
if (partitionPrimitive) partitionPrimitive.show = visible
|
if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = visible
|
if (commandPostBillboardCollection) commandPostBillboardCollection.show = visible
|
if (deviceBillboardCollection) deviceBillboardCollection.show = visible
|
if (deviceRingFillPrimitives.length) {
|
deviceRingFillPrimitives.forEach(primitive => {
|
primitive.show = visible
|
})
|
}
|
if (deviceRingOutlinePrimitives.length) {
|
deviceRingOutlinePrimitives.forEach(primitive => {
|
primitive.show = visible
|
})
|
}
|
if (!visible) closePopup()
|
}
|
|
const setClusterVisibility = visible => {
|
clusterVisible.value = visible
|
if (aggregationSource) aggregationSource.show = visible
|
}
|
|
const setDroneVisibility = visible => {
|
if (droneTrackBillboardCollection) droneTrackBillboardCollection.show = visible
|
if (droneTrackPolylineCollection) droneTrackPolylineCollection.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 = () => {
|
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 clearDroneTrackEntities = () => {
|
if (!viewer) return
|
stopDroneTrackAnimation()
|
if (droneTrackBillboardCollection) {
|
if (!droneTrackBillboardCollection.isDestroyed?.()) {
|
removeCockpitPrimitive(droneTrackBillboardCollection)
|
}
|
droneTrackBillboardCollection = null
|
}
|
if (droneTrackPolylineCollection) {
|
if (!droneTrackPolylineCollection.isDestroyed?.()) {
|
removeCockpitPrimitive(droneTrackPolylineCollection)
|
}
|
droneTrackPolylineCollection = null
|
}
|
droneTrackRuntime = []
|
dronePickMap.clear()
|
if (selectedTargetType.value === 'drone') {
|
closePopup()
|
}
|
}
|
|
|
const ensureDroneTrackCollections = () => {
|
if (!viewer) return
|
ensureCockpitPrimitiveLayer()
|
if (droneTrackBillboardCollection?.isDestroyed?.()) {
|
droneTrackBillboardCollection = null
|
}
|
if (droneTrackPolylineCollection?.isDestroyed?.()) {
|
droneTrackPolylineCollection = null
|
}
|
if (!droneTrackBillboardCollection) {
|
droneTrackBillboardCollection = new Cesium.BillboardCollection()
|
addCockpitPrimitive(droneTrackBillboardCollection)
|
}
|
if (!droneTrackPolylineCollection) {
|
droneTrackPolylineCollection = new Cesium.PolylineCollection()
|
addCockpitPrimitive(droneTrackPolylineCollection)
|
}
|
}
|
|
const startDroneTrackAnimation = () => {
|
if (!viewer) return
|
stopDroneTrackAnimation()
|
droneTrackStartTime = Cesium.JulianDate.now()
|
droneTrackTickHandler = clock => {
|
if (!viewer || viewer.isDestroyed?.() || !viewer.scene) {
|
stopDroneTrackAnimation()
|
return
|
}
|
if (!droneTrackRuntime.length) return
|
const current = clock.currentTime
|
const elapsed = Cesium.JulianDate.secondsDifference(current, droneTrackStartTime)
|
droneTrackRuntime.forEach(track => {
|
if (!track?.billboard || track.billboard.isDestroyed?.()) return
|
if (!track.billboard._scene) return
|
if (!track.positions || track.positions.length < 2) return
|
const duration = track.duration
|
if (duration <= 0) return
|
const t = ((elapsed % duration) + duration) % duration
|
const seg = Math.min(track.positions.length - 2, Math.floor(t / track.segmentDuration))
|
const ratio = (t - seg * track.segmentDuration) / track.segmentDuration
|
const pos = Cesium.Cartesian3.lerp(
|
track.positions[seg],
|
track.positions[seg + 1],
|
ratio,
|
new Cesium.Cartesian3()
|
)
|
track.billboard.position = pos
|
})
|
}
|
viewer.clock.onTick.addEventListener(droneTrackTickHandler)
|
viewer.clock.shouldAnimate = true
|
}
|
|
const stopDroneTrackAnimation = () => {
|
if (!viewer || !droneTrackTickHandler) return
|
viewer.clock.onTick.removeEventListener(droneTrackTickHandler)
|
droneTrackTickHandler = null
|
}
|
|
const renderSimulatedDroneTrack = () => {
|
if (!viewer) return
|
clearDroneTrackEntities()
|
ensureDroneTrackCollections()
|
droneTrackBillboardCollection.show = detailVisible.value
|
droneTrackPolylineCollection.show = detailVisible.value
|
droneTrackRuntime = []
|
const baseCenters = [['默认区域', { longitude:114.958541, latitude: 27.121917 }]]
|
const segmentDuration = 6
|
const baseTrackColor = Cesium.Color.fromCssColorString('red')
|
baseCenters.forEach(([centerName, center], trackIndex) => {
|
const points = buildSimulatedTrackPoints(center)
|
if (points.length < 2) return
|
const positions = points.map(point =>
|
Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
|
)
|
let trackMaterial = createDroneTrackMaterial({
|
color: baseTrackColor,
|
speed: 4.5,
|
headWidth: 0.2,
|
glowPower: 1.8,
|
backgroundAlpha: 0.34,
|
})
|
if (!trackMaterial) {
|
trackMaterial = Cesium.Material.fromType('Color', { color: baseTrackColor })
|
}
|
droneTrackPolylineCollection.add({
|
positions,
|
width: 3,
|
material: trackMaterial,
|
})
|
const droneId = `drone-track-${trackIndex}`
|
const billboard = droneTrackBillboardCollection.add({
|
position: positions[positions.length - 1],
|
image: droneIcon,
|
width: 36,
|
height: 36,
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
})
|
billboard.id = droneId
|
const speedMs = Math.round(Cesium.Cartesian3.distance(positions[0], positions[1]) / segmentDuration)
|
dronePickMap.set(droneId, {
|
data: {
|
droneName: `无人机-${trackIndex + 1}`,
|
droneSerialNo: `UAV-${trackIndex + 1}`,
|
flightHeightM: points[0].height,
|
flightSpeedMs: speedMs,
|
longitude: points[0].longitude,
|
latitude: points[0].latitude,
|
flightStatus: 1,
|
deviceName: centerName,
|
dataSource: centerName,
|
counterDeviceName: centerName,
|
},
|
billboard,
|
})
|
droneTrackRuntime.push({
|
positions,
|
billboard,
|
segmentDuration,
|
duration: (points.length - 1) * segmentDuration,
|
})
|
})
|
viewer.clock.multiplier = 1
|
startDroneTrackAnimation()
|
reorderCockpitPrimitives()
|
}
|
|
const renderDeviceEntities = 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 ringFillInstancesByStyle = RING_STYLES.map(() => [])
|
const ringOutlineInstancesByStyle = RING_STYLES.map(() => [])
|
devices.forEach((item, index) => {
|
const position = getDevicePosition(item)
|
if (!position) return
|
const entityId = `online-device-${item.id ?? index}-${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 })
|
})
|
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,
|
}
|
)
|
const primitive = new Cesium.GroundPrimitive({
|
geometryInstances: instances,
|
appearance: new Cesium.MaterialAppearance({
|
material,
|
translucent: true,
|
}),
|
})
|
primitive.show = detailVisible.value
|
addCockpitPrimitive(primitive)
|
deviceRingFillPrimitives.push(primitive)
|
})
|
ringOutlineInstancesByStyle.forEach(instances => {
|
if (!instances.length) return
|
const primitive = new Cesium.GroundPolylinePrimitive({
|
geometryInstances: instances,
|
appearance: new Cesium.PolylineColorAppearance(),
|
})
|
primitive.show = detailVisible.value
|
addCockpitPrimitive(primitive)
|
deviceRingOutlinePrimitives.push(primitive)
|
})
|
reorderCockpitPrimitives()
|
}
|
|
const getDefenseZonePositions = geom => {
|
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 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,
|
})
|
polygonInstances.push(
|
new Cesium.GeometryInstance({
|
geometry: polygon,
|
})
|
)
|
const linePositions = positions.length > 1 ? [...positions, positions[0]] : positions
|
lineInstances.push(
|
new Cesium.GeometryInstance({
|
geometry: new Cesium.GroundPolylineGeometry({
|
positions: linePositions,
|
width: 2,
|
}),
|
attributes: {
|
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
|
Cesium.Color.fromCssColorString(lineColor)
|
),
|
},
|
})
|
)
|
})
|
let primitive = null
|
if (polygonInstances.length) {
|
const baseColor1 = Cesium.Color.fromCssColorString(fillColor1)
|
const baseColor2 = Cesium.Color.fromCssColorString(fillColor2)
|
const material = createRadialGradientMaterial(baseColor1.withAlpha(0.64), baseColor2.withAlpha(0.64))
|
primitive = new Cesium.GroundPrimitive({
|
geometryInstances: polygonInstances,
|
appearance: new Cesium.MaterialAppearance({
|
material,
|
translucent: true,
|
}),
|
})
|
addCockpitPrimitive(primitive)
|
}
|
let outlinePrimitive = null
|
if (lineInstances.length) {
|
outlinePrimitive = new Cesium.GroundPolylinePrimitive({
|
geometryInstances: lineInstances,
|
appearance: new Cesium.PolylineColorAppearance(),
|
})
|
addCockpitPrimitive(outlinePrimitive)
|
}
|
return { primitive, outlinePrimitive }
|
}
|
|
const renderDefenseZones = zones => {
|
if (!viewer) return
|
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 renderPartitions = 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 loadPartitions = async () => {
|
if (!viewer) return
|
try {
|
const res = await fwAreaDividePageApi({ current: 1, size: DEFAULT_ZONE_PAGE_SIZE })
|
renderPartitions(res?.data?.data?.records ?? [])
|
} catch (error) {
|
renderPartitions([])
|
}
|
}
|
|
const renderAggregation = list => {
|
if (!viewer) return
|
ensureCountyCenterMap()
|
if (!aggregationSource) {
|
aggregationSource = new Cesium.CustomDataSource('aggregationSource')
|
viewer.dataSources.add(aggregationSource)
|
}
|
clearAggregationEntities()
|
aggregationSource.show = clusterVisible.value
|
const countMap = new Map()
|
; (list || []).forEach(item => {
|
if (!item?.type) return
|
countMap.set(item.type, Number(item.count ?? 0))
|
})
|
Array.from(countyCenterMap.entries()).forEach(([name, center], index) => {
|
const position = Cesium.Cartesian3.fromDegrees(center.longitude, center.latitude, 0)
|
const count = countMap.get(name) ?? 0
|
aggregationSource.entities.add({
|
id: `aggregation-${name}-${index}`,
|
position,
|
billboard: {
|
image: aggregationIcon,
|
width: 66.73,
|
height: 43,
|
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
// 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 = selectedDeviceBillboard.position
|
if (!cartesian) return
|
const screenPosition = viewer.scene.cartesianToCanvasCoordinates(cartesian)
|
if (!screenPosition) return
|
popupPosition.value = { x: screenPosition.x, y: screenPosition.y }
|
}
|
|
const startPopupRender = () => {
|
if (!viewer || popupRenderHandler) return
|
popupRenderHandler = () => updatePopupPosition()
|
viewer.scene.postRender.addEventListener(popupRenderHandler)
|
updatePopupPosition()
|
}
|
|
const stopPopupRender = () => {
|
if (!viewer || !popupRenderHandler) return
|
viewer.scene.postRender.removeEventListener(popupRenderHandler)
|
popupRenderHandler = null
|
}
|
|
const handleDeviceClick = movement => {
|
if (!viewer) return
|
const picks = viewer.scene.drillPick(movement.position) || []
|
const pickedTarget = getPickedTarget(picks)
|
if (!pickedTarget) {
|
closePopup()
|
return
|
}
|
selectedTargetType.value = pickedTarget.type
|
selectedDevice.value = pickedTarget.data
|
selectedDeviceBillboard = pickedTarget.billboard
|
startPopupRender()
|
}
|
|
const initDeviceClickHandler = () => {
|
if (deviceClickHandler || !viewer) return
|
deviceClickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
|
deviceClickHandler.setInputAction(handleDeviceClick, Cesium.ScreenSpaceEventType.LEFT_CLICK)
|
}
|
|
const destroyDeviceClickHandler = () => {
|
deviceClickHandler?.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK)
|
deviceClickHandler?.destroy()
|
deviceClickHandler = null
|
}
|
|
function closePopup () {
|
selectedDevice.value = null
|
selectedDeviceBillboard = null
|
selectedTargetType.value = 'device'
|
stopPopupRender()
|
}
|
|
const popupPosition = ref({ x: 0, y: 0 })
|
const popupVisible = computed(() => Boolean(selectedDevice.value))
|
const isDronePopup = computed(() => selectedTargetType.value === 'drone')
|
const toggleDroneFavorite = () => {
|
if (!selectedDevice.value) return
|
selectedDevice.value.isFavorite = !selectedDevice.value.isFavorite
|
}
|
const handleDroneSignal = () => emit('droneSignal', selectedDevice.value)
|
const handleDroneCounter = () => emit('droneCounter', selectedDevice.value)
|
|
const renderCommandPosts = list => {
|
if (!viewer) return
|
ensureCommandPostCollection()
|
commandPostBillboardCollection.removeAll()
|
commandPostBillboardCollection.show = detailVisible.value
|
; (list || []).forEach((item, index) => {
|
const position = getDevicePosition(item)
|
if (!position) return
|
commandPostBillboardCollection.add({
|
position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
|
image: commandPostIcon,
|
width: 40,
|
height: 56,
|
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
})
|
})
|
reorderCockpitPrimitives()
|
}
|
|
const loadAggregation = async () => {
|
try {
|
const res = await cockpitAggregationApi({
|
effectiveRangeKmIsNotNull: 1
|
})
|
renderAggregation(res?.data?.data ?? [])
|
} catch (error) {
|
renderAggregation([])
|
}
|
}
|
|
const loadCommandPosts = async () => {
|
try {
|
const res = await fwDefenseSceneListApi()
|
renderCommandPosts(res?.data?.data ?? [])
|
} catch (error) {
|
renderCommandPosts([])
|
}
|
}
|
|
|
watch(
|
() => props.onlineDevices,
|
devices => {
|
renderDeviceEntities(devices || [])
|
},
|
{ deep: true }
|
)
|
|
watch(
|
() => props.leftCollapsed,
|
isCollapsed => {
|
if (isCollapsed) showLayerPanel.value = false
|
}
|
)
|
|
const toggleLayerPanel = () => {
|
if (!props.showLayerControl) return
|
showLayerPanel.value = !showLayerPanel.value
|
}
|
|
const updateStageDisplay = stage => {
|
const showCluster = stage === 'cluster'
|
setClusterVisibility(showCluster)
|
setDetailVisibility(!showCluster)
|
setDroneVisibility(!showCluster)
|
}
|
|
const handleMapReady = ({ viewer: mapViewer }) => {
|
viewer = mapViewer
|
ensureCockpitPrimitiveLayer()
|
const height = viewer?.camera?.positionCartographic?.height
|
const stage = getStageByHeight(height)
|
updateStageDisplay(stage)
|
renderDeviceEntities(props.onlineDevices)
|
loadDefenseZones()
|
loadPartitions()
|
loadAggregation()
|
loadCommandPosts()
|
renderSimulatedDroneTrack()
|
initDeviceClickHandler()
|
}
|
|
const handleStageChange = stage => {
|
updateStageDisplay(stage)
|
}
|
|
const handleClickOutside = event => {
|
if (!showLayerPanel.value) return
|
const target = event.target
|
if (layerWrapRef.value?.contains(target)) return
|
showLayerPanel.value = false
|
}
|
|
onMounted(() => {
|
document.addEventListener('click', handleClickOutside)
|
const map = mapRef.value?.getMap()
|
if (map?.viewer) handleMapReady(map)
|
})
|
|
onBeforeUnmount(() => {
|
document.removeEventListener('click', handleClickOutside)
|
clearDeviceEntities()
|
clearDefenseZoneEntities()
|
clearPartitionEntities()
|
clearAggregationEntities()
|
clearCommandPostEntities()
|
clearDroneTrackEntities()
|
closePopup()
|
destroyDeviceClickHandler()
|
if (cockpitPrimitiveLayer && viewer?.scene?.primitives) {
|
viewer.scene.primitives.remove(cockpitPrimitiveLayer)
|
}
|
cockpitPrimitiveLayer = null
|
viewer = null
|
})
|
</script>
|
|
<style lang="scss" scoped>
|
.map-shell {
|
position: relative;
|
width: 100%;
|
height: 100%;
|
}
|
|
.map-container {
|
position: absolute;
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 100%;
|
}
|
|
.layer-control-root {
|
position: absolute;
|
left: 337px;
|
bottom: 22px;
|
z-index: 9;
|
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;
|
left: 66px;
|
bottom: 0;
|
width: 160px;
|
max-height: 442px;
|
background: #191932;
|
border-radius: 8px 8px 8px 8px;
|
|
.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;
|
}
|
}
|
</style>
|