倾斜摄影添加,添加底图地形图
This commit is contained in:
parent
214a8139a0
commit
a7fef183de
@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons-vue": "^7.0.1",
|
"@ant-design/icons-vue": "^7.0.1",
|
||||||
"@element-plus/icons-vue": "^2.0.10",
|
"@element-plus/icons-vue": "^2.0.10",
|
||||||
|
"@turf/turf": "^7.3.5",
|
||||||
"@types/js-cookie": "^3.0.2",
|
"@types/js-cookie": "^3.0.2",
|
||||||
"@univerjs-pro/exchange-client": "0.15.0",
|
"@univerjs-pro/exchange-client": "0.15.0",
|
||||||
"@univerjs/core": "0.14.0",
|
"@univerjs/core": "0.14.0",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -9,9 +9,18 @@ import {
|
|||||||
} from '@/utils/popupHtmlGenerator';
|
} from '@/utils/popupHtmlGenerator';
|
||||||
import { useModelStore } from '@/store/modules/model';
|
import { useModelStore } from '@/store/modules/model';
|
||||||
import { getNearbyPointDensityDisplayRules } from '@/modules/map/domain/nearby-point-rules';
|
import { getNearbyPointDensityDisplayRules } from '@/modules/map/domain/nearby-point-rules';
|
||||||
|
import {
|
||||||
|
LoadOSGB,
|
||||||
|
removeQxsy,
|
||||||
|
osgbChangeClick,
|
||||||
|
osgbLocation,
|
||||||
|
type OSGBItem
|
||||||
|
} from './osgbUtils';
|
||||||
|
|
||||||
export class MapCesium implements MapInterface {
|
export class MapCesium implements MapInterface {
|
||||||
private viewer: Cesium.Viewer | null = null;
|
private viewer: Cesium.Viewer | null = null;
|
||||||
|
private _ready = false;
|
||||||
|
private _pendingOSGBItems: OSGBItem[] = [];
|
||||||
private clickEventHandler: Cesium.ScreenSpaceEventHandler | null = null;
|
private clickEventHandler: Cesium.ScreenSpaceEventHandler | null = null;
|
||||||
private popupElement: HTMLElement | null = null;
|
private popupElement: HTMLElement | null = null;
|
||||||
private hoveredEntityId: string | null = null;
|
private hoveredEntityId: string | null = null;
|
||||||
@ -100,6 +109,7 @@ export class MapCesium implements MapInterface {
|
|||||||
this.containerId = container.id;
|
this.containerId = container.id;
|
||||||
this.containerElement = container;
|
this.containerElement = container;
|
||||||
this.showLoadingOverlay(container);
|
this.showLoadingOverlay(container);
|
||||||
|
const token = 'bearer fa8aa37c-1e52-4631-a699-625b4147ace8';
|
||||||
|
|
||||||
this.viewer = new Cesium.Viewer(container, {
|
this.viewer = new Cesium.Viewer(container, {
|
||||||
animation: false,
|
animation: false,
|
||||||
@ -114,35 +124,33 @@ export class MapCesium implements MapInterface {
|
|||||||
infoBox: false,
|
infoBox: false,
|
||||||
selectionIndicator: false,
|
selectionIndicator: false,
|
||||||
shouldAnimate: true,
|
shouldAnimate: true,
|
||||||
requestRenderMode: true,
|
requestRenderMode: false,
|
||||||
targetFrameRate: 30,
|
targetFrameRate: 30,
|
||||||
terrainProvider: new Cesium.EllipsoidTerrainProvider()
|
terrainProvider: new Cesium.EllipsoidTerrainProvider() // 默认不使用地形
|
||||||
});
|
});
|
||||||
|
|
||||||
this.viewer.cesiumWidget.creditContainer.style.display = 'none';
|
this.viewer.cesiumWidget.creditContainer.style.display = 'none';
|
||||||
const layers = this.viewer.imageryLayers;
|
const layers = this.viewer.imageryLayers;
|
||||||
layers.removeAll();
|
layers.removeAll();
|
||||||
const arcgisUrl =
|
|
||||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/';
|
|
||||||
|
|
||||||
// const provider = await Cesium.ArcGisMapServerImageryProvider.fromUrl(
|
|
||||||
// arcgisUrl,
|
|
||||||
// {
|
|
||||||
// enablePickFeatures: false,
|
|
||||||
// maximumLevel: 19
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
const provider = new Cesium.UrlTemplateImageryProvider({
|
const provider = new Cesium.UrlTemplateImageryProvider({
|
||||||
url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||||
maximumLevel: 19,
|
maximumLevel: 19,
|
||||||
credit:
|
credit:
|
||||||
'Esri, DigitalGlobe, GeoEye, i-cubed, USDA FSA, USGS, AEX, Getmapping, Aerogrid, IGN, IGP, swisstopo, and the GIS User Community'
|
'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 = layers.addImageryProvider(provider);
|
||||||
this.imageryBaseLayer.show = true;
|
this.imageryBaseLayer.show = true;
|
||||||
|
|
||||||
this.viewer.scene.globe.depthTestAgainstTerrain = false;
|
this.viewer.scene.globe.depthTestAgainstTerrain = true;
|
||||||
this.viewer.scene.globe.show = true;
|
this.viewer.scene.globe.show = true;
|
||||||
this.viewer.scene.fog.enabled = false;
|
this.viewer.scene.fog.enabled = false;
|
||||||
this.viewer.scene.fxaa = false;
|
this.viewer.scene.fxaa = false;
|
||||||
@ -153,7 +161,14 @@ export class MapCesium implements MapInterface {
|
|||||||
this.setupClickHandler();
|
this.setupClickHandler();
|
||||||
this.setupCameraPopupReset();
|
this.setupCameraPopupReset();
|
||||||
|
|
||||||
|
this._ready = true;
|
||||||
console.log('[Cesium] init complete, viewer ready');
|
console.log('[Cesium] init complete, viewer ready');
|
||||||
|
|
||||||
|
// 初始化完成后等 5 秒再加载倾斜摄影,确保渲染管线稳定
|
||||||
|
setTimeout(() => {
|
||||||
|
this.flushPendingOSGB();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
return this.viewer;
|
return this.viewer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.hideLoadingOverlay();
|
this.hideLoadingOverlay();
|
||||||
@ -525,10 +540,40 @@ export class MapCesium implements MapInterface {
|
|||||||
this.popupElement.innerHTML = '';
|
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 定位到屏幕坐标 */
|
/** 根据实体世界坐标把 popup 定位到屏幕坐标 */
|
||||||
private updatePopupPosition(position: Cesium.Cartesian3): void {
|
private updatePopupPosition(position: Cesium.Cartesian3): void {
|
||||||
if (!this.viewer || !this.popupElement) return;
|
if (!this.viewer || !this.popupElement) return;
|
||||||
|
|
||||||
|
// 被地形遮挡时隐藏 popup
|
||||||
|
if (this.isPositionBehindTerrain(position)) {
|
||||||
|
this.hidePopup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const canvasPosition = Cesium.SceneTransforms.worldToWindowCoordinates(
|
const canvasPosition = Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||||
this.viewer.scene,
|
this.viewer.scene,
|
||||||
position
|
position
|
||||||
@ -640,6 +685,9 @@ export class MapCesium implements MapInterface {
|
|||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// 被地形遮挡的不展示 popup
|
||||||
|
if (this.isPositionBehindTerrain(position)) return;
|
||||||
|
|
||||||
const rawData: Record<string, any> = {
|
const rawData: Record<string, any> = {
|
||||||
...((entity as any)._rawData || {})
|
...((entity as any)._rawData || {})
|
||||||
};
|
};
|
||||||
@ -1058,6 +1106,8 @@ export class MapCesium implements MapInterface {
|
|||||||
// 取消飞行超时和加载动画
|
// 取消飞行超时和加载动画
|
||||||
this.clearFlightFallbackTimer();
|
this.clearFlightFallbackTimer();
|
||||||
this.hideLoadingOverlay();
|
this.hideLoadingOverlay();
|
||||||
|
this._ready = false;
|
||||||
|
this._pendingOSGBItems = [];
|
||||||
|
|
||||||
// 批量 popup 和 hover popup
|
// 批量 popup 和 hover popup
|
||||||
this.isBatchPopupMode = false;
|
this.isBatchPopupMode = false;
|
||||||
@ -1565,7 +1615,7 @@ export class MapCesium implements MapInterface {
|
|||||||
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
||||||
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
||||||
scaleByDistance: new Cesium.NearFarScalar(1.0e3, 1.5, 2.0e7, 0.8),
|
scaleByDistance: new Cesium.NearFarScalar(1.0e3, 1.5, 2.0e7, 0.8),
|
||||||
disableDepthTestDistance: Number.POSITIVE_INFINITY
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
|
||||||
} as any,
|
} as any,
|
||||||
label: {
|
label: {
|
||||||
text: labelText || undefined,
|
text: labelText || undefined,
|
||||||
@ -1578,7 +1628,7 @@ export class MapCesium implements MapInterface {
|
|||||||
pixelOffset: dynamicLabelOffset as any,
|
pixelOffset: dynamicLabelOffset as any,
|
||||||
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
||||||
scaleByDistance: new Cesium.NearFarScalar(1.0e3, 1.2, 2.0e7, 0.8),
|
scaleByDistance: new Cesium.NearFarScalar(1.0e3, 1.2, 2.0e7, 0.8),
|
||||||
disableDepthTestDistance: Number.POSITIVE_INFINITY
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
|
||||||
} as any,
|
} as any,
|
||||||
properties: {
|
properties: {
|
||||||
...item,
|
...item,
|
||||||
@ -2703,4 +2753,57 @@ export class MapCesium implements MapInterface {
|
|||||||
left.top - padding > right.bottom
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -276,4 +276,22 @@ export class MapClass implements MapClassInterface {
|
|||||||
getCurrentZoom(): number | undefined {
|
getCurrentZoom(): number | undefined {
|
||||||
return this.service.getCurrentZoom();
|
return this.service.getCurrentZoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 倾斜摄影 ====================
|
||||||
|
|
||||||
|
addQxsyLayer(item: any): void {
|
||||||
|
this.service.addQxsyLayer?.(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeQxsyLayer(item: any): void {
|
||||||
|
this.service.removeQxsyLayer?.(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
qxsyChangeClick(item: any, checked: boolean): void {
|
||||||
|
this.service.qxsyChangeClick?.(item, checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
qxsyToPosition(item: any): void {
|
||||||
|
this.service.qxsyToPosition?.(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
frontend/src/components/gis/map.d.ts
vendored
41
frontend/src/components/gis/map.d.ts
vendored
@ -130,35 +130,26 @@ export interface MapInterface {
|
|||||||
* 2D 直接返回原生 zoom,3D 由相机高度换算后返回
|
* 2D 直接返回原生 zoom,3D 由相机高度换算后返回
|
||||||
*/
|
*/
|
||||||
getCurrentZoom(): number | undefined;
|
getCurrentZoom(): number | undefined;
|
||||||
// /**
|
|
||||||
// * 加载倾斜摄影数据
|
|
||||||
// * @param HH3DUrlArray
|
|
||||||
// */
|
|
||||||
// addQxsyLayer(HH3DUrlArray: siteItem): void
|
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * 移除倾斜摄影数据
|
* 加载倾斜摄影数据
|
||||||
// * @param HH3DUrlArray
|
*/
|
||||||
// */
|
addQxsyLayer(item: any): void;
|
||||||
// removeQxsyLayer(HH3DUrlArray: siteItem): void
|
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * 倾斜摄影定位
|
* 移除倾斜摄影数据
|
||||||
// * @param HH3DUrlArray
|
*/
|
||||||
// */
|
removeQxsyLayer(item: any): void;
|
||||||
// qxsyToPosition(HH3DUrlArray: siteItem): void
|
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * 倾斜摄影裁剪
|
* 倾斜摄影显隐切换
|
||||||
// * @param HH3DUrlArray
|
*/
|
||||||
// */
|
qxsyChangeClick(item: any, checked: boolean): void;
|
||||||
// qxsyClipBoundary(HH3DUrlArray: siteItem): void
|
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * 移除倾斜摄影裁剪
|
* 倾斜摄影定位
|
||||||
// * @param HH3DUrlArray
|
*/
|
||||||
// */
|
qxsyToPosition(item: any): void;
|
||||||
// removeQxsyClipBoundary(HH3DUrlArray: siteItem): void
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基地面板控制
|
* 基地面板控制
|
||||||
|
|||||||
@ -2607,4 +2607,10 @@ export class MapOl implements MapInterface {
|
|||||||
this.geoJsonData = null;
|
this.geoJsonData = null;
|
||||||
this.geoJsonData1 = null;
|
this.geoJsonData1 = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 倾斜摄影方法仅3D支持,2D为空实现
|
||||||
|
addQxsyLayer(_item: any): void {}
|
||||||
|
removeQxsyLayer(_item: any): void {}
|
||||||
|
qxsyChangeClick(_item: any, _checked: boolean): void {}
|
||||||
|
qxsyToPosition(_item: any): void {}
|
||||||
}
|
}
|
||||||
|
|||||||
747
frontend/src/components/gis/osgbUtils.ts
Normal file
747
frontend/src/components/gis/osgbUtils.ts
Normal file
@ -0,0 +1,747 @@
|
|||||||
|
/**
|
||||||
|
* 倾斜摄影(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);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从场景移除 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -32,13 +32,16 @@
|
|||||||
class="leaf-item"
|
class="leaf-item"
|
||||||
>
|
>
|
||||||
<div class="leaf-content">
|
<div class="leaf-content">
|
||||||
<span class="leaf-name">{{ item.stnm }}</span>
|
<span class="leaf-name" @click="handleLeafClick(item)">{{
|
||||||
<a-switch
|
item.stnm
|
||||||
:checked="item.checked !== false"
|
}}</span>
|
||||||
size="small"
|
<span @click.stop>
|
||||||
@click.stop
|
<a-switch
|
||||||
@change="(checked: boolean) => handleSwitchChange(item, checked)"
|
:checked="item.checked !== false"
|
||||||
/>
|
size="small"
|
||||||
|
@change="(checked: boolean) => handleSwitchChange(item, checked)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
</a-sub-menu>
|
</a-sub-menu>
|
||||||
@ -79,6 +82,9 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:visible', v: boolean): void;
|
(e: 'update:visible', v: boolean): void;
|
||||||
|
(e: 'loadModel', item: any): void;
|
||||||
|
(e: 'unloadModel', item: any): void;
|
||||||
|
(e: 'flyToSite', item: any): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const tooltipVisible = ref(false);
|
const tooltipVisible = ref(false);
|
||||||
@ -125,6 +131,16 @@ const onOpenChange = (visible: boolean) => {
|
|||||||
// a-switch 切换
|
// a-switch 切换
|
||||||
const handleSwitchChange = (item: any, checked: boolean) => {
|
const handleSwitchChange = (item: any, checked: boolean) => {
|
||||||
item.checked = checked;
|
item.checked = checked;
|
||||||
|
if (checked) {
|
||||||
|
emit('loadModel', item);
|
||||||
|
} else {
|
||||||
|
emit('unloadModel', item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击站名定位
|
||||||
|
const handleLeafClick = (item: any) => {
|
||||||
|
emit('flyToSite', item);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 点击外部关闭tooltip
|
// 点击外部关闭tooltip
|
||||||
|
|||||||
@ -18,6 +18,9 @@
|
|||||||
:countdown="countdown"
|
:countdown="countdown"
|
||||||
:inTransition="is3DTransition && child.key === 'OSBGController'"
|
:inTransition="is3DTransition && child.key === 'OSBGController'"
|
||||||
@update:visible="(v: boolean) => handleComponentVisibleToggle(child.key, v)"
|
@update:visible="(v: boolean) => handleComponentVisibleToggle(child.key, v)"
|
||||||
|
@loadModel="handleLoadModel"
|
||||||
|
@unloadModel="handleUnloadModel"
|
||||||
|
@flyToSite="handleFlyToSite"
|
||||||
/>
|
/>
|
||||||
<!-- 非组件项:包裹 tooltip 显示名称 -->
|
<!-- 非组件项:包裹 tooltip 显示名称 -->
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
@ -106,7 +109,10 @@ const buildTiltPhotoTree = (list: any[]) => {
|
|||||||
const id = String(item.baseId ?? '');
|
const id = String(item.baseId ?? '');
|
||||||
const group = baseMap.get(id);
|
const group = baseMap.get(id);
|
||||||
if (group) {
|
if (group) {
|
||||||
group.children.push({ ...item, checked: true });
|
// 直接 push 原始对象(不用展开),确保与 addQxsyLayer 中的 item 是同一引用
|
||||||
|
// OSGB 状态(osgbtitles, cartesian3, eventListener 等)都挂在这个对象上
|
||||||
|
item.checked = true;
|
||||||
|
group.children.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Array.from(baseMap.entries())
|
return Array.from(baseMap.entries())
|
||||||
@ -146,11 +152,33 @@ const fetchTiltPhotoTree = async () => {
|
|||||||
});
|
});
|
||||||
const list = res.data.data;
|
const list = res.data.data;
|
||||||
tiltPhotoTree.value = buildTiltPhotoTree(list);
|
tiltPhotoTree.value = buildTiltPhotoTree(list);
|
||||||
|
|
||||||
|
// 全部加载倾斜摄影模型(dynamicSetVisible 按 12km 距离自动控制显隐)
|
||||||
|
list.forEach((item: any) => {
|
||||||
|
map.value.addQxsyLayer?.(item);
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('获取倾斜摄影数据失败:', err);
|
console.error('获取倾斜摄影数据失败:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 倾斜摄影模型相关事件处理
|
||||||
|
const handleLoadModel = (item: any) => {
|
||||||
|
// 开关打开:加载/显示模型
|
||||||
|
map.value.qxsyChangeClick?.(item, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnloadModel = (item: any) => {
|
||||||
|
// 开关关闭:卸载/隐藏模型
|
||||||
|
map.value.qxsyChangeClick?.(item, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFlyToSite = (item: any) => {
|
||||||
|
// 先加载模型(如果还没加载),再飞行定位
|
||||||
|
map.value.addQxsyLayer?.(item);
|
||||||
|
map.value.qxsyToPosition?.(item);
|
||||||
|
};
|
||||||
|
|
||||||
// 3D过渡动画状态
|
// 3D过渡动画状态
|
||||||
// layerController / OSBGController / threedRoam 三者互斥,最多一个被选中
|
// layerController / OSBGController / threedRoam 三者互斥,最多一个被选中
|
||||||
const activeKey = ref<string | null>(null);
|
const activeKey = ref<string | null>(null);
|
||||||
@ -375,11 +403,18 @@ const handleControllerClick = (item: any) => {
|
|||||||
fetchTiltPhotoTree();
|
fetchTiltPhotoTree();
|
||||||
start3DTransition();
|
start3DTransition();
|
||||||
} else {
|
} else {
|
||||||
// 3D→2D:打开抽屉,清理过渡
|
// 3D→2D:打开抽屉,清理过渡,卸载所有倾斜摄影模型
|
||||||
uiStore.drawerOpen = true;
|
uiStore.drawerOpen = true;
|
||||||
uiStore.isRoaming = false;
|
uiStore.isRoaming = false;
|
||||||
cleanup3DTransition();
|
cleanup3DTransition();
|
||||||
activeKey.value = null;
|
activeKey.value = null;
|
||||||
|
// 卸载所有倾斜摄影
|
||||||
|
tiltPhotoTree.value.forEach((group: any) => {
|
||||||
|
group.children?.forEach((item: any) => {
|
||||||
|
map.value.removeQxsyLayer?.(item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
tiltPhotoTree.value = [];
|
||||||
}
|
}
|
||||||
props.onClick('dim', uiStore.mapType);
|
props.onClick('dim', uiStore.mapType);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -647,7 +647,7 @@ const focusSelectedAnchorPoint = () => {
|
|||||||
|
|
||||||
mapOrchestrator.focusPoint(
|
mapOrchestrator.focusPoint(
|
||||||
selectedValue,
|
selectedValue,
|
||||||
14,
|
uiStore.mapType === '3D' ? 9 : 14,
|
||||||
isFishSurveyMode.value ? anchorPointOptions.value : undefined
|
isFishSurveyMode.value ? anchorPointOptions.value : undefined
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -58,7 +58,7 @@ service.interceptors.response.use(
|
|||||||
message.error(response.data.msg || '请求失败');
|
message.error(response.data.msg || '请求失败');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
window.location.href = '/';
|
window.location.href = '/login';
|
||||||
}, 1000);
|
}, 1000);
|
||||||
return;
|
return;
|
||||||
} else if (response.data.code == 1) {
|
} else if (response.data.code == 1) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user