/** * 倾斜摄影(OSGB)工具函数 * 适配自旧项目 osgbTool.ts,核心逻辑保持一致 * * 使用字段: url, boundary, location, height, accuracy */ import * as turf from '@turf/turf'; import * as Cesium from 'cesium'; import { getToken } from '@/utils/auth'; // ==================== 类型定义 ==================== export interface OSGBLocation { destination: number[]; // [x, y, z] 或 [lng, lat, height] orientation: { heading: number; pitch: number; roll: number; }; center: number[]; } export interface OSGBItem { /** 3D Tileset 服务地址 */ url: string; /** GeoJSON Polygon 边界(用于裁切) */ boundary?: any; /** 定位信息 */ location?: OSGBLocation | string; /** 模型高度偏移 */ height?: number; /** 屏幕空间误差 (默认 16) */ accuracy?: number; /** 内部状态 - 不要手动设置 */ osgbtitles?: any; cartesian3?: any; clippingPlanes?: any; eventListener?: any; _loading?: boolean; _loadId?: number; /** 用户手动关闭开关标志:为 true 时不自动加载/显示,只做显隐切换 */ _switchOff?: boolean; } // ==================== 内部状态(每个 OSGB 实例独立) ==================== interface OSGBInstanceState { destination: Cesium.Cartesian3 | null; orientation: any; timelookat: any; time: number; num: number; witetime: number; timeout: any; } const instanceStates = new WeakMap(); function getState(obj: OSGBItem): OSGBInstanceState { let state = instanceStates.get(obj); if (!state) { state = { destination: null, orientation: null, timelookat: null, time: 0, num: 0, witetime: 0, timeout: undefined }; instanceStates.set(obj, state); } return state; } function clearState(obj: OSGBItem) { const state = instanceStates.get(obj); if (state) { clearTimeout(state.timeout); clearInterval(state.timelookat); } instanceStates.delete(obj); } // ==================== 3D Tileset 场景管理 ==================== /** * 将 tileset 添加到场景 */ const addOSGB = (viewer: Cesium.Viewer, tileset: Cesium.Cesium3DTileset) => { if (!viewer || viewer.isDestroyed()) return; viewer.scene.primitives.add(tileset); }; /** * 从场景移除 tileset(PrimitiveCollection.remove 默认 destroy=true,会销毁 tileset) */ const removeOSGB = (viewer: Cesium.Viewer, tileset: Cesium.Cesium3DTileset) => { if (!viewer || viewer.isDestroyed()) return; viewer.scene.primitives.remove(tileset); }; // ==================== 裁切相关 ==================== /** * 规范化 GeoJSON 输入,提取纯 Geometry * 兼容 Feature / FeatureCollection / Geometry */ const normalizeGeometry = (geo: any): any => { if (!geo) return null; // 已经是 Geometry 类型(有 type 且 coordinates) if (geo.type && geo.coordinates) return geo; // Feature → 提取 geometry if (geo.type === 'Feature' && geo.geometry) return geo.geometry; // FeatureCollection → 取第一个 feature 的 geometry if (geo.type === 'FeatureCollection' && geo.features?.length) { return normalizeGeometry(geo.features[0]); } return null; }; const get_verts_core = (poly: any): Cesium.Cartesian3[] => { const verts_carte3: Cesium.Cartesian3[] = []; if (!poly) return verts_carte3; try { // 规范化输入:兼容 Feature / FeatureCollection / Geometry const geom = normalizeGeometry(poly); if (!geom) return verts_carte3; let verts = turf.coordAll(geom); const clockwiseRing = turf.booleanClockwise(turf.lineString(verts)); if (clockwiseRing) { verts = verts.reverse(); } for (let i = 0; i < verts.length - 1; i++) { const vert = verts[i]; verts_carte3.push(Cesium.Cartesian3.fromDegrees(vert[0], vert[1])); } } catch (e) { console.warn('get_verts_core 解析 boundary 失败:', e); } return verts_carte3; }; const get_clipping_planes = ( points: Cesium.Cartesian3[] ): Cesium.ClippingPlane[] => { const pointsLength = points.length; const clippingPlanes: Cesium.ClippingPlane[] = []; for (let i = 0; i < pointsLength; ++i) { const nextIndex = (i + 1) % pointsLength; let midpoint = Cesium.Cartesian3.add( points[i], points[nextIndex], new Cesium.Cartesian3() ); midpoint = Cesium.Cartesian3.multiplyByScalar(midpoint, 0.5, midpoint); const up = Cesium.Cartesian3.normalize(midpoint, new Cesium.Cartesian3()); let right = Cesium.Cartesian3.subtract( points[nextIndex], midpoint, new Cesium.Cartesian3() ); right = Cesium.Cartesian3.normalize(right, right); let normal = Cesium.Cartesian3.cross(right, up, new Cesium.Cartesian3()); normal = Cesium.Cartesian3.normalize(normal, normal); const originCenteredPlane = new Cesium.Plane(normal, 0.0); const distance = Cesium.Plane.getPointDistance( originCenteredPlane, midpoint ); clippingPlanes.push(new Cesium.ClippingPlane(normal, distance)); } return clippingPlanes; }; const clipping_terrain = ( viewer: Cesium.Viewer, clippingPlanes: Cesium.ClippingPlane[] ) => { const globe = viewer.scene.globe; globe.clippingPlanes = new Cesium.ClippingPlaneCollection({ planes: clippingPlanes, edgeWidth: 0, edgeColor: Cesium.Color.GREEN, enabled: true }); globe.backFaceCulling = true; globe.showSkirts = true; }; const clipping_3dtiles = ( clippingPlanes: Cesium.ClippingPlane[], tileset: Cesium.Cesium3DTileset ) => { // Cesium 1.141: root.transform 是内部属性,安全访问 const rootTransform = (tileset as any)?.root?.transform; if (!rootTransform) return; const invtrans = Cesium.Matrix4.inverse(rootTransform, new Cesium.Matrix4()); for (let i = 0; i < clippingPlanes.length; i++) { clippingPlanes[i] = Cesium.Plane.transform(clippingPlanes[i], invtrans); } tileset.clippingPlanes = new Cesium.ClippingPlaneCollection({ planes: clippingPlanes, unionClippingRegions: true, edgeWidth: 0, edgeColor: Cesium.Color.RED, enabled: false }); }; const Clip3DTile = (boundary: any, tileset: Cesium.Cesium3DTileset) => { try { const geom = normalizeGeometry(boundary); if (!geom) return; const buffered = turf.buffer(geom, 10, { units: 'meters' }); const verts_core = get_verts_core(buffered).reverse(); if (!verts_core.length) return; const planes = get_clipping_planes(verts_core); clipping_3dtiles(planes, tileset); } catch (e) { console.warn('Clip3DTile 裁切失败:', e); } }; const ClipTerrain = (viewer: Cesium.Viewer, osgbObj: OSGBItem) => { // Cesium 1.141: 安全访问 clippingPlanes 内部属性 _planes const existingPlanes = (viewer.scene.globe.clippingPlanes as any)?._planes; if ( !osgbObj.clippingPlanes || (existingPlanes && !compareClippingPlanes(osgbObj.clippingPlanes, existingPlanes)) ) { const verts_core = get_verts_core(osgbObj.boundary); if (!verts_core.length) return; const planes = get_clipping_planes(verts_core); clipping_terrain(viewer, planes); osgbObj.clippingPlanes = planes; } }; const removeClip = (viewer: Cesium.Viewer, osgbObj: OSGBItem) => { if (osgbObj.clippingPlanes) { removeClippingPlanes(viewer); osgbObj.clippingPlanes = undefined; } }; const removeClippingPlanes = (viewer: Cesium.Viewer) => { if (!Cesium.defined(viewer) || viewer.isDestroyed()) { return; } if (Cesium.defined(viewer.scene.globe.clippingPlanes)) { viewer.scene.globe.clippingPlanes.removeAll(); } }; const compareClippingPlanes = (planes1: any, planes2: any): boolean => { let result = true; if (planes1.length === planes2?.length) { for (let index = 0; index < planes1.length; index++) { if (planes1[index]._distance !== planes2[index]._distance) { result = false; break; } } } else { result = false; } return result; }; const getMinDistanceOfOSGB = (viewer: Cesium.Viewer): number => { let result = Infinity; const length = viewer.scene.primitives.length; for (let i = 0; i < length; i++) { const p = viewer.scene.primitives.get(i); // Cesium 1.141: _url 和 _distanceToCamera 是内部属性,安全访问 // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((p as any)?._url) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const d = (p as any)?.root?._distanceToCamera; if (d != null && d < result) result = d; } } return result; }; // ==================== 动态显隐 ==================== const dynamicSetVisible = (viewer: Cesium.Viewer, osgbObj: OSGBItem) => { if (!Cesium.defined(viewer) || viewer.isDestroyed()) return; if (!Cesium.defined(osgbObj?.cartesian3)) return; let cameraPosition: Cesium.Cartesian3 | undefined; if (viewer.trackedEntity) { cameraPosition = viewer.trackedEntity.position?.getValue( viewer.clock.currentTime ); } if (!cameraPosition) { cameraPosition = viewer.camera.position.clone(); } const distance = Cesium.Cartesian3.distance( osgbObj.cartesian3, cameraPosition ); if (distance < 12000) { // 用户手动关闭了开关 → 不自动加载 if (osgbObj._switchOff) return; // 匹配旧代码逻辑:osgbObj.osgbtitles 同步设置为 nonClassificationTileset 防重入 // 新代码用 _loading + _loadId 实现同样的效果(因为 fromUrl 是异步的) if (!osgbObj.osgbtitles && !osgbObj._loading) { osgbObj._loading = true; osgbObj._loadId = (osgbObj._loadId || 0) + 1; const loadId = osgbObj._loadId; const url = osgbObj.url.includes('?') ? osgbObj.url + `&token=${getToken()}` : osgbObj.url + `?token=${getToken()}`; Cesium.Cesium3DTileset.fromUrl(url, { maximumScreenSpaceError: Number(osgbObj?.accuracy) || 16, show: true, maximumMemoryUsage: 128 }) .then((tileset: Cesium.Cesium3DTileset) => { // 竞态保护:版本号变了(removeQxsy 被调用或新一轮加载开始)→ 销毁退出 if ( osgbObj._loadId !== loadId || !Cesium.defined(viewer) || viewer.isDestroyed() ) { tileset.destroy(); return; } osgbObj._loading = false; if (!Cesium.defined(tileset)) return; tileset.maximumScreenSpaceError = 32; // height 向上偏移:模型几何自带高程,height 只是微调(如 44m) if (osgbObj.height != 0) { const bs = Cesium.Cartographic.fromCartesian( tileset.boundingSphere.center ); const surface = Cesium.Cartesian3.fromRadians( bs.longitude, bs.latitude, 0.0 ); const offset = Cesium.Cartesian3.fromRadians( bs.longitude, bs.latitude, osgbObj.height ); const translation = Cesium.Cartesian3.subtract( offset, surface, new Cesium.Cartesian3() ); tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation); } addOSGB(viewer, tileset); osgbObj.osgbtitles = tileset; }) .catch((e: Error) => { osgbObj._loading = false; console.log('dynamicSetVisible 加载倾斜摄影报错', e); }); } } else if (distance > 13000) { // 匹配旧代码逻辑:镜头拉远 → 销毁 tileset 释放内存 if (osgbObj.osgbtitles) { removeOSGB(viewer, osgbObj.osgbtitles); osgbObj.osgbtitles = undefined; } // 不重置 _switchOff,开关状态由用户手动控制,不受距离影响 } }; // ==================== 地形动态裁切 ==================== const dynamicClipTerrain = (viewer: Cesium.Viewer, osgbObj: OSGBItem) => { if (!osgbObj?.osgbtitles) { removeClip(viewer, osgbObj); return; } // Cesium 1.141: root 和 _distanceToCamera 是内部属性,安全访问 const root = (osgbObj.osgbtitles as any)?.root; if (!Cesium.defined(root)) { removeClip(viewer, osgbObj); if (osgbObj.osgbtitles?.clippingPlanes?.enabled) { osgbObj.osgbtitles.clippingPlanes.enabled = false; } return; } if (!osgbObj?.osgbtitles?.show) { removeClip(viewer, osgbObj); if (osgbObj.osgbtitles?.clippingPlanes?.enabled) { osgbObj.osgbtitles.clippingPlanes.enabled = false; } return; } const distance = root._distanceToCamera; const tilesLoaded = osgbObj.osgbtitles.tilesLoaded; if (osgbObj.boundary && tilesLoaded) { if (distance != null && getMinDistanceOfOSGB(viewer) === distance) { ClipTerrain(viewer, osgbObj); if ( osgbObj.osgbtitles?.clippingPlanes && !osgbObj.osgbtitles.clippingPlanes.enabled ) { osgbObj.osgbtitles.clippingPlanes.enabled = true; } } else { removeClip(viewer, osgbObj); if (osgbObj.osgbtitles?.clippingPlanes?.enabled) { osgbObj.osgbtitles.clippingPlanes.enabled = false; } } } }; // ==================== 公开 API ==================== /** * 加载倾斜摄影 3D Tileset 模型 * * Cesium 1.141 中必须使用 Cesium3DTileset.fromUrl() 静态方法, * new Cesium3DTileset({url}) 不会触发网络请求。 */ export const LoadOSGB = (viewer: Cesium.Viewer, osgbObj: OSGBItem) => { if (!osgbObj.url) return; // 已加载则跳过 if (osgbObj.osgbtitles && !osgbObj.osgbtitles.isDestroyed?.()) return; // 正在加载中也跳过(防止重复调用 fromUrl) if (osgbObj._loading) return; osgbObj._loading = true; // 自增版本号:每次 LoadOSGB 都 +1,.then() 里靠版本号判断是否已失效 osgbObj._loadId = (osgbObj._loadId || 0) + 1; const loadId = osgbObj._loadId; const url = osgbObj.url.includes('?') ? osgbObj.url + `&token=${getToken()}` : osgbObj.url + `?token=${getToken()}`; Cesium.Cesium3DTileset.fromUrl(url, { maximumScreenSpaceError: Number(osgbObj?.accuracy) || 16, show: true, maximumMemoryUsage: 128 }) .then((tileset: Cesium.Cesium3DTileset) => { // 竞态保护:版本号变了 → 销毁退出 if ( osgbObj._loadId !== loadId || !Cesium.defined(viewer) || viewer.isDestroyed() ) { tileset.destroy(); return; } if (!Cesium.defined(tileset)) return; osgbObj._loading = false; tileset.maximumScreenSpaceError = 32; // height 向上偏移:模型几何自带高程,height 只是微调(如 44m) if (osgbObj.height != 0) { const bs = Cesium.Cartographic.fromCartesian( tileset.boundingSphere.center ); const surface = Cesium.Cartesian3.fromRadians( bs.longitude, bs.latitude, 0.0 ); const offset = Cesium.Cartesian3.fromRadians( bs.longitude, bs.latitude, osgbObj.height ); const translation = Cesium.Cartesian3.subtract( offset, surface, new Cesium.Cartesian3() ); tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation); } // 加入场景(匹配旧代码 readyPromise 中 addOSGB) addOSGB(viewer, tileset); osgbObj.osgbtitles = tileset; // 始终设置 cartesian3,保证 dynamicSetVisible 能计算距离 osgbObj.cartesian3 = tileset.boundingSphere.center.clone(); // boundary 裁切(匹配旧代码 readyPromise 中处理) if (osgbObj.boundary) { if (typeof osgbObj.boundary === 'string') { osgbObj.boundary = JSON.parse(osgbObj.boundary); } Clip3DTile(osgbObj.boundary, tileset); } // 注册 postRender 动态显隐(300ms 节流,匹配旧代码 throttle) let lastTime = 0; osgbObj.eventListener = viewer.scene.postRender.addEventListener(() => { if (!Cesium.defined(viewer) || viewer.isDestroyed()) return; const now = performance.now(); if (now - lastTime < 300) return; lastTime = now; if (!getState(osgbObj).timelookat) { dynamicSetVisible(viewer, osgbObj); } dynamicClipTerrain(viewer, osgbObj); }); }) .catch((e: Error) => { osgbObj._loading = false; console.log('加载倾斜摄影报错', e); }); }; /** * 卸载倾斜摄影模型(完全移除) * 仅在切换 2D 等场景调用,平时开关节用 osgbChangeClick 做显隐 * 匹配旧代码 removeqxsy(osgbTool.ts:233-244):不清理 cartesian3 */ export const removeQxsy = (viewer: Cesium.Viewer, osgbObj: OSGBItem) => { // 递增版本号:使所有进行中的 fromUrl().then() 失效 osgbObj._loadId = (osgbObj._loadId || 0) + 1; removeClippingPlanes(viewer); osgbObj.clippingPlanes = undefined; osgbObj._loading = false; osgbObj._switchOff = undefined; if (typeof osgbObj.eventListener === 'function') { osgbObj.eventListener(); osgbObj.eventListener = undefined; } if (osgbObj.osgbtitles) { removeOSGB(viewer, osgbObj.osgbtitles); osgbObj.osgbtitles = undefined; } // 注意:不清理 cartesian3,匹配旧代码行为 }; /** * 倾斜摄影显隐切换 * - 开关状态全局记录,不受距离影响 * - 12km 以内:执行实际的显隐/加载操作 * - 12km 以外:仅记录状态,不做 tileset 操作(tileset 不存在或已销毁) * - 仅切换 2D 时才通过 removeQxsy 真正销毁 */ export const osgbChangeClick = ( viewer: Cesium.Viewer, osgbObj: OSGBItem, checked: boolean ) => { // 先记录开关状态(无论距离远近) osgbObj._switchOff = !checked; // 12km 以外:只记状态不操作 tileset(此时 tileset 可能不存在或已销毁) if (osgbObj.cartesian3) { const cameraPosition = viewer.camera.position.clone(); const distance = Cesium.Cartesian3.distance( osgbObj.cartesian3, cameraPosition ); if (distance >= 12000) return; } if (checked) { // 开关 ON:已有未销毁的 tileset → 直接显示;否则加载 if (osgbObj.osgbtitles && !osgbObj.osgbtitles.isDestroyed?.()) { osgbObj.osgbtitles.show = true; } else { LoadOSGB(viewer, osgbObj); } } else { // 开关 OFF:隐藏 tileset,移除 terrain 裁切 if (osgbObj.osgbtitles && !osgbObj.osgbtitles.isDestroyed?.()) { osgbObj.osgbtitles.show = false; } removeClip(viewer, osgbObj); } }; /** * 解析 location 字段为结构化定位信息 */ const parseLocation = (location: any): OSGBLocation | null => { if (!location) return null; // 如果已经是对象,直接用 if (typeof location === 'object' && location.destination) { return location as OSGBLocation; } // 如果是字符串,尝试解析 if (typeof location === 'string') { try { const jsonString = location.replace(/\n/g, ''); const startIndex = jsonString.indexOf('{'); const endIndex = jsonString.lastIndexOf('}'); if (startIndex === -1 || endIndex === -1) return null; const extractedData = jsonString.substring(startIndex, endIndex + 1); const parsed = eval('(' + extractedData + ')'); return { destination: parsed.destination, orientation: parsed.orientation, center: parsed.center }; } catch { return null; } } return null; }; /** * 飞行定位到倾斜摄影模型 * 包含 flyTo + 到达后旋转动画 */ export const osgbLocation = (viewer: Cesium.Viewer, osgbObj: OSGBItem) => { const state = getState(osgbObj); clearTimeout(state.timeout); state.num = 0; state.time = 0; state.witetime = 0; viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY); let center = Cesium.Cartesian3.fromDegrees( 100.271729604817, 35.3107902811941, 2800 ); let heading = Cesium.Math.toRadians(0.0); const pitch = Cesium.Math.toRadians(-30.0); const range = 2000.0; const locationInfor = parseLocation(osgbObj.location); if (!locationInfor) { console.warn('该模型定位坐标未配置!'); return; } // 判断是经纬度还是笛卡尔坐标 const destCoords = locationInfor.destination; if (destCoords[0] <= 180 && destCoords[0] >= -180) { state.destination = Cesium.Cartesian3.fromDegrees( destCoords[0], destCoords[1], destCoords[2] ); } else { state.destination = new Cesium.Cartesian3( destCoords[0], destCoords[1], destCoords[2] ); } state.orientation = locationInfor.orientation; center = Cesium.Cartesian3.fromDegrees( locationInfor.center[0], locationInfor.center[1], locationInfor.center[2] ); if (!state.destination || !state.orientation) return; viewer.camera.cancelFlight(); viewer.camera.flyTo({ destination: state.destination, orientation: state.orientation, complete: () => { state.timeout = setTimeout(() => { rotate(); }, 2000); } }); const rotate = () => { clearInterval(state.timelookat); state.timelookat = setInterval(() => { if (!Cesium.defined(viewer) || viewer.isDestroyed()) { return; } state.witetime += 0.7; if (state.witetime > 180 && state.witetime < 360) { state.time += 0.7; heading = Cesium.Math.toRadians(state.time); viewer.camera.lookAt( center, new Cesium.HeadingPitchRange(heading, pitch, range) ); } else if (state.witetime > 360) { viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY); clearInterval(state.timelookat); state.timelookat = null; viewer.camera.flyTo({ destination: state.destination!, orientation: state.orientation, duration: 2 }); return; } }, 10); }; // 点击 body 两次退出旋转 const body = document.getElementsByTagName('body')[0]; body.onclick = function () { if (viewer && !viewer.isDestroyed() && viewer.camera) { state.num = state.num + 1; if (state.num > 1) { viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY); clearInterval(state.timelookat); clearTimeout(state.timeout); state.timelookat = null; } } }; };