<template>
|
<div class="command-cesium map-container" :id="props.containerId"></div>
|
<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>
|
</template>
|
|
<script setup>
|
import * as Cesium from 'cesium'
|
import { PublicCesium } from '@/utils/cesium/publicCesium'
|
import { geomAnalysis } from '@ztzf/utils'
|
import { fwDefenseZonePageApi } from '@/views/areaManage/defenseZone/defenseZoneApi'
|
import { fwAreaDividePageApi } from '@/views/areaManage/partition/partitionApi'
|
import jaGeojsonRaw from '@/assets/geojson/ja.geojson?raw'
|
import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png'
|
import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
|
|
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
|
|
let viewInstance = null
|
let viewer = null
|
const deviceEntityIds = new Set()
|
let defenseZoneSource = null
|
let partitionSource = null
|
let adminBoundarySource = null
|
let adminBoundaryLabelSource = null
|
let adminBoundaryLineSource = null
|
let adminBoundaryFlyDone = false
|
const showLayerPanel = ref(false)
|
const layerWrapRef = ref(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 clearDeviceEntities = () => {
|
if (!viewer) return
|
deviceEntityIds.forEach(id => {
|
viewer.entities.removeById(id)
|
})
|
deviceEntityIds.clear()
|
}
|
|
const clearDefenseZoneEntities = () => {
|
if (!defenseZoneSource) return
|
defenseZoneSource.entities.removeAll()
|
}
|
|
const clearPartitionEntities = () => {
|
if (!partitionSource) return
|
partitionSource.entities.removeAll()
|
}
|
|
const clearAdminBoundaryEntities = () => {
|
if (!adminBoundarySource) return
|
adminBoundarySource.entities.removeAll()
|
}
|
|
const clearAdminBoundaryLabels = () => {
|
if (!adminBoundaryLabelSource) return
|
adminBoundaryLabelSource.entities.removeAll()
|
}
|
|
const clearAdminBoundaryLines = () => {
|
if (!adminBoundaryLineSource) return
|
adminBoundaryLineSource.entities.removeAll()
|
}
|
|
const RING_STYLES = [
|
{ inner: 0, outer: 2000, gradient: ['#FF361C', '#360B00'] }
|
]
|
|
const MATERIAL_TYPE = 'RadialGradientMaterial'
|
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, baseId) => {
|
RING_STYLES.forEach((ring, index) => {
|
const outerPositions = buildCirclePositions(center, ring.outer)
|
const holes = ring.inner
|
? [new Cesium.PolygonHierarchy(buildCirclePositions(center, ring.inner))]
|
: []
|
const entityId = `${baseId}-ring-${index}`
|
deviceEntityIds.add(entityId)
|
registerRadialGradientMaterial()
|
const [startColor, endColor] = ring.gradient
|
const color1 = Cesium.Color.fromCssColorString(startColor).withAlpha(0.34)
|
const color2 = Cesium.Color.fromCssColorString(endColor).withAlpha(0.34)
|
const material = new RadialGradientMaterialProperty(color1, color2)
|
viewer.entities.add({
|
id: entityId,
|
polygon: {
|
hierarchy: new Cesium.PolygonHierarchy(outerPositions, holes),
|
material,
|
},
|
})
|
})
|
}
|
|
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
|
clearDeviceEntities()
|
devices.forEach((item, index) => {
|
const position = getDevicePosition(item)
|
if (!position) return
|
const entityId = `online-device-${item.id ?? index}`
|
deviceEntityIds.add(entityId)
|
addDeviceRings(position, entityId)
|
viewer.entities.add({
|
id: entityId,
|
position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0),
|
billboard: {
|
image: equipmentIcon,
|
width: 40.34,
|
height: 40.34,
|
},
|
})
|
})
|
}
|
|
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 renderZonePolygons = ({ zones, source, idPrefix, lineColor, fillGradient }) => {
|
if (!viewer || !source) return
|
registerRadialGradientMaterial()
|
const borderColor = Cesium.Color.fromCssColorString(lineColor)
|
const fillColorStart = Cesium.Color.fromCssColorString(fillGradient[0]).withAlpha(0.34)
|
const fillColorEnd = Cesium.Color.fromCssColorString(fillGradient[1]).withAlpha(0.34)
|
const fillMaterial = new RadialGradientMaterialProperty(fillColorStart, fillColorEnd)
|
zones.forEach((zone, index) => {
|
if (!zone?.geom) return
|
const positions = getDefenseZonePositions(zone.geom)
|
if (!positions.length) return
|
const entityId = `${idPrefix}-${zone.id ?? index}`
|
const linePositions = positions.length > 1 ? [...positions, positions[0]] : positions
|
source.entities.add({
|
id: entityId,
|
polygon: {
|
hierarchy: new Cesium.PolygonHierarchy(positions),
|
material: fillMaterial,
|
outline: true,
|
outlineColor: borderColor,
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
},
|
polyline: {
|
positions: linePositions,
|
clampToGround: true,
|
width: 2,
|
material: borderColor,
|
},
|
})
|
})
|
}
|
|
const renderDefenseZones = zones => {
|
if (!viewer) return
|
if (!defenseZoneSource) {
|
defenseZoneSource = new Cesium.CustomDataSource('defenseZoneSource')
|
viewer.dataSources.add(defenseZoneSource)
|
}
|
clearDefenseZoneEntities()
|
renderZonePolygons({
|
zones,
|
source: defenseZoneSource,
|
idPrefix: 'defense-zone',
|
lineColor: '#19D266',
|
fillGradient: ['#2AEDBF', '#012B11'],
|
})
|
}
|
|
const renderPartitions = zones => {
|
if (!viewer) return
|
if (!partitionSource) {
|
partitionSource = new Cesium.CustomDataSource('partitionSource')
|
viewer.dataSources.add(partitionSource)
|
}
|
clearPartitionEntities()
|
renderZonePolygons({
|
zones,
|
source: partitionSource,
|
idPrefix: 'partition-zone',
|
lineColor: '#FFCD2A',
|
fillGradient: ['#FFC609', '#583300'],
|
})
|
}
|
|
const 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 loadAdminBoundary = async () => {
|
if (!viewer) return
|
try {
|
if (adminBoundarySource) {
|
viewer.dataSources.remove(adminBoundarySource)
|
adminBoundarySource = null
|
}
|
if (adminBoundaryLabelSource) {
|
viewer.dataSources.remove(adminBoundaryLabelSource)
|
adminBoundaryLabelSource = null
|
}
|
if (adminBoundaryLineSource) {
|
viewer.dataSources.remove(adminBoundaryLineSource)
|
adminBoundaryLineSource = null
|
}
|
const geojson = JSON.parse(jaGeojsonRaw)
|
adminBoundarySource = await Cesium.GeoJsonDataSource.load(geojson, {
|
stroke: Cesium.Color.fromCssColorString('#FFFFFF'),
|
strokeWidth: 2,
|
fill: Cesium.Color.fromCssColorString('#FFFFFF').withAlpha(0.05),
|
clampToGround: true,
|
})
|
viewer.dataSources.add(adminBoundarySource)
|
if (!adminBoundaryFlyDone) {
|
adminBoundaryFlyDone = true
|
viewer.flyTo(adminBoundarySource, {
|
duration: 0,
|
offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-75), 0),
|
})
|
}
|
|
adminBoundaryLineSource = new Cesium.CustomDataSource('adminBoundaryLineSource')
|
adminBoundarySource.entities.values.forEach(entity => {
|
const polygon = entity.polygon
|
if (!polygon) return
|
const hierarchy = polygon.hierarchy?.getValue?.(viewer.clock.currentTime)
|
const positions = hierarchy?.positions
|
if (!positions?.length) return
|
adminBoundaryLineSource.entities.add({
|
polyline: {
|
positions,
|
clampToGround: true,
|
width: 2,
|
material: Cesium.Color.WHITE,
|
},
|
})
|
})
|
viewer.dataSources.add(adminBoundaryLineSource)
|
|
adminBoundaryLabelSource = new Cesium.CustomDataSource('adminBoundaryLabelSource')
|
geojson.features?.forEach(feature => {
|
const name = feature?.properties?.name
|
const point = feature?.properties?.centroid || feature?.properties?.center
|
if (!name || !Array.isArray(point) || point.length < 2) return
|
adminBoundaryLabelSource.entities.add({
|
position: Cesium.Cartesian3.fromDegrees(point[0], point[1]),
|
label: {
|
text: name,
|
font: '14px Source Han Sans CN',
|
fillColor: Cesium.Color.WHITE,
|
outlineColor: Cesium.Color.BLACK.withAlpha(0.6),
|
outlineWidth: 2,
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
},
|
})
|
})
|
viewer.dataSources.add(adminBoundaryLabelSource)
|
} catch (error) {
|
clearAdminBoundaryEntities()
|
clearAdminBoundaryLabels()
|
clearAdminBoundaryLines()
|
}
|
}
|
|
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 handleClickOutside = event => {
|
if (!showLayerPanel.value) return
|
const target = event.target
|
if (layerWrapRef.value?.contains(target)) return
|
showLayerPanel.value = false
|
}
|
|
onMounted(() => {
|
document.addEventListener('click', handleClickOutside)
|
viewInstance = new PublicCesium({
|
dom: props.containerId,
|
flatMode: false,
|
terrain: false,
|
layerMode: 4,
|
contour: false,
|
})
|
|
viewer = viewInstance.getViewer()
|
renderDeviceEntities(props.onlineDevices)
|
loadDefenseZones()
|
loadPartitions()
|
loadAdminBoundary()
|
})
|
|
onBeforeUnmount(() => {
|
document.removeEventListener('click', handleClickOutside)
|
clearDeviceEntities()
|
clearDefenseZoneEntities()
|
clearPartitionEntities()
|
clearAdminBoundaryEntities()
|
clearAdminBoundaryLabels()
|
clearAdminBoundaryLines()
|
if (viewInstance) {
|
viewInstance?.viewerDestroy()
|
viewInstance = null
|
}
|
|
viewer = null
|
})
|
</script>
|
|
<style lang="scss" scoped>
|
.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;
|
}
|
|
::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>
|