WholeProcessPlatform/frontend/src/components/gis/osgbUtils.ts
2026-07-17 18:03:34 +08:00

748 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 倾斜摄影(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<OSGBItem, OSGBInstanceState>();
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);
};
/**
* 从场景移除 tilesetPrimitiveCollection.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;
}
}
};
};