363 lines
11 KiB
TypeScript
363 lines
11 KiB
TypeScript
/**
|
||
* 三维漫游管理器
|
||
* 负责加载 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;
|
||
|
||
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<void> {
|
||
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()
|
||
);
|
||
}
|
||
}
|
||
|
||
/** 视角定位到指定时间点的位置 */
|
||
flyToPositionAtTime(seconds: number): void {
|
||
if (!this.roamingEntity || !this.startTime) return;
|
||
const time = Cesium.JulianDate.addSeconds(
|
||
this.startTime,
|
||
seconds,
|
||
new Cesium.JulianDate()
|
||
);
|
||
const position = this.roamingEntity.position?.getValue(time);
|
||
if (position) {
|
||
this.viewer.camera.flyTo({
|
||
destination: position,
|
||
duration: 0.5
|
||
});
|
||
}
|
||
}
|
||
|
||
/** 是否已加载漫游实体 */
|
||
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;
|
||
|
||
// 插值系数:越小越平滑(跟随延迟越大)
|
||
const lerpFactor = 0.06;
|
||
|
||
// 对位置做 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();
|
||
}
|
||
} |