<template>
|
<div class="map-shell">
|
<CommonCesiumMap ref="mapRef" class="command-cesium map-container" :dom-id="props.containerId" :active="true"
|
:flat-mode="false" :terrain="true" :layer-mode="4" :contour="false" :boundary="false"
|
:show-admin-boundary="true" :zoom-to-boundary="true" :enable-stage-emit="true" :cluster-height="100000"
|
@ready="handleMapReady" @stage-change="handleStageChange" />
|
<div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.leftCollapsed }">
|
<div class="layer-control-wrap" ref="layerWrapRef">
|
<div class="layer-control" @click="toggleLayerPanel">
|
<img :src="layerControlIcon" alt="图层控制" />
|
</div>
|
<div v-if="showLayerPanel" class="layer-panel">
|
<div class="panel-title">图层管理</div>
|
|
<div class="panel-content">
|
<el-tree :data="layerTree" show-checkbox default-expand-all node-key="key"
|
:props="layerTreeProps" :default-checked-keys="defaultCheckedKeys" />
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div v-if="popupVisible" class="device-popup" :style="popupStyle" @click.stop>
|
<div class="device-popup__header">
|
<el-tooltip
|
class="device-popup__title-tooltip"
|
:content="popupTitle"
|
placement="top"
|
effect="dark"
|
:show-after="200"
|
>
|
<span class="device-popup__title">{{ popupTitleDisplay }}</span>
|
</el-tooltip>
|
|
<img class="device-popup__close" :src="popupClose" alt="" @click.stop="closePopup">
|
</div>
|
<div class="device-popup__type">
|
<span class="value">{{ popupTypeText }}</span>
|
</div>
|
|
<div class="device-popup__subtitle">
|
<span class="device-popup__dot" :class="popupActionClass"></span>
|
<span class="device-popup__action">{{ popupActionText }}</span>
|
</div>
|
|
<div class="device-popup__rows">
|
<div class="device-popup__row">
|
<div class="device-popup__col">
|
<span class="label">状态</span>
|
<span class="value" :class="popupStatusClass">{{ popupStatusText }}</span>
|
</div>
|
|
<span class="divider">|</span>
|
|
<div class="device-popup__col">
|
<span class="label">电量</span>
|
<span class="value">{{ popupBatteryText }}</span>
|
</div>
|
</div>
|
<div class="device-popup__row">
|
<div class="device-popup__col">
|
<span class="label">方位角</span>
|
<span class="value">{{ popupAzimuthText }}</span>
|
</div>
|
|
<span class="divider">|</span>
|
|
<div class="device-popup__col">
|
<span class="label">俯仰角</span>
|
<span class="value">{{ popupElevationText }}</span>
|
</div>
|
</div>
|
<div class="device-popup__row">
|
<span class="label">侦测目标</span>
|
<span class="value">{{ popupDetectCountText }}</span>
|
</div>
|
<div class="device-popup__row">
|
<span class="label">有效范围</span>
|
<span class="value">{{ popupRangeText }}</span>
|
</div>
|
</div>
|
</div>
|
</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 popupClose from '@/assets/images/dataCockpit/map/popup-close.png'
|
import jaGeojsonRaw from '@/assets/geojson/ja.geojson?raw'
|
|
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 DEFAULT_ZONE_PAGE_SIZE = 999
|
|
const mapRef = ref(null)
|
let viewer = null
|
let cockpitPrimitiveLayer = null
|
const devicePickMap = 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)
|
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 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 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 = base => {
|
ensureCountyCenterMap()
|
const anchor = base || { longitude: 116.397, latitude: 39.908 }
|
const offsets = [
|
[0, 0, 120],
|
[0.02, 0.012, 150],
|
[0.04, 0.006, 180],
|
[0.06, -0.008, 200],
|
[0.03, -0.02, 160],
|
[0.0, -0.015, 140],
|
[-0.02, -0.005, 120],
|
[-0.01, 0.01, 130],
|
]
|
return offsets.map(item => ({
|
longitude: anchor.longitude + item[0],
|
latitude: anchor.latitude + item[1],
|
height: item[2],
|
}))
|
}
|
|
|
const clearDroneTrackEntities = () => {
|
if (!viewer) return
|
if (droneTrackBillboardCollection) {
|
droneTrackBillboardCollection.removeAll()
|
removeCockpitPrimitive(droneTrackBillboardCollection)
|
droneTrackBillboardCollection = null
|
}
|
if (droneTrackPolylineCollection) {
|
droneTrackPolylineCollection.removeAll()
|
removeCockpitPrimitive(droneTrackPolylineCollection)
|
droneTrackPolylineCollection = null
|
}
|
droneTrackRuntime = []
|
stopDroneTrackAnimation()
|
}
|
|
|
const ensureDroneTrackCollections = () => {
|
if (!viewer) return
|
ensureCockpitPrimitiveLayer()
|
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 (!droneTrackRuntime.length) return
|
const current = clock.currentTime
|
const elapsed = Cesium.JulianDate.secondsDifference(current, droneTrackStartTime)
|
droneTrackRuntime.forEach(track => {
|
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
|
ensureCountyCenterMap()
|
ensureDroneTrackCollections()
|
droneTrackBillboardCollection.removeAll()
|
droneTrackPolylineCollection.removeAll()
|
droneTrackRuntime = []
|
const centers = Array.from(countyCenterMap.values())
|
const baseCenters = centers.length ? centers.slice(0, 4) : [{ longitude: 116.397, latitude: 39.908 }]
|
const segmentDuration = 6
|
baseCenters.forEach((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)
|
)
|
droneTrackPolylineCollection.add({
|
positions,
|
width: 2,
|
material: Cesium.Material.fromType('Color', {
|
color: Cesium.Color.fromCssColorString('#2AEDBF'),
|
}),
|
})
|
const billboard = droneTrackBillboardCollection.add({
|
position: positions[0],
|
image: droneIcon,
|
width: 36,
|
height: 36,
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
})
|
droneTrackRuntime.push({
|
positions,
|
billboard,
|
segmentDuration,
|
duration: (points.length - 1) * segmentDuration,
|
})
|
})
|
viewer.clock.multiplier = 1
|
startDroneTrackAnimation()
|
reorderCockpitPrimitives()
|
}
|
|
const RING_STYLES = [
|
{ inner: 0, outer: 2000, gradient: ['#FF361C', '#360B00'] }
|
]
|
|
const MATERIAL_TYPE = 'RadialGradientMaterial'
|
const TEXTURED_VERTEX_FORMAT =
|
Cesium.MaterialAppearance?.MaterialSupport?.TEXTURED?.vertexFormat || Cesium.VertexFormat.POSITION_AND_ST
|
let materialRegistered = false
|
|
const registerRadialGradientMaterial = () => {
|
if (materialRegistered || !Cesium?.Material) return
|
materialRegistered = true
|
Cesium.Material._materialCache.addMaterial(MATERIAL_TYPE, {
|
fabric: {
|
type: MATERIAL_TYPE,
|
uniforms: {
|
color1: new Cesium.Color(1.0, 1.0, 1.0, 1.0),
|
color2: new Cesium.Color(0.0, 0.0, 0.0, 1.0),
|
},
|
source: `
|
czm_material czm_getMaterial(czm_materialInput materialInput) {
|
czm_material material = czm_getDefaultMaterial(materialInput);
|
vec2 st = materialInput.st - vec2(0.5);
|
float t = clamp(length(st) * 2.0, 0.0, 1.0);
|
vec4 color = mix(color1, color2, t);
|
material.diffuse = color.rgb;
|
material.alpha = color.a;
|
return material;
|
}
|
`,
|
},
|
translucent: () => true,
|
})
|
}
|
|
const buildCirclePositions = (center, radiusMeters, steps = 64) => {
|
const positions = []
|
const latRad = Cesium.Math.toRadians(center.latitude)
|
const metersToLat = 1 / 111320
|
const metersToLon = 1 / (111320 * Math.cos(latRad))
|
for (let i = 0; i <= steps; i += 1) {
|
const angle = Cesium.Math.toRadians((i / steps) * 360)
|
const dx = radiusMeters * Math.cos(angle)
|
const dy = radiusMeters * Math.sin(angle)
|
const lon = center.longitude + dx * metersToLon
|
const lat = center.latitude + dy * metersToLat
|
positions.push(Cesium.Cartesian3.fromDegrees(lon, lat))
|
}
|
return positions
|
}
|
|
const addDeviceRings = (center, ringFillInstancesByStyle, ringOutlineInstancesByStyle) => {
|
if (!ringFillInstancesByStyle) return
|
const centerPosition = Cesium.Cartesian3.fromDegrees(center.longitude, center.latitude, 0)
|
RING_STYLES.forEach((ring, index) => {
|
if (!ringFillInstancesByStyle[index]) return
|
ringFillInstancesByStyle[index].push(
|
new Cesium.GeometryInstance({
|
geometry: new Cesium.EllipseGeometry({
|
center: centerPosition,
|
semiMajorAxis: ring.outer,
|
semiMinorAxis: ring.outer,
|
vertexFormat: TEXTURED_VERTEX_FORMAT,
|
}),
|
})
|
)
|
})
|
if (!ringOutlineInstancesByStyle) return
|
RING_STYLES.forEach((ring, index) => {
|
if (!ringOutlineInstancesByStyle[index]) return
|
const positions = buildCirclePositions(center, ring.outer)
|
ringOutlineInstancesByStyle[index].push(
|
new Cesium.GeometryInstance({
|
geometry: new Cesium.GroundPolylineGeometry({
|
positions,
|
width: 2,
|
}),
|
attributes: {
|
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
|
Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(0.8)
|
),
|
},
|
})
|
)
|
})
|
}
|
|
class RadialGradientMaterialProperty {
|
constructor(color1, color2) {
|
this._definitionChanged = new Cesium.Event()
|
this.color1 = color1
|
this.color2 = color2
|
}
|
|
get isConstant () {
|
return true
|
}
|
|
get definitionChanged () {
|
return this._definitionChanged
|
}
|
|
getType () {
|
return MATERIAL_TYPE
|
}
|
|
getValue (time, result) {
|
const target = result || {}
|
target.color1 = this.color1
|
target.color2 = this.color2
|
return target
|
}
|
|
equals (other) {
|
return (
|
other instanceof RadialGradientMaterialProperty &&
|
Cesium.Color.equals(this.color1, other.color1) &&
|
Cesium.Color.equals(this.color2, other.color2)
|
)
|
}
|
}
|
|
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}`
|
addDeviceRings(position, ringFillInstancesByStyle, ringOutlineInstancesByStyle)
|
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 (selectedDeviceBillboard && !devicePickMap.has(selectedDeviceBillboard.id)) {
|
closePopup()
|
}
|
registerRadialGradientMaterial()
|
ringFillInstancesByStyle.forEach((instances, index) => {
|
if (!instances.length) return
|
const ring = RING_STYLES[index]
|
const material = Cesium.Material.fromType(MATERIAL_TYPE, {
|
color1: Cesium.Color.fromCssColorString(ring.gradient[0]).withAlpha(0.64),
|
color2: Cesium.Color.fromCssColorString(ring.gradient[1]).withAlpha(0.64),
|
})
|
const primitive = new Cesium.GroundPrimitive({
|
geometryInstances: instances,
|
appearance: new Cesium.MaterialAppearance({
|
material,
|
translucent: true,
|
}),
|
})
|
primitive.show = detailVisible.value
|
addCockpitPrimitive(primitive)
|
deviceRingFillPrimitives.push(primitive)
|
})
|
ringOutlineInstancesByStyle.forEach(instances => {
|
if (!instances.length) return
|
const primitive = new Cesium.GroundPolylinePrimitive({
|
geometryInstances: instances,
|
appearance: new Cesium.PolylineColorAppearance(),
|
})
|
primitive.show = detailVisible.value
|
addCockpitPrimitive(primitive)
|
deviceRingOutlinePrimitives.push(primitive)
|
})
|
reorderCockpitPrimitives()
|
}
|
|
const getDefenseZonePositions = geom => {
|
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 = []
|
registerRadialGradientMaterial()
|
; (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: TEXTURED_VERTEX_FORMAT,
|
})
|
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 = Cesium.Material.fromType(MATERIAL_TYPE, {
|
color1: baseColor1.withAlpha(0.64),
|
color2: 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, '#19D266', '#2AEDBF', '#012B11')
|
defenseZonePrimitive = result.primitive
|
defenseZoneOutlinePrimitive = result.outlinePrimitive
|
if (defenseZonePrimitive) defenseZonePrimitive.show = detailVisible.value
|
if (defenseZoneOutlinePrimitive) defenseZoneOutlinePrimitive.show = detailVisible.value
|
reorderCockpitPrimitives()
|
}
|
|
const renderPartitions = zones => {
|
if (!viewer) return
|
clearPartitionEntities()
|
const result = buildZonePrimitives(zones, '#FFCD2A', '#FFC609', '#583300')
|
partitionPrimitive = result.primitive
|
partitionOutlinePrimitive = result.outlinePrimitive
|
if (partitionPrimitive) partitionPrimitive.show = detailVisible.value
|
if (partitionOutlinePrimitive) partitionOutlinePrimitive.show = detailVisible.value
|
reorderCockpitPrimitives()
|
}
|
|
const loadDefenseZones = async () => {
|
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,
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
},
|
label: {
|
text: `${count}`,
|
fillColor: Cesium.Color.WHITE,
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
font: '11pt Source Han Sans CN',
|
eyeOffset: new Cesium.Cartesian3(0, 0, -20), // 让label "浮" 在广告牌前面
|
|
pixelOffset: new Cesium.Cartesian2(0, -35),
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
},
|
})
|
})
|
}
|
|
const getPickedDevice = picks => {
|
for (const pick of picks) {
|
const pickId = pick?.id
|
const resolvedId = typeof pickId === 'string' ? pickId : pickId?.id
|
if (resolvedId && devicePickMap.has(resolvedId)) {
|
return devicePickMap.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 pickedDevice = getPickedDevice(picks)
|
if (!pickedDevice) {
|
closePopup()
|
return
|
}
|
selectedDevice.value = pickedDevice.data
|
selectedDeviceBillboard = pickedDevice.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
|
stopPopupRender()
|
}
|
|
const popupPosition = ref({ x: 0, y: 0 })
|
const popupVisible = computed(() => Boolean(selectedDevice.value))
|
const popupStyle = computed(() => ({
|
left: `${popupPosition.value.x}px`,
|
top: `${popupPosition.value.y}px`,
|
}))
|
|
const POPUP_TITLE_MAX = 12
|
const popupTitle = computed(() => selectedDevice.value?.deviceName || selectedDevice.value?.deviceModel || '-')
|
const popupTitleOverflow = computed(() => popupTitle.value.length > POPUP_TITLE_MAX)
|
const popupTitleDisplay = computed(() => {
|
if (!popupTitleOverflow.value) return popupTitle.value
|
return `${popupTitle.value.slice(0, POPUP_TITLE_MAX)}……`
|
})
|
const popupStatusText = computed(() => {
|
const status = selectedDevice.value?.status
|
if (status === 0) return '在线'
|
if (status === 1) return '离线'
|
if (status === 2) return '故障'
|
if (status === 3) return '报废'
|
return '-'
|
})
|
const popupStatusClass = computed(() => (selectedDevice.value?.status === 0 ? 'online' : ''))
|
const popupActionText = computed(() => {
|
const workMode = selectedDevice.value?.workMode
|
if (workMode === 1 || workMode === '1') return '侦测中'
|
if (workMode === 2 || workMode === '2') return '信号干扰中'
|
if (workMode === 3 || workMode === '3') return '诱导驱离中'
|
if (workMode === 4 || workMode === '4') return '待机'
|
return workMode || '-'
|
})
|
const popupActionClass = computed(() => {
|
const workMode = selectedDevice.value?.workMode
|
if (workMode === 1 || workMode === '1') return 'is-detecting'
|
if (workMode === 2 || workMode === '2') return 'is-jamming'
|
if (workMode === 3 || workMode === '3') return 'is-driving'
|
if (workMode === 4 || workMode === '4') return 'is-standby'
|
return 'is-standby'
|
})
|
const popupTypeText = computed(() => {
|
const type = selectedDevice.value?.deviceType
|
if (type === 1 || type === '1') return '便捷侦测箱'
|
if (type === 2 || type === '2') return '反制枪'
|
if (type === 3 || type === '3') return '察打一体'
|
return type || '-'
|
})
|
const popupBatteryText = computed(() => {
|
const val = selectedDevice.value?.batteryPct
|
if (val == null) return '-'
|
return `${val}%`
|
})
|
const popupAzimuthText = computed(() => {
|
const val = selectedDevice.value?.azimuth
|
if (val == null) return '-'
|
return `${val}°`
|
})
|
const popupElevationText = computed(() => {
|
const val = selectedDevice.value?.elevation
|
if (val == null) return '-'
|
return `${val}°`
|
})
|
const popupDetectCountText = computed(() => {
|
const val = selectedDevice.value?.detectTargetCnt
|
if (val == null) return '-'
|
return `${val}台`
|
})
|
const popupRangeText = computed(() => {
|
const val = selectedDevice.value?.effectiveRangeKm
|
if (val == null) return '-'
|
return `${val}KM`
|
})
|
|
const renderCommandPosts = list => {
|
if (!viewer) return
|
ensureCommandPostCollection()
|
commandPostBillboardCollection.removeAll()
|
commandPostBillboardCollection.show = detailVisible.value
|
; (list || []).forEach((item, index) => {
|
const position = getDevicePosition(item)
|
if (!position) return
|
commandPostBillboardCollection.add({
|
position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
|
image: commandPostIcon,
|
width: 40,
|
height: 56,
|
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
})
|
})
|
reorderCockpitPrimitives()
|
}
|
|
const loadAggregation = async () => {
|
try {
|
const res = await cockpitAggregationApi()
|
renderAggregation(res?.data?.data ?? [])
|
} catch (error) {
|
renderAggregation([])
|
}
|
}
|
|
const loadCommandPosts = async () => {
|
try {
|
const res = await fwDefenseSceneListApi()
|
renderCommandPosts(res?.data?.data ?? [])
|
} catch (error) {
|
renderCommandPosts([])
|
}
|
}
|
|
|
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)
|
}
|
|
const handleMapReady = ({ viewer: mapViewer }) => {
|
viewer = mapViewer
|
ensureCockpitPrimitiveLayer()
|
const height = viewer?.camera?.positionCartographic?.height
|
const stage = height != null && height >= 100000 ? 'cluster' : 'detail'
|
updateStageDisplay(stage)
|
renderDeviceEntities(props.onlineDevices)
|
loadDefenseZones()
|
loadPartitions()
|
loadAggregation()
|
loadCommandPosts()
|
renderSimulatedDroneTrack()
|
initDeviceClickHandler()
|
}
|
|
const handleStageChange = stage => {
|
updateStageDisplay(stage)
|
}
|
|
const handleClickOutside = event => {
|
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%;
|
}
|
|
.device-popup {
|
position: absolute;
|
transform: translate(-50%, calc(-100% - 72px));
|
width: 202px;
|
padding: 0 16px 20px;
|
border-radius: 16px;
|
background: rgba(5, 5, 15, 0.7);
|
backdrop-filter: blur(16px);
|
color: #C3C3DD;
|
font-family: Source Han Sans CN, Source Han Sans CN;
|
font-size: 12px;
|
pointer-events: auto;
|
z-index: 11;
|
box-sizing: border-box;
|
|
&::after {
|
content: "";
|
position: absolute;
|
left: 50%;
|
bottom: -8px;
|
transform: translateX(-50%);
|
width: 0;
|
height: 0;
|
border-left: 8px solid transparent;
|
border-right: 8px solid transparent;
|
border-top: 8px solid rgba(5, 5, 15, 0.7);
|
}
|
|
.device-popup__header {
|
height: 40px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
position: relative;
|
|
font-weight: bold;
|
font-size: 14px;
|
color: #FFFFFF;
|
|
&::after {
|
content: "";
|
position: absolute;
|
left: 0;
|
bottom: 0;
|
width: 50px;
|
height: 2px;
|
background: #284FE3;
|
box-shadow: 0px 3px 2px 0px rgba(15, 89, 255, 0.1), 0px 7px 5px 0px rgba(15, 89, 255, 0.15), 0px 13px 10px 0px rgba(15, 89, 255, 0.18), 0px 22px 18px 0px rgba(15, 89, 255, 0.21), 0px 42px 33px 0px rgba(15, 89, 255, 0.26), 0px 100px 80px 0px rgba(15, 89, 255, 0.36);
|
border-radius: 5px 5px 0px 0px;
|
}
|
|
&::before {
|
content: "";
|
position: absolute;
|
left: 55px;
|
right: 0;
|
bottom: 0;
|
height: 2px;
|
background: #323245;
|
border-radius: 0px 0px 5px 5px;
|
}
|
|
.device-popup__title-tooltip {
|
flex: 1;
|
min-width: 0;
|
display: block;
|
}
|
|
.device-popup__title {
|
display: inline-block;
|
max-width: 100%;
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
}
|
|
.device-popup__close {
|
cursor: pointer;
|
}
|
}
|
|
.device-popup__type {
|
display: flex;
|
align-items: center;
|
gap: 6px;
|
font-size: 12px;
|
margin-top: 8px;
|
margin-bottom: 10px;
|
color: #d2d7df;
|
|
.label {
|
color: #9aa4b2;
|
}
|
}
|
|
.device-popup__subtitle {
|
display: inline-flex;
|
align-items: center;
|
gap: 6px;
|
padding: 2px 10px;
|
border-radius: 999px;
|
background: rgba(255, 255, 255, 0.06);
|
font-size: 12px;
|
margin-bottom: 10px;
|
|
.device-popup__dot {
|
width: 6px;
|
height: 6px;
|
border-radius: 50%;
|
background: #6b7685;
|
|
&.is-detecting {
|
background: #35f08c;
|
box-shadow: 0 0 6px rgba(53, 240, 140, 0.8);
|
}
|
|
&.is-jamming {
|
background: #ff4444;
|
box-shadow: 0 0 6px rgba(255, 68, 68, 0.7);
|
}
|
|
&.is-driving {
|
background: #ffb020;
|
box-shadow: 0 0 6px rgba(255, 176, 32, 0.7);
|
}
|
|
&.is-standby {
|
background: #9aa4b2;
|
}
|
}
|
}
|
|
.device-popup__rows {
|
display: flex;
|
flex-direction: column;
|
gap: 6px;
|
font-size: 12px;
|
color: #d2d7df;
|
|
.device-popup__row {
|
display: flex;
|
align-items: center;
|
margin-bottom: 10px;
|
|
.device-popup__col {
|
flex: 1;
|
display: flex;
|
align-items: center;
|
|
&:last-child {
|
justify-content: flex-end;
|
}
|
}
|
}
|
|
.value {
|
margin-left: 10px;
|
}
|
|
.value.online {
|
color: #35f08c;
|
}
|
}
|
|
}
|
|
.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;
|
}
|
|
::v-deep(.el-tree) {
|
background: transparent;
|
color: #c3c3dd;
|
font-size: 12px;
|
|
.el-tree-node {
|
line-height: 30px !important;
|
|
.el-tree-node__content {
|
padding-left: 0 !important;
|
display: flex;
|
align-items: center;
|
height: 40px !important;
|
line-height: 40px !important;
|
border-bottom: 1px solid rgba(70, 70, 100, 0.5);
|
box-sizing: border-box;
|
}
|
|
.el-tree-node__children {
|
.el-tree-node__content {
|
border: none;
|
}
|
}
|
|
&:focus {
|
.el-tree-node__content {
|
background: transparent !important;
|
}
|
}
|
}
|
}
|
}
|
|
.layer-panel :deep(.el-tree-node__label) {
|
color: #c3c3dd;
|
}
|
|
.layer-panel :deep(.el-tree-node__expand-icon) {
|
order: 3;
|
margin-left: auto;
|
}
|
|
.layer-panel :deep(.el-tree-node__expand-icon.is-leaf) {
|
visibility: hidden;
|
}
|
|
.layer-panel :deep(.el-checkbox) {
|
order: 1;
|
}
|
|
.layer-panel :deep(.el-tree-node__label) {
|
order: 2;
|
}
|
|
.layer-panel :deep(.el-tree-node__expand-icon:not(.is-leaf) ~ .el-checkbox) {
|
display: none;
|
}
|
|
.layer-panel :deep(.el-tree-node__content:hover),
|
.layer-panel :deep(.el-tree-node__content:focus) {
|
background: transparent !important;
|
}
|
|
.layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
|
background: transparent !important;
|
color: #ffffff;
|
}
|
|
.layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content .el-tree-node__label) {
|
color: #ffffff;
|
}
|
|
.layer-panel :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
background-color: #023aff;
|
border-color: #023aff;
|
}
|
|
.layer-panel :deep(.el-checkbox__inner) {
|
background-color: transparent;
|
border-color: #a1a3d4;
|
}
|
</style>
|