import * as Cesium from 'cesium'; import type { MDOptions } from './map.class'; import type { MapInterface, layer } from './map.d'; import { getIconPath } from '@/utils/index'; import { generatePopupHtml, shouldPreferEng2Popup, getNormalizedPopupSttpMap } from '@/utils/popupHtmlGenerator'; import { useModelStore } from '@/store/modules/model'; import { getNearbyPointDensityDisplayRules } from '@/modules/map/domain/nearby-point-rules'; import { LoadOSGB, removeQxsy, osgbChangeClick, osgbLocation, type OSGBItem } from './osgbUtils'; export class MapCesium implements MapInterface { private viewer: Cesium.Viewer | null = null; private _ready = false; private _pendingOSGBItems: OSGBItem[] = []; private clickEventHandler: Cesium.ScreenSpaceEventHandler | null = null; private popupElement: HTMLElement | null = null; private hoveredEntityId: string | null = null; private hoverRafId: number | null = null; // requestAnimationFrame 节流 private removePostRenderListener: (() => void) | null = null; private removeCameraChangedListener: (() => void) | null = null; private isBatchPopupMode = false; private batchPopupContainer: HTMLDivElement | null = null; private batchPopupItems: Array<{ entity: Cesium.Entity; element: HTMLDivElement; popupWidth: number; popupHeight: number; }> = []; private batchPostRenderRemover: (() => void) | null = null; private containerId = ''; private containerElement: HTMLElement | null = null; private loadingOverlayElement: HTMLDivElement | null = null; private loadingSpinnerAnimation: Animation | null = null; private flightFallbackTimer: number | null = null; private imageryBaseLayer: Cesium.ImageryLayer | null = null; private baseLayerRegistry = new Map(); private baseLayerAliasMap = new Map(); private pointLayerRegistry = new Map(); private hydropBaseConfig: any = null; private BASEID = ''; private currentClipGeoJson: any = null; private clipRequestController: AbortController | null = null; private maskPolygonEntities: Cesium.Entity[] = []; private originalBgColor: Cesium.Color | null = null; private skyBoxShown = true; private skyAtmosphereShown = true; private readonly IGNORED_BASE_LAYER_KEYS = new Set([ 'powerBaseStation', 's_hydropBase_wfs' ]); private readonly PRIMARY_BASE_LAYER_KEYS = new Set([ 'customBaseLayer', 's_province_boundaries', 'BASEMAP-white', 'BASEMAP-img' ]); private readonly CHINA_CENTER = { lng: 104.5, lat: 36.5 }; private readonly CHINA_BOOT_HEIGHT = 20000000; private readonly CHINA_DEFAULT_HEIGHT = 9000000; private readonly CHINA_VIEW_ORIENTATION = { heading: Cesium.Math.toRadians(-6), pitch: Cesium.Math.toRadians(-90), roll: 0 }; private readonly CESIUM_ZOOM_FORMULA_A = 40487.57; private readonly CESIUM_ZOOM_FORMULA_B = 0.00007096758; private readonly CESIUM_ZOOM_FORMULA_C = 91610.74; private readonly CESIUM_ZOOM_FORMULA_D = -40467.74; private readonly HOVER_POPUP_MAX_HEIGHT = 9_000_000; // ~zoom 4.5,中国视角以上不触发 hover private readonly BATCH_POPUP_MODE_ZOOM = 14.9; // zoom >= 14.9 进入批量固定模式(float 容差避免 15 刚好踩边界) // ==================== Step 6: 碰撞检测 ==================== private collisionRefreshFrameId: number | null = null; private labelVisibilityDebounceTimerId: number | null = null; private readonly COLLISION_GRID_CELL_SIZE = 96; // 与 2D 一致 private readonly DENSITY_ISOLATION_THRESHOLD = 56; // 孤立点 pixel 阈值 private readonly COLLISION_LABEL_PADDING = 4; // 标签碰撞 padding // 图标缩放可调参数(只影响 3D Cesium,不参与 2D 计算) // scale = base + (zoom - 4.5) * rate,钳位到 [min, max] // zoom≈4.5 是中国全国视角,zoom≈15 是最大放大级别 private readonly ICON_MIN_SCALE = 0.4; // 最小缩放 private readonly ICON_MAX_SCALE = 1.2; // 最大缩放(限制放大后图标不超过 1.2x 基础值) private readonly ICON_BASE_SCALE = 0.7; // zoom≈4.5 基准(中国视角图标 ≈ 原始尺寸) private readonly ICON_SCALE_RATE = 0.035; // 每级 zoom 增长(放大 10 级才增加 ~0.35) // 标签文字可调参数 // fontSize = clamp(base * scale, min, max),scale = 0.7 + (zoom-4.5)*0.08 private readonly LABEL_FONT_MIN = 10; // 最小字号(px),改大 → 小缩放时文字更清晰 private readonly LABEL_FONT_MAX = 20; // 最大字号(px) private readonly LABEL_FONT_BASE = 16; // 基准乘数,改大 → 所有缩放级别文字都变大 private readonly LABEL_OFFSET_SINGLE = 14; // 单行偏移系数,改大 → 文字离图标更远 private readonly LABEL_OFFSET_MULTI = 30; // 多行偏移系数 // ==================== Step 1: 初始化 + 飞中国 ==================== async init(container: HTMLElement, _rectangle?: any): Promise { try { this.containerId = container.id; this.containerElement = container; this.showLoadingOverlay(container); const token = 'bearer fa8aa37c-1e52-4631-a699-625b4147ace8'; this.viewer = new Cesium.Viewer(container, { animation: false, timeline: false, baseLayerPicker: false, fullscreenButton: false, vrButton: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, infoBox: false, selectionIndicator: false, shouldAnimate: true, requestRenderMode: false, targetFrameRate: 30, terrainProvider: new Cesium.EllipsoidTerrainProvider() // 默认不使用地形 }); this.viewer.cesiumWidget.creditContainer.style.display = 'none'; const layers = this.viewer.imageryLayers; layers.removeAll(); const provider = new Cesium.UrlTemplateImageryProvider({ url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', maximumLevel: 19, credit: 'Esri, DigitalGlobe, GeoEye, i-cubed, USDA FSA, USGS, AEX, Getmapping, Aerogrid, IGN, IGP, swisstopo, and the GIS User Community' }); const customTerrain = await Cesium.CesiumTerrainProvider.fromUrl( import.meta.env.VITE_APP_MAP_URL + '/Terrain?token=' + token, // 指向包含 layer.json 的目录 { requestVertexNormals: true } ); // 应用地形到Viewer this.viewer.terrainProvider = customTerrain; this.imageryBaseLayer = layers.addImageryProvider(provider); this.imageryBaseLayer.show = true; this.viewer.scene.globe.depthTestAgainstTerrain = true; this.viewer.scene.globe.show = true; this.viewer.scene.fog.enabled = false; this.viewer.scene.fxaa = false; this.viewer.useBrowserRecommendedResolution = true; await this.flyToChina(); this.setupClickHandler(); this.setupCameraPopupReset(); this._ready = true; console.log('[Cesium] init complete, viewer ready'); // 初始化完成后等 5 秒再加载倾斜摄影,确保渲染管线稳定 setTimeout(() => { this.flushPendingOSGB(); }, 5000); return this.viewer; } catch (error) { this.hideLoadingOverlay(); console.error('Cesium Init Critical Error:', error); throw error; } } private flyToChina(): Promise { return new Promise(resolve => { if (!this.viewer) { this.hideLoadingOverlay(); resolve(); return; } // Step 1: 瞬跳至高空俯视中国 this.viewer.camera.cancelFlight(); this.viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees( this.CHINA_CENTER.lng, this.CHINA_CENTER.lat, this.CHINA_BOOT_HEIGHT ), orientation: this.CHINA_VIEW_ORIENTATION }); this.viewer.scene.requestRender(); // Step 2: 等一帧让 setView 生效后,飞入到默认视角 requestAnimationFrame(() => { const finish = () => { this.clearFlightFallbackTimer(); this.hideLoadingOverlay(); resolve(); }; this.clearFlightFallbackTimer(); this.flightFallbackTimer = window.setTimeout(() => { finish(); }, 2600); this.viewer!.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees( this.CHINA_CENTER.lng, this.CHINA_CENTER.lat, this.CHINA_DEFAULT_HEIGHT ), orientation: this.CHINA_VIEW_ORIENTATION, duration: 1.8, complete: finish, cancel: finish }); }); }); } private clearFlightFallbackTimer() { if (this.flightFallbackTimer !== null) { window.clearTimeout(this.flightFallbackTimer); this.flightFallbackTimer = null; } } private showLoadingOverlay(container: HTMLElement) { this.hideLoadingOverlay(); const overlay = document.createElement('div'); overlay.setAttribute('data-map-cesium-loading', 'true'); overlay.style.position = 'absolute'; overlay.style.inset = '0'; overlay.style.display = 'flex'; overlay.style.flexDirection = 'column'; overlay.style.alignItems = 'center'; overlay.style.justifyContent = 'center'; overlay.style.gap = '12px'; overlay.style.background = 'linear-gradient(180deg, rgba(7, 20, 36, 0.72) 0%, rgba(7, 20, 36, 0.46) 100%)'; overlay.style.zIndex = '30'; overlay.style.backdropFilter = 'blur(2px)'; overlay.style.pointerEvents = 'auto'; const spinner = document.createElement('div'); spinner.style.width = '36px'; spinner.style.height = '36px'; spinner.style.borderRadius = '50%'; spinner.style.border = '3px solid rgba(255, 255, 255, 0.25)'; spinner.style.borderTopColor = '#ffffff'; this.loadingSpinnerAnimation = spinner.animate( [{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }], { duration: 900, iterations: Infinity, easing: 'linear' } ); const label = document.createElement('div'); label.textContent = '3D场景加载中...'; label.style.color = '#ffffff'; label.style.fontSize = '14px'; label.style.lineHeight = '20px'; label.style.letterSpacing = '0.5px'; label.style.textShadow = '0 1px 2px rgba(0, 0, 0, 0.35)'; overlay.appendChild(spinner); overlay.appendChild(label); container.appendChild(overlay); this.loadingOverlayElement = overlay; } private hideLoadingOverlay() { if (this.loadingSpinnerAnimation) { this.loadingSpinnerAnimation.cancel(); this.loadingSpinnerAnimation = null; } if (this.loadingOverlayElement?.parentNode) { this.loadingOverlayElement.parentNode.removeChild( this.loadingOverlayElement ); } this.loadingOverlayElement = null; } // ==================== Step 7: 点击锚点打开详情弹框 ==================== private setupClickHandler() { if (!this.viewer) return; if (this.clickEventHandler) { this.clickEventHandler.destroy(); } const modelStore = useModelStore(); const canvas = this.viewer.scene.canvas; this.clickEventHandler = new Cesium.ScreenSpaceEventHandler(canvas); // 默认箭头光标,与 2D 一致(不显示拖拽小手) canvas.style.cursor = 'default'; // 鼠标移动:光标样式 + hover popup(rAF 节流,批量模式下只改光标) let pendingMoveEvent: { endPosition: Cesium.Cartesian2 } | null = null; this.clickEventHandler.setInputAction( (move: { endPosition: Cesium.Cartesian2 }) => { pendingMoveEvent = move; if (this.hoverRafId !== null) return; // 已有待处理的帧,跳过 this.hoverRafId = requestAnimationFrame(() => { this.hoverRafId = null; const evt = pendingMoveEvent; pendingMoveEvent = null; if (!evt) return; const scene = this.viewer?.scene; if (!scene) return; const cameraHeight = this.getCurrentCameraHeight(); // 缩放门槛:中国视角以上不触发 hover if (cameraHeight > this.HOVER_POPUP_MAX_HEIGHT) { canvas.style.cursor = 'default'; this.hidePopup(); return; } const picked = scene.pick(evt.endPosition); const entity = Cesium.defined(picked) && picked.id instanceof Cesium.Entity ? (picked.id as Cesium.Entity) : null; // 检测是否命中可交互的锚点 if (entity && this.isEntityInteractive(entity)) { canvas.style.cursor = 'pointer'; // 批量模式下单点 hover 不触发 popup if (this.isBatchPopupMode) return; // 同一个实体不重复刷新 if (this.hoveredEntityId === entity.id) { const position = entity.position?.getValue( this.viewer!.clock.currentTime ); if (position) { this.updatePopupPosition(position); } return; } this.showPopupForEntity(entity); return; } canvas.style.cursor = 'default'; // 批量模式下不隐藏批量 popup if (!this.isBatchPopupMode) { this.hidePopup(); } }); }, Cesium.ScreenSpaceEventType.MOUSE_MOVE ); // 点击:打开详情弹框 this.clickEventHandler.setInputAction( (click: { position: Cesium.Cartesian2 }) => { const scene = this.viewer?.scene; if (!scene) return; const picked = scene.pick(click.position); if (!Cesium.defined(picked) || !picked.id) return; const entity = picked.id as Cesium.Entity; if (!this.isEntityInteractive(entity)) return; const rawData: Record = (entity as any)._rawData || {}; if (rawData.sttpMap === 'ylfb') { modelStore.ylfbModalVisible = true; modelStore.params = rawData; } else { modelStore.modalVisible = true; modelStore.params = rawData; modelStore.title = rawData.titleName || rawData.stnm; } }, Cesium.ScreenSpaceEventType.LEFT_CLICK ); } // ==================== Step 5: hover popup + 批量 popup ==================== /** 监听相机移动结束:检测缩放阈值切换批量模式 */ private setupCameraPopupReset() { if (!this.viewer) return; if (this.removePostRenderListener) { this.removePostRenderListener(); this.removePostRenderListener = null; } if (this.removeCameraChangedListener) { this.removeCameraChangedListener(); this.removeCameraChangedListener = null; } // camera.changed:相机运动中实时刷新碰撞检测(debounce 150ms) this.removeCameraChangedListener = this.viewer.camera.changed.addEventListener(() => { this.requestRefreshPointLabelVisibility(false, 150); }); this.removePostRenderListener = this.viewer.camera.moveEnd.addEventListener( () => { const zoom = this.getZoomLevelFromCameraHeight( this.getCurrentCameraHeight() ); if (zoom >= this.BATCH_POPUP_MODE_ZOOM) { if (!this.isBatchPopupMode) { this.enableBatchPopupMode(); } else { // 已处于批量模式:重建 popup(与 2D moveend 行为对齐) // 拖拽/平移后视口内可能出现新的可见锚点,仅 postRender 重定位无法创建新 popup // 同时修复 zoom 进出循环后 popup 消失的问题 this.rebuildBatchPopupsIfNeeded(); } } else { if (this.isBatchPopupMode) { this.disableBatchPopupMode(); } // hover popup 重定位而非隐藏,避免缩放结束后闪烁 if (this.hoveredEntityId) { this.repositionHoverPopup(); } } // Step 6: 碰撞检测 — 相机停止移动后重新计算图标/标签可见性 this.requestRefreshPointLabelVisibility(); } ); } /** 根据 hoveredEntityId 重定位 popup(相机移动后调用,不重建内容) */ private repositionHoverPopup(): void { if (!this.viewer || !this.popupElement || !this.hoveredEntityId) return; const entity = this.viewer.entities.getById(this.hoveredEntityId); if (!entity || !entity.position) { this.hidePopup(); return; } const position = entity.position.getValue(this.viewer.clock.currentTime); if (!position) { this.hidePopup(); return; } this.updatePopupPosition(position); } /** 批量模式下每帧更新 popup 位置,实现流畅跟随拖拽 */ private setupBatchPopupPostRenderSync() { if (!this.viewer || this.batchPostRenderRemover) return; this.batchPostRenderRemover = this.viewer.scene.postRender.addEventListener( () => { if (!this.isBatchPopupMode) return; this.updateBatchPopupPositions(); } ); } /** 停止每帧同步 */ private clearBatchPopupPostRenderSync() { if (this.batchPostRenderRemover) { this.batchPostRenderRemover(); this.batchPostRenderRemover = null; } } /** 获取当前相机高度(米) */ private getCurrentCameraHeight(): number { if (!this.viewer) return Number.POSITIVE_INFINITY; const height = this.viewer.camera.positionCartographic.height; if (!height || !isFinite(height) || height <= 0) { return Number.POSITIVE_INFINITY; } return height; } /** 将相机高度映射为近似 2D zoom 级别(与 getCurrentCesiumZoom 同公式) */ private getZoomLevelFromCameraHeight(height: number): number { const zoom = this.CESIUM_ZOOM_FORMULA_D + (this.CESIUM_ZOOM_FORMULA_A - this.CESIUM_ZOOM_FORMULA_D) / (1 + Math.pow( height / this.CESIUM_ZOOM_FORMULA_C, this.CESIUM_ZOOM_FORMULA_B )) + 1; return zoom > -1 ? zoom : 0; } /** 判断实体是否属于 ENG 类型(用于 popup 碰撞优先保留,与 isEntityEng 对齐) */ private isEngEntity(entity: Cesium.Entity): boolean { const e = entity as any; // 优先从实体 properties._sttpMap 直接检查(与 Entity 创建时设置的 _sttpMap 一致) const sttpFromProps = String(e._sttpMap || '').toUpperCase(); if (sttpFromProps.startsWith('ENG')) return true; // 其次从 _rawData 检查 const rawData = e._rawData as Record | undefined; if (!rawData) return false; const sttpMap = getNormalizedPopupSttpMap(rawData).toUpperCase(); return sttpMap.startsWith('ENG'); } /** 判断实体当前是否可交互(图层可见 + 图例可见 + 区域可见) */ private isEntityInteractive(entity: Cesium.Entity): boolean { const e = entity as any; if (e._layerVisible === false) return false; if (e._legendVisible === false) return false; if (e._regionVisible === false) return false; if (entity.show === false) return false; return true; } /** 根据实体属性生成 popup HTML,复用 generatePopupHtml */ private getPopupHtml(rawData: Record): string { return rawData.popupHtml || generatePopupHtml(rawData) || ''; } /** 关闭单个 hover popup */ private hidePopup(): void { this.hoveredEntityId = null; if (!this.popupElement) return; this.popupElement.style.display = 'none'; this.popupElement.innerHTML = ''; } /** 判断 3D 位置是否被地形遮挡(基于射线与 terrain 求交) */ private isPositionBehindTerrain(position: Cesium.Cartesian3): boolean { if (!this.viewer) return true; const scene = this.viewer.scene; const windowCoord = Cesium.SceneTransforms.worldToWindowCoordinates( scene, position ); if (!windowCoord) return true; // 在地球背面 const ray = scene.camera.getPickRay(windowCoord); if (!ray) return false; const terrainPos = scene.globe.pick(ray, scene); if (!terrainPos) return false; // 该方向无地形 const cameraPos = scene.camera.positionWC; const terrainDist = Cesium.Cartesian3.distance(cameraPos, terrainPos); const entityDist = Cesium.Cartesian3.distance(cameraPos, position); // 地形交点明显更近 → 实体被遮挡 return terrainDist + 10 < entityDist; } /** 根据实体世界坐标把 popup 定位到屏幕坐标 */ private updatePopupPosition(position: Cesium.Cartesian3): void { if (!this.viewer || !this.popupElement) return; // 被地形遮挡时隐藏 popup if (this.isPositionBehindTerrain(position)) { this.hidePopup(); return; } const canvasPosition = Cesium.SceneTransforms.worldToWindowCoordinates( this.viewer.scene, position ); if (!canvasPosition) { this.hidePopup(); return; } this.popupElement.style.left = `${canvasPosition.x}px`; this.popupElement.style.top = `${canvasPosition.y}px`; this.popupElement.style.display = 'block'; } /** 读取实体原始数据并渲染 hover popup */ private showPopupForEntity(entity: Cesium.Entity): void { if (!this.viewer || !this.popupElement || !entity.position) return; const rawData: Record = { ...((entity as any)._rawData || {}) }; if (!rawData.sttpMap) { rawData.sttpMap = rawData._sttpMap || rawData.popupSttpMap || ''; } const popupHtml = this.getPopupHtml(rawData); const position = entity.position.getValue(this.viewer.clock.currentTime); if (!popupHtml || !position) { this.hidePopup(); return; } this.popupElement.innerHTML = popupHtml; this.hoveredEntityId = entity.id; this.updatePopupPosition(position); } // ==================== 批量 popup ==================== /** 启用批量 popup:所有可见锚点固定显示 popup */ private enableBatchPopupMode() { console.log('[Cesium] enableBatchPopupMode'); this.isBatchPopupMode = true; this.clearBatchPopups(); this.hidePopup(); // 清除可能残留的 hover popup // 先同步执行碰撞检测,确保 _iconCollisionVisible 已正确计算, // 避免重叠锚点的 popup 在 showBatchPopups 中被短暂创建 this.refreshPointLabelVisibility(); // 等所有 chunk 处理完再注册 postRender,避免用不完整列表做碰撞 this.showBatchPopups().then(() => { if (this.isBatchPopupMode) { this.setupBatchPopupPostRenderSync(); } }); } /** 退出批量 popup */ private disableBatchPopupMode() { console.log('[Cesium] disableBatchPopupMode'); this.isBatchPopupMode = false; this.clearBatchPopupPostRenderSync(); this.clearBatchPopups(); this.hidePopup(); } /** 收集视口内可交互实体并按 X 坐标排序(ENG 优先) */ private collectBatchPopupCandidates(): Array<{ entity: Cesium.Entity; x: number; y: number; popupHtml: string; }> { if (!this.viewer) return []; const canvas = this.viewer.scene.canvas; const viewW = canvas.clientWidth; const viewH = canvas.clientHeight; const padding = 60; const candidates: Array<{ entity: Cesium.Entity; x: number; y: number; popupHtml: string; }> = []; this.pointLayerRegistry.forEach(entities => { entities.forEach(entity => { if (!this.isEntityInteractive(entity)) return; // 图标被碰撞/密度隐藏的锚点不展示 popup(没有可见图标,popup 无意义且干扰碰撞) if ((entity as any)._iconCollisionVisible === false) return; const position = entity.position?.getValue( this.viewer!.clock.currentTime ); if (!position) return; const cp = Cesium.SceneTransforms.worldToWindowCoordinates( this.viewer!.scene, position ); if (!cp) return; if ( cp.x < -padding || cp.y < -padding || cp.x > viewW + padding || cp.y > viewH + padding ) return; // 被地形遮挡的不展示 popup if (this.isPositionBehindTerrain(position)) return; const rawData: Record = { ...((entity as any)._rawData || {}) }; if (!rawData.sttpMap) { rawData.sttpMap = rawData._sttpMap || rawData.popupSttpMap || ''; } let popupHtml = shouldPreferEng2Popup(rawData) ? generatePopupHtml(rawData, { forceEng2: true }) || rawData.popupHtml || generatePopupHtml(rawData) : this.getPopupHtml(rawData); if (!popupHtml) { const title = rawData.titleName || rawData.stnm || rawData.ennm || ''; if (!title) return; popupHtml = `
${title}
`; } candidates.push({ entity, x: cp.x, y: cp.y, popupHtml }); }); }); // 按 X 坐标排序,X 接近时(5px 容差,3D 投影浮点误差)ENG 优先 candidates.sort((a, b) => { const dx = a.x - b.x; if (Math.abs(dx) > 5) return dx; const aEng = this.isEngEntity(a.entity) ? 0 : 1; const bEng = this.isEngEntity(b.entity) ? 0 : 1; if (aEng !== bEng) return aEng - bEng; return dx; }); return candidates; } /** 在指定容器中分帧构建 popup DOM(避免卡顿),完成后 resolve */ private buildBatchPopupsInContainer( container: HTMLDivElement, candidates: Array<{ entity: Cesium.Entity; x: number; y: number; popupHtml: string; }> ): Promise { return new Promise(resolve => { let index = 0; const CHUNK_SIZE = 80; const placedPopups: Array<{ left: number; right: number; top: number; bottom: number; entity: Cesium.Entity; element: HTMLDivElement; }> = []; const processChunk = () => { if (!this.isBatchPopupMode) return; const end = Math.min(index + CHUNK_SIZE, candidates.length); for (let i = index; i < end; i++) { const { entity, x, y, popupHtml } = candidates[i]; const popupEl = document.createElement('div'); popupEl.className = this.popupElement?.className || 'map-popup-container'; popupEl.innerHTML = popupHtml; popupEl.style.position = 'absolute'; popupEl.style.display = 'block'; popupEl.style.transform = 'translate(-50%, calc(-100% - 10px))'; popupEl.style.pointerEvents = 'none'; // 先隐藏测量尺寸 popupEl.style.visibility = 'hidden'; popupEl.style.left = `${x}px`; popupEl.style.top = `${y}px`; container.appendChild(popupEl); const rect = popupEl.getBoundingClientRect(); const pw = rect.width || 120; const ph = rect.height || 40; const popupRect = { left: x - pw / 2, right: x + pw / 2, top: y - 10 - ph, bottom: y - 10 }; const isCurrentEng = this.isEngEntity(entity); let collidedIdx = -1; for (let k = 0; k < placedPopups.length; k++) { if (this.checkPopupCollision(popupRect, placedPopups[k])) { collidedIdx = k; break; } } if (collidedIdx === -1) { // 无碰撞:正常放置 popupEl.style.visibility = 'visible'; popupEl.style.left = `${x}px`; popupEl.style.top = `${y}px`; this.batchPopupItems.push({ entity, element: popupEl, popupWidth: pw, popupHeight: ph }); placedPopups.push({ ...popupRect, entity, element: popupEl }); } else { const collided = placedPopups[collidedIdx]; const isCollidedEng = this.isEngEntity(collided.entity); if (isCurrentEng && !isCollidedEng) { // ENG 碰撞普通 popup:移除普通,展示 ENG collided.element.remove(); this.batchPopupItems = this.batchPopupItems.filter( item => item.element !== collided.element ); popupEl.style.visibility = 'visible'; popupEl.style.left = `${x}px`; popupEl.style.top = `${y}px`; this.batchPopupItems.push({ entity, element: popupEl, popupWidth: pw, popupHeight: ph }); placedPopups[collidedIdx] = { ...popupRect, entity, element: popupEl }; } else if (!isCurrentEng && isCollidedEng) { // 普通碰撞 ENG:隐藏当前普通 popup,保留 ENG container.removeChild(popupEl); } else { // 同类型碰撞:后来的让路 container.removeChild(popupEl); } } } index = end; if (index < candidates.length && this.isBatchPopupMode) { requestAnimationFrame(processChunk); } else { resolve(); } }; requestAnimationFrame(processChunk); }); } /** 创建批量 popup 容器并构建所有可见 popup */ private showBatchPopups(): Promise { if (!this.viewer || !this.containerElement) return Promise.resolve(); const batchContainer = document.createElement('div'); batchContainer.className = 'batch-popup-container'; batchContainer.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1000; `; batchContainer.id = 'batch-popup-container'; this.batchPopupContainer = batchContainer; this.containerElement.appendChild(batchContainer); const candidates = this.collectBatchPopupCandidates(); return this.buildBatchPopupsInContainer(batchContainer, candidates); } /** 每一帧都在更新,不需要持久化,但清理时一并重置 */ private popupCollisionFrames = new WeakMap(); /** 每帧更新批量 popup 屏幕位置 + 碰撞检测(拖拽/缩放时流畅跟随) */ private updateBatchPopupPositions() { if (!this.viewer || !this.batchPopupContainer) return; if (this.batchPopupItems.length === 0) return; const canvas = this.viewer.scene.canvas; const viewW = canvas.clientWidth; const viewH = canvas.clientHeight; const padding = 48; // 第一遍:更新位置、收集可见项的屏幕矩形(用缓存宽高,不依赖 DOM 测量) const visibleRects: { item: (typeof this.batchPopupItems)[number]; x: number; y: number; left: number; right: number; top: number; bottom: number; }[] = []; for (let i = 0; i < this.batchPopupItems.length; i++) { const item = this.batchPopupItems[i]; const { entity, element, popupWidth, popupHeight } = item; // 先恢复为 block,碰撞检测再做最终决定 element.style.display = 'block'; if (!this.isEntityInteractive(entity)) { element.style.display = 'none'; continue; } // 图标被碰撞隐藏的锚点不展示 popup if ((entity as any)._iconCollisionVisible === false) { element.style.display = 'none'; continue; } const position = entity.position?.getValue( this.viewer!.clock.currentTime ); if (!position) { element.style.display = 'none'; continue; } const cp = Cesium.SceneTransforms.worldToWindowCoordinates( this.viewer!.scene, position ); if (!cp) { element.style.display = 'none'; continue; } if ( cp.x < -padding || cp.y < -padding || cp.x > viewW + padding || cp.y > viewH + padding ) { element.style.display = 'none'; continue; } const x = cp.x; const y = cp.y; element.style.left = `${x}px`; element.style.top = `${y}px`; visibleRects.push({ item, x, y, left: x - popupWidth / 2, right: x + popupWidth / 2, top: y - 10 - popupHeight, bottom: y - 10 }); } // 第二遍:碰撞检测 — 按 X 排序后右让左,X 接近时(5px 容差)ENG 优先 visibleRects.sort((a, b) => { const dx = a.x - b.x; if (Math.abs(dx) > 5) return dx; const aEng = this.isEngEntity(a.item.entity) ? 0 : 1; const bEng = this.isEngEntity(b.item.entity) ? 0 : 1; if (aEng !== bEng) return aEng - bEng; return dx; }); const collisionFlags = new Array(visibleRects.length).fill(false); const instantFlags = new Array(visibleRects.length).fill(false); // ENG 碰撞立即隐藏 for (let i = 1; i < visibleRects.length; i++) { const a = visibleRects[i]; for (let j = 0; j < i; j++) { if (collisionFlags[j]) continue; const b = visibleRects[j]; if (!this.checkPopupCollision(a, b)) continue; const aEng = this.isEngEntity(a.item.entity); const bEng = this.isEngEntity(b.item.entity); if (aEng && !bEng) { // ENG (a) 碰撞普通 (b):立即隐藏普通,ENG 继续检查是否碰撞其他已放置项 collisionFlags[j] = true; instantFlags[j] = true; } else if (!aEng && bEng) { // 普通 (a) 碰撞 ENG (b):立即隐藏普通 collisionFlags[i] = true; instantFlags[i] = true; break; } else { // 同类型碰撞:后来的让路(经 2 帧滞后防闪烁) collisionFlags[i] = true; break; } } } // 第三遍:应用显示状态,ENG 碰撞立即隐藏,同类型 2 帧滞后防闪烁 for (let i = 0; i < visibleRects.length; i++) { const { element } = visibleRects[i].item; const hidden = collisionFlags[i]; if (hidden && instantFlags[i]) { // ENG 碰撞:立即隐藏,不经过滞后 element.style.display = 'none'; this.popupCollisionFrames.delete(element); continue; } const prevFrames = this.popupCollisionFrames.get(element) || 0; if (hidden) { const count = prevFrames > 0 ? prevFrames + 1 : 1; this.popupCollisionFrames.set(element, count); if (count >= 2) { element.style.display = 'none'; } } else { const count = prevFrames < 0 ? prevFrames - 1 : -1; this.popupCollisionFrames.set(element, count); if (count <= -2) { element.style.display = 'block'; } } } } /** 批量模式下双缓冲重建 popup(图层/图例变化、拖拽/缩放 moveEnd 时调用)。 * 在新隐藏容器中构建 popup,构建完成后替换旧容器,避免 clear→rebuild 空白帧闪烁。 */ private _rebuildGeneration = 0; private rebuildBatchPopupsIfNeeded() { if (!this.isBatchPopupMode || !this.containerElement) return; console.log( '[Cesium] rebuildBatchPopups gen=%d', this._rebuildGeneration + 1 ); // 停止旧容器的 postRender 同步(旧 popup 在新容器就绪前保持显示) this.clearBatchPopupPostRenderSync(); const oldContainer = this.batchPopupContainer; const generation = ++this._rebuildGeneration; // 在隐藏容器中构建新 popup(尚未插入 DOM) const newContainer = document.createElement('div'); newContainer.className = 'batch-popup-container'; newContainer.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:1000;'; newContainer.id = 'batch-popup-container'; this.batchPopupContainer = newContainer; this.batchPopupItems = []; this.popupCollisionFrames = new WeakMap(); const candidates = this.collectBatchPopupCandidates(); this.buildBatchPopupsInContainer(newContainer, candidates).then(() => { // 如果期间触发了新的重建,丢弃本次结果 if (generation !== this._rebuildGeneration) { newContainer.remove(); return; } if (!this.isBatchPopupMode) { newContainer.remove(); return; } // 双缓冲替换:移除旧容器 → 插入新容器 → 注册 postRender if (oldContainer) oldContainer.remove(); this.containerElement!.appendChild(newContainer); this.setupBatchPopupPostRenderSync(); }); } /** 清除所有批量 popup */ private clearBatchPopups() { this.batchPopupItems = []; this.popupCollisionFrames = new WeakMap(); if (this.batchPopupContainer) { this.batchPopupContainer.remove(); this.batchPopupContainer = null; return; } const existing = document.getElementById('batch-popup-container'); if (existing) { existing.remove(); } } /** 简单碰撞检测(AABB + padding) */ private checkPopupCollision( a: { left: number; right: number; top: number; bottom: number }, b: { left: number; right: number; top: number; bottom: number } ): boolean { const padding = 4; return !( a.right + padding < b.left || a.left - padding > b.right || a.bottom + padding < b.top || a.top - padding > b.bottom ); } // ==================== destroy ==================== destroy(): void { // 取消飞行超时和加载动画 this.clearFlightFallbackTimer(); this.hideLoadingOverlay(); this._ready = false; this._pendingOSGBItems = []; // 批量 popup 和 hover popup this.isBatchPopupMode = false; this.clearBatchPopupPostRenderSync(); this.clearBatchPopups(); this.hidePopup(); // hover 节流 requestAnimationFrame if (this.hoverRafId !== null) { cancelAnimationFrame(this.hoverRafId); this.hoverRafId = null; } // 相机事件监听 if (this.removePostRenderListener) { this.removePostRenderListener(); this.removePostRenderListener = null; } if (this.removeCameraChangedListener) { this.removeCameraChangedListener(); this.removeCameraChangedListener = null; } // 碰撞检测 rAF / 定时器 if (this.collisionRefreshFrameId !== null) { cancelAnimationFrame(this.collisionRefreshFrameId); this.collisionRefreshFrameId = null; } if (this.labelVisibilityDebounceTimerId !== null) { window.clearTimeout(this.labelVisibilityDebounceTimerId); this.labelVisibilityDebounceTimerId = null; } // 中止进行中的基地裁切请求 if (this.clipRequestController) { this.clipRequestController.abort(); this.clipRequestController = null; } // 清理遮罩多边形 Entity(需在 viewer.destroy 前清除) if (this.viewer && !this.viewer.isDestroyed()) { this.maskPolygonEntities.forEach(entity => { this.viewer!.entities.remove(entity); }); } this.maskPolygonEntities = []; // 清理锚点图层和底图注册表 this.destroyAllPointLayers(); this.pointLayerRegistry.clear(); this.baseLayerRegistry.clear(); this.baseLayerAliasMap.clear(); this.imageryBaseLayer = null; // 清理 Cesium 事件处理器 if (this.clickEventHandler) { this.clickEventHandler.destroy(); this.clickEventHandler = null; } this.popupElement = null; // 销毁 Cesium Viewer if (this.viewer) { this._rebuildGeneration++; // 中止进行中的双缓冲重建 this.viewer.camera.cancelFlight(); this.viewer.destroy(); this.viewer = null; } this.containerElement = null; // 重置所有状态变量(确保下次 init 时是干净状态) this.popupCollisionFrames = new WeakMap(); this.currentClipGeoJson = null; this.originalBgColor = null; this.skyBoxShown = true; this.skyAtmosphereShown = true; this.hydropBaseConfig = null; this.BASEID = ''; this.hoveredEntityId = null; console.log('[Cesium] destroyed'); } private requestRender() { this.viewer?.scene.requestRender(); } /** * 标签文本格式化:与 2D formatPointLabelText 完全对齐 * - 去除括号 ()() * - 每行最多 12 个字符 * - 13~24 字符 → 拆成两行 * - 超过 24 字符 → 第二行末尾用 "..." 截断 */ private formatPointLabelText(text: string): string { const normalizedText = String(text || '') .replace(/[()()]/g, '') .trim(); if (!normalizedText) { return ''; } const maxLineLength = 12; if (normalizedText.length <= maxLineLength) { return normalizedText; } const firstLine = normalizedText.slice(0, maxLineLength); if (normalizedText.length <= maxLineLength * 2) { return `${firstLine}\n${normalizedText.slice(maxLineLength)}`; } const secondLine = `${normalizedText.slice( maxLineLength, maxLineLength + 9 )}...`; return `${firstLine}\n${secondLine}`; } // ==================== Step 2: 基础底图 ==================== private getRegistryKeys(layerConfig: any): string[] { const keys = [layerConfig?.key, layerConfig?.id].filter( (key): key is string => typeof key === 'string' && key.length > 0 ); return Array.from(new Set(keys)); } private resolvePrimaryBaseLayerKey(layerKey: string): string | undefined { if (!layerKey) return undefined; if (this.baseLayerRegistry.has(layerKey)) return layerKey; return this.baseLayerAliasMap.get(layerKey); } private unregisterBaseLayer(layerConfig: any) { if (!this.viewer) return; const registryKeys = this.getRegistryKeys(layerConfig); const uniquePrimaryKeys = new Set(); registryKeys.forEach(key => { const primaryKey = this.resolvePrimaryBaseLayerKey(key) || key; uniquePrimaryKeys.add(primaryKey); this.baseLayerAliasMap.delete(key); }); uniquePrimaryKeys.forEach(primaryKey => { const existingLayer = this.baseLayerRegistry.get(primaryKey); if (existingLayer) { this.viewer?.imageryLayers.remove(existingLayer, true); this.baseLayerRegistry.delete(primaryKey); } }); } private registerBaseLayer( layerConfig: any, imageryLayer: Cesium.ImageryLayer ) { const registryKeys = this.getRegistryKeys(layerConfig); const primaryKey = layerConfig?.key || layerConfig?.id; if (!primaryKey) return; this.baseLayerRegistry.set(primaryKey, imageryLayer); registryKeys.forEach(key => { this.baseLayerAliasMap.set(key, primaryKey); }); } private getLayerRequestUrl(layerConfig: any): string { if (layerConfig?.key === 'customBaseLayer') { return layerConfig?.url_3d; } return layerConfig?.url || layerConfig?.url_3d || ''; } private createImageryLayer(layerConfig: any, visible: boolean) { if (layerConfig?.key === 'hydropBase') return null; if (!this.viewer) return null; const url = this.getLayerRequestUrl(layerConfig); if (!url) return null; let imageryLayer: Cesium.ImageryLayer | null = null; // WMTS:与 2D addBaseDataLayer 对齐,从 URL 参数中提取 LAYER / TILEMATRIXSET // 必须提供 tileMatrixLabels,否则 Cesium 默认用纯数字 tileMatrix 值, // 而服务器期望的是 "TileMatrixSet:zoom" 格式(如 EPSG:3857_hbb_zrbhq_l13:4) if (layerConfig?.type === 'wmts') { const urlParts = url.split('?'); const baseUrl = urlParts[0]; const urlParams = new URLSearchParams(urlParts[1] || ''); const wmtsLayer = urlParams.get('LAYER') || urlParams.get('layer') || ''; const tileMatrixSetID = urlParams.get('TILEMATRIXSET') || urlParams.get('TileMatrixSet') || ''; const maxLevel = 19; const tileMatrixLabels = Array.from( { length: maxLevel + 1 }, (_, i) => `${tileMatrixSetID}:${i}` ); imageryLayer = this.viewer.imageryLayers.addImageryProvider( new Cesium.WebMapTileServiceImageryProvider({ url: baseUrl, layer: wmtsLayer, style: 'raster', format: 'image/png', tileMatrixSetID: tileMatrixSetID, tileMatrixLabels: tileMatrixLabels, maximumLevel: maxLevel }) ); } else if ( layerConfig?.type === 'raster-dem' || /\{x\}|\{y\}|\{z\}/.test(url) ) { imageryLayer = this.viewer.imageryLayers.addImageryProvider( new Cesium.UrlTemplateImageryProvider({ url }) ); } else if (url.includes('MapServer')) { imageryLayer = this.viewer.imageryLayers.add( Cesium.ImageryLayer.fromProviderAsync( Cesium.ArcGisMapServerImageryProvider.fromUrl(url, { enablePickFeatures: false, maximumLevel: 19 }) ) ); } else { const cleanUrl = url.split('?')[0]; const wmsLayers = layerConfig?.layers || ''; imageryLayer = this.viewer.imageryLayers.addImageryProvider( new Cesium.WebMapServiceImageryProvider({ url: cleanUrl, layers: wmsLayers, parameters: { service: 'WMS', transparent: 'true', format: 'image/png' } }) ); } if (!imageryLayer) return null; imageryLayer.show = visible; if (typeof layerConfig?.rasteropacity === 'number') { imageryLayer.alpha = layerConfig.rasteropacity; } return imageryLayer; } addBaseDataLayer(layerConfig: any, isShow = true): void { if (!this.viewer || !layerConfig?.key) return; // 基地裁切配置存储(与 2D 对齐:layer.type === 'vector' && layer.key === 'hydropBase') if (layerConfig.type === 'vector' && layerConfig.key === 'hydropBase') { this.hydropBaseConfig = layerConfig; return; } if ( this.IGNORED_BASE_LAYER_KEYS.has(layerConfig.key) || this.IGNORED_BASE_LAYER_KEYS.has(layerConfig.id) ) { return; } if (layerConfig.key === 'BASEMAP-img') { if (this.imageryBaseLayer) { this.imageryBaseLayer.show = !!isShow; } this.requestRender(); return; } const imageryLayer = this.createImageryLayer(layerConfig, !!isShow); if (!imageryLayer) return; this.unregisterBaseLayer(layerConfig); this.registerBaseLayer(layerConfig, imageryLayer); this.requestRender(); } controlBaseLayerTreeShowAndHidden( layerType: string, key: string, checked: boolean ): void { const targetKey = key || layerType; if (targetKey === 'BASEMAP-img') { if (this.imageryBaseLayer) { this.imageryBaseLayer.show = checked; } return; } const primaryKey = this.resolvePrimaryBaseLayerKey(targetKey); if (!primaryKey) return; const imageryLayer = this.baseLayerRegistry.get(primaryKey); if (!imageryLayer) return; imageryLayer.show = checked; this.requestRender(); } hasBaseLayer(layerKey: string): boolean { if (layerKey === 'BASEMAP-img') return !!this.imageryBaseLayer; return !!this.resolvePrimaryBaseLayerKey(layerKey); } // ==================== 其他公共方法 ==================== flyTopanto(position: number[], zoom: number): void { if (!this.viewer || !position) return; const [lng, lat] = position; const height = 20000000 / Math.pow(2, zoom); this.viewer.camera.cancelFlight(); this.viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees( lng, lat, height > 100 ? height : 1000 ), duration: 1.5 }); } getCurrentZoom(): number | undefined { return this.getCurrentCesiumZoom(); } private getCurrentCesiumZoom() { if (!this.viewer) return undefined; const height = this.viewer.camera.positionCartographic.height; if (!height || !isFinite(height) || height <= 0) return undefined; const zoom = this.CESIUM_ZOOM_FORMULA_D + (this.CESIUM_ZOOM_FORMULA_A - this.CESIUM_ZOOM_FORMULA_D) / (1 + Math.pow( Number(height) / this.CESIUM_ZOOM_FORMULA_C, this.CESIUM_ZOOM_FORMULA_B )) + 1; return zoom > -1 ? zoom : 0; } zoomToggle(type: 'out' | 'in'): void { if (!this.viewer) return; const factor = 1.5; if (type === 'in') this.viewer.camera.zoomIn(factor); else this.viewer.camera.zoomOut(factor); } mapOutPut(fileName = '3D_截图.png'): void { if (!this.viewer) { console.warn('Viewer is not initialized.'); return; } try { const canvas = this.viewer.canvas; this.viewer.render(); const dataURL = canvas.toDataURL('image/png'); if (dataURL.length < 100) { console.warn( 'Canvas data is too small, likely black or empty. Check CORS settings.' ); } const link = document.createElement('a'); link.href = dataURL; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); } catch (error) { console.error('Failed to export map image:', error); } } initPopupOverlay(popupContainer: HTMLDivElement): void { this.popupElement = popupContainer; if (this.popupElement) { // 清除可能残留的旧样式 this.popupElement.style.removeProperty('position'); this.popupElement.style.removeProperty('transform'); this.popupElement.style.removeProperty('left'); this.popupElement.style.removeProperty('top'); this.popupElement.style.display = 'none'; this.popupElement.style.position = 'absolute'; this.popupElement.style.transform = 'translate(-50%, calc(-100% - 10px))'; this.popupElement.style.setProperty( 'pointer-events', 'none', 'important' ); } } // ==================== Step 3: 锚点展示 ==================== addInitDataLayer( pointData: any, layerType: any, _mdoptions?: MDOptions ): void { if (!this.viewer) return; let dataArray: any[] = []; let targetLayerKey = layerType; // 与 2D PointLayerManager.addDataLayer 对齐的输入归一化 if (Array.isArray(pointData)) { dataArray = pointData; } else { dataArray = pointData?.data || []; targetLayerKey = pointData?.key || layerType; } if (!targetLayerKey) { console.warn('[Cesium] 缺少图层 Key,无法加载锚点'); return; } // 先移除旧图层数据 this.removePointLayer(targetLayerKey); const entities: Cesium.Entity[] = []; dataArray.forEach((item: any) => { const { lgtd, lttd, stnm, iconCode, titleName, ennm } = item; if (lgtd == null || lttd == null) return; const lon = Number(lgtd); const lat = Number(lttd); if (isNaN(lon) || isNaN(lat) || !isFinite(lon) || !isFinite(lat)) return; if (Math.abs(lon) > 180 || Math.abs(lat) > 90) return; // 图标:复用现有图标资源路径,与 2D 一致 const iconUrl = iconCode ? getIconPath(iconCode) : getIconPath('default') || ''; // 标签文本:格式化(换行截断)+ ylfb 用 ftp,其他用 titleName > stnm > ennm const rawLabelText = item.sttpMap === 'ylfb' ? item.ftp || '' : titleName || stnm || ennm || ''; const labelText = this.formatPointLabelText(rawLabelText); // 行数决定纵向偏移量,与 2D getPointLabelRenderOffsetY 对齐 const labelLineCount = labelText.includes('\n') ? 2 : labelText ? 1 : 0; // Entity ID:与 2D 对齐,使用 _id 优先(_id = sttpMap_stcd),确保同 stcd 不同 sttpMap 的点不互相覆盖 const rawId = item._id || item.stcd || `${lon.toFixed(6)},${lat.toFixed(6)}`; const entityId = `${targetLayerKey}:${rawId}`; // 图标缩放:使用 CallbackProperty 动态响应相机缩放变化 // 调参见顶部 ICON_ 常量 const dynamicIconScale = new Cesium.CallbackProperty(() => { const zoom = this.getCurrentCesiumZoom() || 4.5; return Math.max( this.ICON_MIN_SCALE, Math.min( this.ICON_MAX_SCALE, this.ICON_BASE_SCALE + (zoom - 4.5) * this.ICON_SCALE_RATE ) ); }, false); // 字体大小:与 2D getPointFontSize 对齐 → fontSize = clamp(base * scale, min, max) // 调参见顶部 LABEL_FONT_ 常量 const dynamicLabelFont = new Cesium.CallbackProperty(() => { const zoom = this.getCurrentCesiumZoom() || 4.5; const scale = Math.max(0.5, Math.min(3.0, 0.7 + (zoom - 4.5) * 0.08)); const fontSize = Math.round( Math.max( this.LABEL_FONT_MIN, Math.min(this.LABEL_FONT_MAX, this.LABEL_FONT_BASE * scale) ) ); return `${fontSize}px sans-serif`; }, false); // 标签纵向偏移:与 2D getPointLabelRenderOffsetY 对齐 // 调参见顶部 LABEL_OFFSET_ 常量 const dynamicLabelOffset = new Cesium.CallbackProperty(() => { const zoom = this.getCurrentCesiumZoom() || 4.5; const scale = Math.max(0.5, Math.min(3.0, 0.7 + (zoom - 4.5) * 0.08)); const offsetY = labelLineCount > 1 ? -this.LABEL_OFFSET_MULTI * scale : -this.LABEL_OFFSET_SINGLE * scale; return new Cesium.Cartesian2(0, offsetY); }, false); const entity = new Cesium.Entity({ id: entityId, position: Cesium.Cartesian3.fromDegrees(lon, lat, 0), billboard: { image: iconUrl || undefined, scale: dynamicIconScale as any, verticalOrigin: Cesium.VerticalOrigin.CENTER, horizontalOrigin: Cesium.HorizontalOrigin.CENTER, scaleByDistance: new Cesium.NearFarScalar(1.0e3, 1.5, 2.0e7, 0.8), heightReference: Cesium.HeightReference.CLAMP_TO_GROUND } as any, label: { text: labelText || undefined, font: dynamicLabelFont as any, fillColor: Cesium.Color.WHITE, outlineColor: Cesium.Color.BLACK, outlineWidth: 2, style: Cesium.LabelStyle.FILL_AND_OUTLINE, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, pixelOffset: dynamicLabelOffset as any, horizontalOrigin: Cesium.HorizontalOrigin.CENTER, scaleByDistance: new Cesium.NearFarScalar(1.0e3, 1.2, 2.0e7, 0.8), heightReference: Cesium.HeightReference.CLAMP_TO_GROUND } as any, properties: { ...item, _layerKey: targetLayerKey, _iconUrl: iconUrl, _labelText: labelText, _legendVisible: true, _regionVisible: true, _sttpMap: item.sttpMap } as any }); this.viewer!.entities.add(entity); // 存储原始业务数据引用,避免点击时遍历 Cesium Property (entity as any)._rawData = item; (entity as any)._labelText = labelText; // 可见性控制标志(组合计算 entity.show) (entity as any)._layerVisible = true; (entity as any)._legendVisible = true; (entity as any)._regionVisible = true; // 碰撞检测可见性标志(初始均为可见,碰撞检测会按需更新) (entity as any)._iconCollisionVisible = true; (entity as any)._labelCollisionVisible = true; // 初始应用可见性(确保 entity.show / entity.label.show 被显式设置) this.applyEntityVisibility(entity); entities.push(entity); }); this.pointLayerRegistry.set(targetLayerKey, entities); // 如果当前有活跃的基地裁切,新图层也需要过滤 if (this.currentClipGeoJson) { this.filterPointsByRegionForLayer( targetLayerKey, this.extractPolygonCoords(this.currentClipGeoJson) ); } // 数据加载完成后触发首次碰撞检测(同步执行,避免异步 rAF 导致的首帧闪烁) this.requestRefreshPointLabelVisibility(true); } mdLayerTreeShowOrHidden(layerType: string, checked?: boolean): void { const entities = this.pointLayerRegistry.get(layerType); if (!entities) return; const visible = checked !== false; entities.forEach(entity => { (entity as any)._layerVisible = visible; this.applyEntityVisibility(entity); }); // 图层切换后立即重算碰撞(同步执行,避免闪烁) this.requestRefreshPointLabelVisibility(true); // 批量模式下同步重建 popup this.rebuildBatchPopupsIfNeeded(); } removePointLayer(layerKey: string): void { if (!layerKey) return; const entities = this.pointLayerRegistry.get(layerKey); if (!entities) return; entities.forEach(entity => { if (this.viewer && !this.viewer.isDestroyed()) { this.viewer.entities.remove(entity); } }); this.pointLayerRegistry.delete(layerKey); } hasLayer(layerKey: string): boolean { return this.pointLayerRegistry.has(layerKey); } private destroyAllPointLayers(): void { this.pointLayerRegistry.forEach((entities, layerKey) => { entities.forEach(entity => { if (this.viewer && !this.viewer.isDestroyed()) { this.viewer.entities.remove(entity); } }); }); this.pointLayerRegistry.clear(); } // ==================== Stubs(后续步骤补齐) ==================== baseLayerSwitcher(_key: string): void {} switchView(_type: '2D' | '3D'): void {} fitBounds(_bounds: any): void {} lengthCalculate(): void {} areCalculate(): void {} removeQueryLayer(): void {} addTertiarybasinLayer( _layer: layer, _fillcolor: any, _outlineColor: any, _datas: any ): void {} hideTertiarybasinLayer(_layer: layer): void {} async jdPanelControlShowAndHidden( _regionId: string, isAll: boolean ): Promise { if (!this.viewer || !this.hydropBaseConfig) { console.warn('地图未初始化或 hydropBaseConfig 未配置'); return; } // 取消正在进行的裁切请求 if (this.clipRequestController) { this.clipRequestController.abort(); this.clipRequestController = null; } this.BASEID = _regionId; if (!isAll) { // 取消裁切:清除遮罩多边形,恢复全部锚点可见,平滑飞回全国视图 this.currentClipGeoJson = null; this.clearMaskPolygon(); this.setAllPointsRegionVisible(true); this.viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees( this.CHINA_CENTER.lng, this.CHINA_CENTER.lat, this.CHINA_DEFAULT_HEIGHT ), orientation: this.CHINA_VIEW_ORIENTATION, duration: 1.0 }); return; } // 用户从"全部"进入,锚点本来就全可见,无需 API 前先隐藏(避免全量遍历卡顿) this.clipRequestController = new AbortController(); const signal = this.clipRequestController.signal; try { const url = this.hydropBaseConfig.geojson_url + `&cql_filter=BASEID='${this.BASEID}'`; const geoJsonData = await this.fetchGeoJson(url, signal); if (signal.aborted) return; if (this.BASEID !== _regionId) return; this.currentClipGeoJson = geoJsonData; const regionCoords = this.extractPolygonCoords(geoJsonData); this.filterPointsByRegion(regionCoords); this.fitViewToGeoJson(regionCoords); this.createMaskPolygon(regionCoords); } catch (error: any) { if (error.name === 'AbortError') return; this.currentClipGeoJson = null; console.error('加载裁切数据失败:', error); this.clearMaskPolygon(); this.setAllPointsRegionVisible(true); } finally { if (this.clipRequestController?.signal === signal) { this.clipRequestController = null; } } } mdLayerShowOrHidden( _layerType: string, _key?: string, _baseid?: string, _checked?: boolean, _isAll?: boolean ): void {} setLegendPointVisible( layerKey: string, anchoPointState: string, checked: boolean ): void { const entities = this.pointLayerRegistry.get(layerKey); if (!entities) return; if (!anchoPointState) { // 第一步:全部隐藏(配合 restoreLayerLegendVisibility 两步走:先全隐藏再按图例逐项恢复) entities.forEach(entity => { (entity as any)._legendVisible = false; this.applyEntityVisibility(entity); }); this.requestRefreshPointLabelVisibility(true); this.rebuildBatchPopupsIfNeeded(); return; } // 按 anchoPointState 字段匹配并设置可见性 entities.forEach(entity => { const rawData = (entity as any)._rawData; if (rawData && rawData.anchoPointState === anchoPointState) { (entity as any)._legendVisible = checked; this.applyEntityVisibility(entity); } }); // 图例切换后立即重算碰撞(同步执行,避免闪烁) this.requestRefreshPointLabelVisibility(true); this.rebuildBatchPopupsIfNeeded(); } // ==================== 基地裁切辅助方法 ==================== /** 获取 GeoJSON 数据 */ private async fetchGeoJson(url: string, signal?: AbortSignal): Promise { const response = await fetch(url, { signal }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } /** 批量设置全部锚点的 _regionVisible 标志 */ private setAllPointsRegionVisible(visible: boolean): void { this.pointLayerRegistry.forEach(entities => { entities.forEach(entity => { (entity as any)._regionVisible = visible; this.applyEntityVisibility(entity); }); }); } /** 根据 GeoJSON 边界批量更新锚点的区域显隐状态(射线法) */ private filterPointsByRegion(regionCoords: number[][][]): void { if (!regionCoords.length) { console.warn('无法解析区域边界,显示所有锚点'); this.setAllPointsRegionVisible(true); return; } this.pointLayerRegistry.forEach((_entities, layerKey) => { this.filterPointsByRegionForLayer(layerKey, regionCoords); }); } /** 对单个图层应用基地裁切过滤 */ private filterPointsByRegionForLayer( layerKey: string, regionCoords: number[][][] ): void { const entities = this.pointLayerRegistry.get(layerKey); if (!entities || !regionCoords.length) return; entities.forEach(entity => { const rawData = (entity as any)._rawData; if (!rawData) { (entity as any)._regionVisible = false; this.applyEntityVisibility(entity); return; } const lon = Number(rawData.lgtd); const lat = Number(rawData.lttd); if (!isFinite(lon) || !isFinite(lat)) { (entity as any)._regionVisible = false; this.applyEntityVisibility(entity); return; } (entity as any)._regionVisible = this.isPointInPolygon( lon, lat, regionCoords ); this.applyEntityVisibility(entity); }); } /** 根据多边形坐标飞行相机到基地边界 */ private fitViewToGeoJson(regionCoords: number[][][]): void { if (!this.viewer) return; try { if (!regionCoords.length) return; // 计算所有坐标的经纬度范围 let minLon = Infinity; let maxLon = -Infinity; let minLat = Infinity; let maxLat = -Infinity; regionCoords.forEach(ring => { ring.forEach(([lon, lat]) => { if (lon < minLon) minLon = lon; if (lon > maxLon) maxLon = lon; if (lat < minLat) minLat = lat; if (lat > maxLat) maxLat = lat; }); }); if (!isFinite(minLon)) return; const rectangle = Cesium.Rectangle.fromDegrees( minLon, minLat, maxLon, maxLat ); this.viewer.camera.flyTo({ destination: rectangle, duration: 1.0, complete: () => this.requestRender() }); } catch (error) { console.error('调整3D视野失败:', error); } } /** 应用基地裁切:globe.clippingPolygons 直接裁切地球渲染 */ private createMaskPolygon(regionCoords: number[][][]): void { if (!this.viewer) return; // 仅清理旧边框(不重置 clipping/背景色/skyBox,避免 GPU 状态反复重建) this.maskPolygonEntities.forEach(entity => { if (!this.viewer!.isDestroyed()) { this.viewer!.entities.remove(entity); } }); this.maskPolygonEntities = []; if (!regionCoords.length) return; const cartesianRings = regionCoords.map(ring => Cesium.Cartesian3.fromDegreesArray( ring.flatMap(([lon, lat]) => [lon, lat]) ) ); // 首次进入裁切时保存原始状态 if (!this.originalBgColor) { this.originalBgColor = this.viewer.scene.backgroundColor.clone(); this.skyBoxShown = this.viewer.scene.skyBox?.show ?? true; this.skyAtmosphereShown = this.viewer.scene.skyAtmosphere?.show ?? true; } // 边界外设为白色 if (this.viewer.scene.skyBox) this.viewer.scene.skyBox.show = false; if (this.viewer.scene.skyAtmosphere) this.viewer.scene.skyAtmosphere.show = false; this.viewer.scene.backgroundColor = Cesium.Color.WHITE; // 直接设新裁切(不经过空集合,避免 GPU shader 重复编译) this.viewer.scene.globe.clippingPolygons = new Cesium.ClippingPolygonCollection({ polygons: cartesianRings.map( positions => new Cesium.ClippingPolygon({ positions }) ), inverse: true, enabled: true }); // 绘制基地边界线(双线视觉效果:紫色 + 浅紫色) const borderStyles = [ { width: 3, color: '#6D64DF' }, { width: 1.5, color: '#CCC9F4' } ]; borderStyles.forEach(({ width, color }) => { cartesianRings.forEach(positions => { this.maskPolygonEntities.push( this.viewer!.entities.add({ polyline: { positions, width, material: Cesium.Color.fromCssColorString(color), clampToGround: true } }) ); }); }); this.requestRender(); this.viewer.scene.screenSpaceCameraController.maximumZoomDistance = 6_400_000; } /** 清除遮罩多边形 */ private clearMaskPolygon(): void { if (!this.viewer) return; // 解除 globe 裁切 this.viewer.scene.globe.clippingPolygons = new Cesium.ClippingPolygonCollection(); // 恢复背景色、星空、大气层 if (this.originalBgColor) { this.viewer.scene.backgroundColor = this.originalBgColor; this.originalBgColor = null; } if (this.viewer.scene.skyBox) { this.viewer.scene.skyBox.show = this.skyBoxShown; } if (this.viewer.scene.skyAtmosphere) { this.viewer.scene.skyAtmosphere.show = this.skyAtmosphereShown; } this.viewer.scene.screenSpaceCameraController.maximumZoomDistance = Infinity; this.maskPolygonEntities.forEach(entity => { if (!this.viewer!.isDestroyed()) { this.viewer!.entities.remove(entity); } }); this.maskPolygonEntities = []; } /** 从 GeoJSON 中提取多边形环坐标 */ private extractPolygonCoords(geoJson: any): number[][][] { if (!geoJson?.features?.length) return []; const coords: number[][][] = []; geoJson.features.forEach((feature: any) => { const geometry = feature.geometry; if (!geometry) return; if (geometry.type === 'Polygon') { coords.push(...geometry.coordinates); } else if (geometry.type === 'MultiPolygon') { geometry.coordinates.forEach((polygon: number[][][]) => { coords.push(...polygon); }); } }); return coords; } /** 射线法判断点是否在任意一个多边形环内 */ private isPointInPolygon( lon: number, lat: number, polygons: number[][][] ): boolean { for (const polygon of polygons) { if (this.isPointInRing(lon, lat, polygon)) { return true; } } return false; } /** 射线法判断点是否在单个闭合环内 */ private isPointInRing(lon: number, lat: number, ring: number[][]): boolean { let inside = false; const n = ring.length; for (let i = 0, j = n - 1; i < n; j = i++) { const xi = ring[i][0]; const yi = ring[i][1]; const xj = ring[j][0]; const yj = ring[j][1]; const intersect = yi > lat !== yj > lat && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi; if (intersect) inside = !inside; } return inside; } /** * 组合计算可见性(3D 解耦模型): * - entity.show = 基础可见(layer && legend && region),保持 entity 始终"在作用域内" * - entity.billboard.show = 基础可见 && 图标碰撞通过 * - entity.label.show = 基础可见 && 标签碰撞通过(图标碰撞不影响标签) */ private applyEntityVisibility(entity: Cesium.Entity): void { const e = entity as any; const baseVisible = e._layerVisible && e._legendVisible && e._regionVisible; // entity 整体始终基于 layer/legend/region(不受碰撞影响,保证 popup/click 等交互不受碰撞干扰) entity.show = baseVisible; // 图标单独受碰撞控制 if (entity.billboard) { (entity.billboard as any).show = baseVisible && e._iconCollisionVisible !== false; } // 标签单独受碰撞控制(不与图标碰撞联动) if (entity.label) { (entity.label as any).show = baseVisible && e._labelCollisionVisible !== false; } } // ==================== Step 6: 碰撞检测 ==================== /** * 触发碰撞检测刷新(带 rAF 节流,与 2D requestRefreshPointLabelVisibility 对齐) */ private requestRefreshPointLabelVisibility( immediate = false, debounceMs = 0 ) { if (this.labelVisibilityDebounceTimerId !== null) { window.clearTimeout(this.labelVisibilityDebounceTimerId); this.labelVisibilityDebounceTimerId = null; } if (immediate) { if (this.collisionRefreshFrameId !== null) { cancelAnimationFrame(this.collisionRefreshFrameId); this.collisionRefreshFrameId = null; } this.refreshPointLabelVisibility(); return; } if (debounceMs > 0) { this.labelVisibilityDebounceTimerId = window.setTimeout(() => { this.labelVisibilityDebounceTimerId = null; this.requestRefreshPointLabelVisibility(); }, debounceMs); return; } if (this.collisionRefreshFrameId !== null) return; this.collisionRefreshFrameId = window.requestAnimationFrame(() => { this.collisionRefreshFrameId = null; this.refreshPointLabelVisibility(); }); } /** * 主碰撞检测:密度门控 → 图标碰撞 → 标签碰撞 * 与 2D refreshPointLabelVisibility 核心算法完全对齐 */ private refreshPointLabelVisibility(): void { if (!this.viewer) return; const currentZoom = this.getCurrentCesiumZoom(); if (currentZoom === undefined || currentZoom === null) return; console.log( `[Cesium Collision] zoom=${currentZoom?.toFixed(2)} layers=${ this.pointLayerRegistry.size }` ); // ===== 阶段0:收集视口内所有基础可见实体 ===== type CollisionCandidate = { entity: Cesium.Entity; iconLeft: number; iconRight: number; iconTop: number; iconBottom: number; hasLabel: boolean; labelLeft: number; labelRight: number; labelTop: number; labelBottom: number; engPriority: number; nearbyPriority: number; densityPriority: number; pixelX: number; pixelY: number; id: string; }; const candidates: CollisionCandidate[] = []; const canvas = this.viewer.scene.canvas; const viewPadding = 120; // 视口外的宽容范围 // 孤立点密度网格:用于 shouldBypassDensityGate const densityPixelGrid = new Map< string, Array<{ entity: Cesium.Entity; layerKey: string; pixelX: number; pixelY: number; }> >(); const dynamicScale = Math.max( 0.5, Math.min(3.0, 0.7 + (currentZoom - 4.5) * 0.08) ); const fontSize = Math.max(10, Math.min(24, 12 * dynamicScale)); // 第一遍:密度门控 + 构建候选 this.pointLayerRegistry.forEach((entities, layerKey) => { entities.forEach(entity => { const e = entity as any; // 基础可见性检查(layer/legend/region) if ( e._layerVisible === false || e._legendVisible === false || e._regionVisible === false ) { e._iconCollisionVisible = false; e._labelCollisionVisible = false; this.applyEntityVisibility(entity); return; } const position = entity.position?.getValue( this.viewer!.clock.currentTime ); if (!position) { e._iconCollisionVisible = false; e._labelCollisionVisible = false; this.applyEntityVisibility(entity); return; } const screenPos = Cesium.SceneTransforms.worldToWindowCoordinates( this.viewer!.scene, position ); if (!screenPos) { e._iconCollisionVisible = false; e._labelCollisionVisible = false; this.applyEntityVisibility(entity); return; } const pixelX = screenPos.x; const pixelY = screenPos.y; // 视口裁剪:太远的实体不参与碰撞,但保留其当前碰撞状态(不重置) if ( pixelX < -viewPadding || pixelY < -viewPadding || pixelX > canvas.clientWidth + viewPadding || pixelY > canvas.clientHeight + viewPadding ) { return; } // 密度 gating(与 2D getFeatureDensityMinZoom / shouldRenderFeatureByDensity 对齐) const densityMinZoom = this.getEntityDensityMinZoom(entity); const bypassDensity = currentZoom < densityMinZoom && this.shouldBypassDensityGateForIsolatedEntity( entity, pixelX, pixelY, layerKey, densityPixelGrid ); const densityVisible = currentZoom >= densityMinZoom || bypassDensity; if (!densityVisible) { e._iconCollisionVisible = false; e._labelCollisionVisible = false; this.applyEntityVisibility(entity); return; } // 构建图标碰撞盒(与 2D 一致:Math.max(14, 24 * dynamicScale)) const iconCollisionSize = Math.max(14, 24 * dynamicScale); const iconLeft = pixelX - iconCollisionSize / 2; const iconRight = pixelX + iconCollisionSize / 2; const iconTop = pixelY - iconCollisionSize / 2; const iconBottom = pixelY + iconCollisionSize / 2; // 构建标签碰撞盒 const labelText: string = e._labelText || ''; const hasLabel = !!labelText; let labelLeft = 0; let labelRight = 0; let labelTop = 0; let labelBottom = 0; if (hasLabel) { const lines = labelText.split('\n'); const maxLineLength = Math.max( ...lines.map((l: string) => l.length), 1 ); const labelLineCount = lines.length; // 标签碰撞偏移与 2D getPointLabelCollisionOffsetY 对齐 const labelOffsetY = labelLineCount > 1 ? -36 * dynamicScale : -28 * dynamicScale; const estimatedWidth = maxLineLength * fontSize * 0.6 + 16; const estimatedHeight = labelLineCount * (fontSize + 4) + 8; const centerY = pixelY + labelOffsetY; labelLeft = pixelX - estimatedWidth / 2; labelRight = pixelX + estimatedWidth / 2; labelTop = centerY - estimatedHeight / 2; labelBottom = centerY + estimatedHeight / 2; } const entityId = String(entity.id || ''); const rawData: Record = e._rawData || {}; candidates.push({ entity, iconLeft, iconRight, iconTop, iconBottom, hasLabel, labelLeft, labelRight, labelTop, labelBottom, engPriority: this.getEntityEngRenderPriority(entity), nearbyPriority: Number(rawData._nearbyPriority || 9999), densityPriority: this.getEntityDensityPriority(entity), pixelX, pixelY, id: entityId }); }); }); if (candidates.length === 0) return; // ===== 阶段1:排序(eng优先级 > 近邻点优先级 > 密度优先级 > Y轴 > ID) ===== candidates.sort((left, right) => { if (left.engPriority !== right.engPriority) { return right.engPriority - left.engPriority; } if (left.nearbyPriority !== right.nearbyPriority) { return left.nearbyPriority - right.nearbyPriority; } if (left.densityPriority !== right.densityPriority) { return left.densityPriority - right.densityPriority; } if (left.pixelY !== right.pixelY) { return left.pixelY - right.pixelY; } return left.id.localeCompare(right.id); }); // ===== 阶段2:图标碰撞(AABB + 空间网格加速) ===== const iconPlacedGrid = new Map< string, Array<{ left: number; right: number; top: number; bottom: number; }> >(); candidates.forEach(candidate => { const iconRect = { left: candidate.iconLeft, right: candidate.iconRight, top: candidate.iconTop, bottom: candidate.iconBottom }; const hasCollision = this.findCollidingRectInGrid( iconRect, iconPlacedGrid, this.COLLISION_GRID_CELL_SIZE, placed => this.checkIconsAabbCollision(placed, iconRect) ); const iconVisible = !hasCollision; (candidate.entity as any)._iconCollisionVisible = iconVisible; if (iconVisible) { this.addRectToCollisionGrid( iconRect, iconPlacedGrid, this.COLLISION_GRID_CELL_SIZE ); } }); // ===== 阶段3:标签碰撞(先避让图标,再避让标签) ===== const labelCandidates = [...candidates].sort((left, right) => { if (left.engPriority !== right.engPriority) return right.engPriority - left.engPriority; if (left.nearbyPriority !== right.nearbyPriority) return left.nearbyPriority - right.nearbyPriority; if (left.pixelY !== right.pixelY) return left.pixelY - right.pixelY; if (left.pixelX !== right.pixelX) return left.pixelX - right.pixelX; return left.id.localeCompare(right.id); }); const labelPlacedGrid = new Map< string, Array<{ left: number; right: number; top: number; bottom: number; }> >(); labelCandidates.forEach(candidate => { const e = candidate.entity as any; if (!candidate.hasLabel) { e._labelCollisionVisible = false; return; } // 3D 解耦:图标被碰撞隐藏不影响标签显示 // 标签仍会避让已放置的图标(见下方 iconPlacedGrid 检查) const labelRect = { left: candidate.labelLeft, right: candidate.labelRight, top: candidate.labelTop, bottom: candidate.labelBottom }; // 先避让已放置的图标(缩小保护区4px,允许边缘擦过) const hasIconCollision = this.findCollidingRectInGrid( labelRect, iconPlacedGrid, this.COLLISION_GRID_CELL_SIZE, iconRect => { const inset = 4; const narrowed = { left: iconRect.left + inset, right: iconRect.right - inset, top: iconRect.top + inset, bottom: iconRect.bottom - inset }; return !( narrowed.right <= labelRect.left || narrowed.left >= labelRect.right || narrowed.bottom <= labelRect.top || narrowed.top >= labelRect.bottom ); } ); if (hasIconCollision) { e._labelCollisionVisible = false; return; } // 再避让已放置的标签 const hasLabelCollision = this.findCollidingRectInGrid( labelRect, labelPlacedGrid, this.COLLISION_GRID_CELL_SIZE, placed => this.checkLabelAabbCollision(placed, labelRect) ); const labelVisible = !hasLabelCollision; e._labelCollisionVisible = labelVisible; if (labelVisible) { this.addRectToCollisionGrid( labelRect, labelPlacedGrid, this.COLLISION_GRID_CELL_SIZE ); } }); // ===== 阶段4:统一应用可见性变化 ===== let iconVisibleCount = 0; let labelVisibleCount = 0; candidates.forEach(candidate => { const e = candidate.entity as any; if (e._iconCollisionVisible) iconVisibleCount++; if (e._labelCollisionVisible) labelVisibleCount++; this.applyEntityVisibility(candidate.entity); }); console.log( `[Cesium Collision] 候选=${candidates.length} 图标可见=${iconVisibleCount} 标签可见=${labelVisibleCount}` ); } // ==================== 碰撞检测辅助:密度门控 ==================== /** 从实体原始数据中提取 density 字段(密度分档值,非物理距离) */ private getEntityDensityValue(entity: Cesium.Entity): number | null { const rawData = (entity as any)._rawData; if (!rawData) return null; const rawDistance = rawData.distance; if ( rawDistance === undefined || rawDistance === null || rawDistance === '' ) { return null; } const densityValue = Number(rawDistance); return Number.isFinite(densityValue) ? densityValue : null; } /** 密度优先级(档位越低越优先,与 2D getFeatureDensityPriority 对齐) */ private getEntityDensityPriority(entity: Cesium.Entity): number { const densityValue = this.getEntityDensityValue(entity); const densityDisplayRules = getNearbyPointDensityDisplayRules(); if (densityValue === null) { return densityDisplayRules.length; } const matchedRuleIndex = densityDisplayRules.findIndex(rule => { return densityValue >= rule.minDensityValue; }); return matchedRuleIndex >= 0 ? matchedRuleIndex : densityDisplayRules.length; } /** 根据密度值获取最低显示缩放级别(与 2D getFeatureDensityMinZoom 对齐) */ private getEntityDensityMinZoom(entity: Cesium.Entity): number { const densityValue = this.getEntityDensityValue(entity); if (densityValue === null) return 0; const densityDisplayRules = getNearbyPointDensityDisplayRules(); const matchedRule = densityDisplayRules.find(rule => { return densityValue >= rule.minDensityValue; }); if (matchedRule) return matchedRule.minZoom; return densityDisplayRules.at(-1)?.minZoom ?? 0; } /** * 孤立点密度豁免:如果当前视口内 56px 范围内没有其他同图层可见点, * 则跳过密度门控直接显示(与 2D shouldBypassDensityGateForIsolatedFeature 对齐) */ private shouldBypassDensityGateForIsolatedEntity( entity: Cesium.Entity, pixelX: number, pixelY: number, layerKey: string, densityPixelGrid: Map< string, Array<{ entity: Cesium.Entity; layerKey: string; pixelX: number; pixelY: number; }> > ): boolean { // 先登记当前实体到密度网格 const gridKey = `${layerKey}:${Math.floor( pixelX / this.DENSITY_ISOLATION_THRESHOLD )}:${Math.floor(pixelY / this.DENSITY_ISOLATION_THRESHOLD)}`; const bucket = densityPixelGrid.get(gridKey) || []; bucket.push({ entity, layerKey, pixelX, pixelY }); densityPixelGrid.set(gridKey, bucket); // 检查周围 3x3 网格单元 const checkedKeys = new Set(); for (let dx = -1; dx <= 1; dx++) { for (let dy = -1; dy <= 1; dy++) { const neighborKey = `${layerKey}:${ Math.floor(pixelX / this.DENSITY_ISOLATION_THRESHOLD) + dx }:${Math.floor(pixelY / this.DENSITY_ISOLATION_THRESHOLD) + dy}`; if (checkedKeys.has(neighborKey)) continue; checkedKeys.add(neighborKey); const neighborBucket = densityPixelGrid.get(neighborKey); if (!neighborBucket?.length) continue; for (const neighbor of neighborBucket) { if (neighbor.entity === entity) continue; const dist = Math.hypot( neighbor.pixelX - pixelX, neighbor.pixelY - pixelY ); if (dist <= this.DENSITY_ISOLATION_THRESHOLD) { return false; // 有近邻点,不豁免 } } } } return true; // 孤立点,豁免密度门控 } // ==================== 碰撞检测辅助:eng 优先级 ==================== /** 判断是否为 eng 点(与 2D isFeatureEng 对齐) */ private isEntityEng(entity: Cesium.Entity): boolean { const e = entity as any; const rawData: Record = e._rawData || {}; const layerKey = String(e._layerKey || '').toLowerCase(); const sttpMap = String( rawData.sttpMap || rawData._sttpMap || '' ).toUpperCase(); const sttpCode = String( rawData.sttpCode || rawData.sttp || '' ).toUpperCase(); return ( layerKey.includes('eng_point') || sttpMap === 'ENG' || sttpMap === 'ENG2' || sttpCode === 'ENG' ); } /** 判断是否为 eng 告警点(与 2D isFeatureEngAlarm 对齐) */ private isEntityEngAlarm(entity: Cesium.Entity): boolean { const e = entity as any; const rawData: Record = e._rawData || {}; const layerKey = String(e._layerKey || '').toLowerCase(); const legendState = String(rawData.anchoPointState || '') .trim() .toLowerCase(); return ( layerKey.includes('eng_alarm_point') || layerKey.includes('alarm_range') || legendState.startsWith('alarm_range_') || legendState.startsWith('large_eng_built_alarm_range_') || legendState.startsWith('mid_eng_built_alarm_range_') ); } /** 获取 eng 渲染优先级:告警=300, eng=200, 普通=100(与 2D 完全一致) */ private getEntityEngRenderPriority(entity: Cesium.Entity): number { if (this.isEntityEngAlarm(entity)) return 300; return this.isEntityEng(entity) ? 200 : 100; } // ==================== 碰撞检测辅助:空间网格与 AABB ==================== /** 计算矩形覆盖的碰撞网格 cell keys */ private getCollisionGridKeys( rect: { left: number; right: number; top: number; bottom: number; }, cellSize: number ): string[] { const startX = Math.floor(rect.left / cellSize); const endX = Math.floor(rect.right / cellSize); const startY = Math.floor(rect.top / cellSize); const endY = Math.floor(rect.bottom / cellSize); const keys: string[] = []; for (let x = startX; x <= endX; x += 1) { for (let y = startY; y <= endY; y += 1) { keys.push(`${x}:${y}`); } } return keys; } /** 将矩形注册到碰撞网格 */ private addRectToCollisionGrid< T extends { left: number; right: number; top: number; bottom: number; } >(rect: T, grid: Map, cellSize: number) { this.getCollisionGridKeys(rect, cellSize).forEach(key => { const bucket = grid.get(key) || []; bucket.push(rect); grid.set(key, bucket); }); } /** 在碰撞网格中查找与给定矩形碰撞的矩形 */ private findCollidingRectInGrid< T extends { left: number; right: number; top: number; bottom: number; } >( rect: { left: number; right: number; top: number; bottom: number; }, grid: Map, cellSize: number, isCollision: (candidate: T) => boolean ): T | undefined { for (const key of this.getCollisionGridKeys(rect, cellSize)) { const bucket = grid.get(key); if (!bucket?.length) continue; for (const candidate of bucket) { if (isCollision(candidate)) { return candidate; } } } return undefined; } /** 图标碰撞检测:AABB 重叠判定(padding=0,与 2D 图标碰撞一致) */ private checkIconsAabbCollision( left: { left: number; right: number; top: number; bottom: number }, right: { left: number; right: number; top: number; bottom: number } ): boolean { return !( left.right < right.left || left.left > right.right || left.bottom < right.top || left.top > right.bottom ); } /** 标签碰撞检测:AABB 重叠 + padding=4(与 2D checkLabelCollision 对齐) */ private checkLabelAabbCollision( left: { left: number; right: number; top: number; bottom: number }, right: { left: number; right: number; top: number; bottom: number } ): boolean { const padding = this.COLLISION_LABEL_PADDING; return !( left.right + padding < right.left || left.left - padding > right.right || left.bottom + padding < right.top || left.top - padding > right.bottom ); } // ==================== 倾斜摄影(OSGB) ==================== /** * 加载倾斜摄影 3D Tileset 模型 * 如果 viewer 尚未就绪,暂存到队列中,等 5 秒后批量加载 */ addQxsyLayer(item: OSGBItem): void { if (!this.viewer || !this._ready) { // 未就绪:放入等待队列 if (!this._pendingOSGBItems.includes(item)) { this._pendingOSGBItems.push(item); } return; } LoadOSGB(this.viewer, item); } /** * 批量加载所有等待中的倾斜摄影 */ private flushPendingOSGB(): void { if (!this.viewer || this.viewer.isDestroyed()) return; const items = this._pendingOSGBItems.splice(0); console.log(`[Cesium] 开始加载 ${items.length} 个待加载的倾斜摄影模型`); items.forEach(item => LoadOSGB(this.viewer!, item)); } /** * 移除倾斜摄影模型 */ removeQxsyLayer(item: OSGBItem): void { if (!this.viewer) return; // 从待加载队列中移除,防止已被移除的模型在 flushPendingOSGB 时又被加载 this._pendingOSGBItems = this._pendingOSGBItems.filter(i => i !== item); removeQxsy(this.viewer, item); } /** * 倾斜摄影显隐切换 */ qxsyChangeClick(item: OSGBItem, checked: boolean): void { if (!this.viewer) return; osgbChangeClick(this.viewer, item, checked); } /** * 飞行定位到倾斜摄影模型 */ qxsyToPosition(item: OSGBItem): void { if (!this.viewer) return; osgbLocation(this.viewer, item); } }