/** * 三维漫游管理器 * 负责加载 CZML 飞行路径、控制动画、第一人称相机跟随 */ import { czml_huanghe } from './data/riverline'; import * as Cesium from 'cesium'; export class ThreeDRoamManager { private viewer: any; private dataSource: any = null; private roamingEntity: any = null; private startTime: any = null; private callbackEvent: (() => void) | null = null; private tickCallback: ((clock: any) => void) | null = null; private totalDuration = 0; private onProgress: ((seconds: number) => void) | null = null; private positionList: number[] = []; // 开关状态 private _flightPathVisible = true; private _flightModelVisible = true; // 相机平滑跟随插值状态 private _smoothCenter: Cesium.Cartesian3 | null = null; private _smoothOrientation: Cesium.Quaternion | null = null; // 点击跳转后的过渡截止时间(performance.now() 时间戳): // 过渡期内 setCameraLookAt 使用更大的插值系数,让相机平滑“飞”向目标而非瞬移 private _transitionEnd = 0; constructor(viewer: any) { this.viewer = viewer; } /** * 初始化漫游(加载 CZML 并定位到第0帧) * @param positionList API 返回的坐标点位数组 * @param totalTime 总时长(秒) * @param milestoneList 里程碑列表 [[name, time], ...] * @param onProgress 进度回调 */ async init( positionList: number[], totalTime: number, milestoneList?: [string, string][], onProgress?: (seconds: number) => void ): Promise { this.onProgress = onProgress || null; this.totalDuration = Math.ceil(totalTime || 0); // 0. 清除旧数据源(避免堆积) if (this.dataSource) { this.viewer.dataSources.remove(this.dataSource); this.dataSource = null; } this.roamingEntity = null; // 1. 处理点位数据(每4个点 +600 形成站点停留) const processedList = [...positionList]; for (let i = 0; i < processedList.length; i++) { if ((i + 1) % 4 === 0) { processedList[i] = processedList[i] + 600; } } this.positionList = processedList; // 2. 基于模板生成 CZML,替换坐标 const c_czml = JSON.parse(JSON.stringify(czml_huanghe)); c_czml[1].position.cartographicDegrees = processedList; // 动态更新时间区间(总时长秒 → ISO 时间) const totalSec = Math.ceil(totalTime || 0); const endDate = new Date('2021-03-16T10:00:01Z'); endDate.setSeconds(endDate.getSeconds() + totalSec); const endStr = endDate.toISOString().replace(/\.\d+Z$/, 'Z'); c_czml[0].clock.interval = `2021-03-16T10:00:01Z/${endStr}`; c_czml[0].clock.currentTime = '2021-03-16T10:00:01Z'; c_czml[1].availability = `2021-03-16T10:00:01Z/${endStr}`; // 3. 加载 CZML try { const dataSourcePromise = await this.viewer.dataSources.add( Cesium.CzmlDataSource.load(c_czml) ); this.dataSource = dataSourcePromise; this.roamingEntity = this.dataSource.entities.getById(c_czml[1].id); if (this.roamingEntity) { this.roamingEntity.path.show = this._flightPathVisible; this.roamingEntity.model.show = this._flightModelVisible; } // 4. 锁定相机到飞机模型后方(第0帧视角) this.viewer.trackedEntity = this.roamingEntity; this._smoothCenter = null; this._smoothOrientation = null; this.firstPersonModel(); this.startTime = this.viewer.clock.currentTime.clone(); this.viewer.clock.clockRange = Cesium.ClockRange.CLAMPED; this.viewer.clock.multiplier = 1; } catch (error) { console.error('CZML 加载失败:', error); } } /** 开始飞行 */ start(): void { if (!this.roamingEntity) { console.warn('请先初始化漫游数据'); return; } this.viewer.trackedEntity = this.roamingEntity; this.viewer.clock.shouldAnimate = true; this.entitiesVisible(false); this.registerTickCallback(); } /** 暂停飞行(相机仍锁定在模型位置) */ pause(): void { this.viewer.clock.shouldAnimate = false; this.entitiesVisible(true); this.removeTickCallback(); // 不清除平滑插值状态:暂停后相机仍会平滑收敛到模型位置再停下(保留缓冲, // 避免“立刻暂停”的突兀感);此时时钟已冻结,目标固定,不会产生抖动 } /** 重置飞行(回到起点/第0帧) */ reset(): void { this.unLockViewAndEntity(); this.clearResult(); this.viewer.clock.shouldAnimate = false; this._smoothCenter = null; this._smoothOrientation = null; if (this.onProgress) this.onProgress(0); } /** 清除飞行路径和模型(保留相机视角完全不变) */ clear(): void { // 隐藏模型和路径(不删数据源,不动 trackedEntity 和 lookAtTransform) if (this.roamingEntity) { this.roamingEntity.path.show = false; this.roamingEntity.model.show = false; } // 停止时钟(不动相机,视角完全不变) this.viewer.clock.shouldAnimate = false; // 清理引用 this.roamingEntity = null; this.removeTickCallback(); this._smoothCenter = null; this._smoothOrientation = null; if (this.onProgress) this.onProgress(0); } /** 设置飞行倍率 */ setSpeed(multiplier: number): void { this.viewer.clock.multiplier = multiplier; } /** 切换飞行路径显隐 */ setFlightPathVisible(visible: boolean): void { this._flightPathVisible = visible; if (this.roamingEntity) { this.roamingEntity.path.show = visible; } } /** 切换飞行模型显隐 */ setFlightModelVisible(visible: boolean): void { this._flightModelVisible = visible; if (this.roamingEntity) { this.roamingEntity.model.show = visible; } } /** 跳转到指定时间点(秒) */ jumpToTime(seconds: number): void { if (this.startTime) { this.viewer.clock.currentTime = Cesium.JulianDate.addSeconds( this.startTime, seconds, new Cesium.JulianDate() ); } } /** * 视角定位到指定时间点的位置 * 说明:不再使用 camera.flyTo —— 漫游中第一人称跟随(setCameraLookAt)每帧执行 * lookAtTransform,camera.flyTo 会与其争抢相机控制,导致相机高度/zoom 振荡, * 锚点文字随之“先缩小后重新放大”。改为开启一段过渡窗口, * setCameraLookAt 在窗口内用更大的插值系数平滑“飞”向目标时间点的跟随视角。 * 位置/朝向仍由 setCameraLookAt 每帧从模型实时取值,因此无需在此读取坐标。 */ flyToPositionAtTime(seconds: number): void { if (!this.roamingEntity || !this.startTime) return; this._transitionEnd = performance.now() + 500; } /** 是否已加载漫游实体 */ get hasEntity(): boolean { return this.roamingEntity !== null && this.roamingEntity !== undefined; } /** 销毁清理(完全释放) */ destroy(skipCesiumCleanup = false): void { if (skipCesiumCleanup) { // dim 切换:只清引用,Cesium 数据由地图切换自行销毁 this.removeTickCallback(); this.roamingEntity = null; this.dataSource = null; this.viewer = null; return; } this.clear(); if (this.dataSource) { this.viewer.dataSources.remove(this.dataSource); this.dataSource = null; } this.removeFirstPersonModel(); this.viewer = null; } // ==================== Private ==================== /** 隐藏/显示地图实体 */ private entitiesVisible(visible: boolean): void { const entities = this.viewer.entities?.values; if (entities?.length > 0) { entities.forEach((it: any) => { const name = it?.myname; if ( (name === 'tempMarketImg' || name === 'tempMarkets' || name === 'tempMarket') && it.show !== visible ) { it.show = visible; } }); } } /** 第一人称相机跟随(preRender)—— 平滑插值版 */ private firstPersonModel(): void { if (this.viewer.scene) { this.removeFirstPersonModel(); this.callbackEvent = this.viewer.scene.preRender.addEventListener(() => { this.setCameraLookAt(); }); } } /** 移除第一人称 */ private removeFirstPersonModel(): void { if (this.callbackEvent && typeof this.callbackEvent === 'function') { this.callbackEvent(); this.callbackEvent = null; } } /** 每帧平滑插值相机到模型后方(解决转向时突然跳动) */ private setCameraLookAt(): void { try { const entity = this.viewer.trackedEntity; if (!entity) return; const time = this.viewer.clock.currentTime; const targetCenter = entity.position?.getValue(time); const targetOrientation = entity.orientation?.getValue(time); if (!targetCenter || !targetOrientation) return; // 插值系数:常规跟随 0.1(越小越平滑,跟随延迟越大); // 点击跳转后的过渡期内 0.15 → 0.1 线性衰减,让相机快速且平滑地“飞”向目标位置 const now = performance.now(); let lerpFactor = 0.1; if (now < this._transitionEnd) { const t = 1 - (this._transitionEnd - now) / 500; // 0 → 1 lerpFactor = 0.15 + (0.1 - 0.15) * t; } // 对位置做 lerp let center: Cesium.Cartesian3; if (this._smoothCenter) { center = Cesium.Cartesian3.lerp( this._smoothCenter, targetCenter, lerpFactor, new Cesium.Cartesian3() ); } else { center = targetCenter.clone(); } // 对朝向做 slerp(球面线性插值,保证旋转平滑) let orientation: Cesium.Quaternion; if (this._smoothOrientation) { orientation = Cesium.Quaternion.slerp( this._smoothOrientation, targetOrientation, lerpFactor, new Cesium.Quaternion() ); } else { orientation = targetOrientation.clone(); } this._smoothCenter = center; this._smoothOrientation = orientation; const transform = Cesium.Matrix4.fromRotationTranslation( Cesium.Matrix3.fromQuaternion(orientation), center ); this.viewer.camera.lookAtTransform( transform, new Cesium.Cartesian3(-1200, 0, 500) ); } catch (e) { // ignore render error } } /** 注册 onTick 进度回调 */ private registerTickCallback(): void { this.removeTickCallback(); const render = (clock: any) => { if (!this.viewer.clock.shouldAnimate) return; if (!this.startTime) return; const timeOffset = Math.floor( Cesium.JulianDate.secondsDifference(clock.currentTime, this.startTime) ); if (this.onProgress) { this.onProgress(timeOffset); } // 播放结束自动停止 if (this.totalDuration > 0 && timeOffset >= this.totalDuration) { this.clear(); } }; this.viewer.clock.onTick.addEventListener(render); this.tickCallback = render; } /** 移除 onTick 回调 */ private removeTickCallback(): void { if (this.tickCallback) { this.viewer.clock.onTick.removeEventListener(this.tickCallback); this.tickCallback = null; } } /** 解除实体和视角锁定 */ private unLockViewAndEntity(): void { this.viewer.trackedEntity = undefined; if (this.viewer.camera) { this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY); } this.removeFirstPersonModel(); } /** 清除数据源 */ private clearResult(): void { this.viewer.clock.shouldAnimate = false; this.viewer.trackedEntity = undefined; if (this.dataSource) { this.viewer.dataSources.remove(this.dataSource); this.dataSource = null; } this.removeTickCallback(); } }