| | |
| | | <template> |
| | | <div class="ztzf-cesium map-container" id="cesium"> |
| | | <div class="ztzf-cesium map-container" id="cesium"></div> |
| | | <div 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 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 |
| | | } |
| | | }) |
| | | |
| | | let viewInstance = null |
| | | let viewer = null |
| | | const deviceEntityIds = new Set() |
| | | 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 RING_STYLES = [ |
| | | { inner: 0, outer: 5000, gradient: ['#FF361C', '#360B00'] }, |
| | | { inner: 5000, outer: 8000, gradient: ['#FFC609', '#583300'] }, |
| | | { inner: 8000, outer: 10000, gradient: ['#2AEDBF', '#012B11'] } |
| | | ] |
| | | |
| | | 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 |
| | | }) |
| | | } |
| | | |
| | | 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 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 |
| | | } |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | const renderDeviceEntities = (devices) => { |
| | | console.log(devices, '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 |
| | | } |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | watch( |
| | | () => props.onlineDevices, |
| | | (devices) => { |
| | | renderDeviceEntities(devices || []) |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.leftCollapsed, |
| | | (isCollapsed) => { |
| | | if (isCollapsed) showLayerPanel.value = false |
| | | } |
| | | ) |
| | | |
| | | const toggleLayerPanel = () => { |
| | | 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: 'cesium', |
| | | flatMode: false, |
| | | terrain: false, |
| | | layerMode: 4, |
| | | contour: false, |
| | | contour: false |
| | | }) |
| | | |
| | | viewer = viewInstance.getViewer() |
| | | }) |
| | | renderDeviceEntities(props.onlineDevices) |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | document.removeEventListener('click', handleClickOutside) |
| | | clearDeviceEntities() |
| | | if (viewInstance) { |
| | | viewInstance?.destroy() |
| | | viewInstance?.viewerDestroy() |
| | | viewInstance = null |
| | | } |
| | | |
| | |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | </style> |
| | | |
| | | .layer-control-root { |
| | | position: absolute; |
| | | left: 411px; |
| | | bottom: 22px; |
| | | z-index: 9; |
| | | transition: transform 0.3s ease-in-out; |
| | | pointer-events: none; |
| | | |
| | | &.collapsed { |
| | | transform: translateX(-401px); |
| | | } |
| | | } |
| | | |
| | | .layer-control-wrap { |
| | | position: relative; |
| | | display: flex; |
| | | align-items: flex-end; |
| | | pointer-events: auto; |
| | | } |
| | | |
| | | .layer-control { |
| | | width: 34px; |
| | | height: 34px; |
| | | cursor: pointer; |
| | | |
| | | img { |
| | | width: 100%; |
| | | height: 100%; |
| | | display: block; |
| | | } |
| | | } |
| | | |
| | | .layer-panel { |
| | | display: flex; |
| | | flex-direction: column; |
| | | position: absolute; |
| | | left: 46px; |
| | | 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 { |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | .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) { |
| | | 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> |