/**
|
* 无人机轨迹回放系统
|
* 纯前端实现,基于Cesium
|
*/
|
|
class TrajectoryPlayback {
|
constructor(config = {}) {
|
this.viewer = null;
|
this.trajectoryData = null;
|
this.currentIndex = 0;
|
this.isPlaying = false;
|
this.playbackSpeed = 1000; // 默认1秒间隔
|
this.playInterval = null;
|
this.droneEntity = null;
|
this.cameraDirection = null; // 相机中心视角方向箭头
|
this.cameraFrustum = null; // 相机视锥
|
this.trajectoryPolyline = null;
|
this.trajectoryPoints = null; // 轨迹点球形标记
|
|
// 轨迹配置项
|
this.config = {
|
trajectoryJsonPath: './flight_1581F6Q8D241J00CGT7R_20250804_115151/trajectory_20250804_115151.json',
|
videoFramesDirectory: './flight_1581F6Q8D241J00CGT7R_20250804_115151/video_frames/',
|
...config
|
};
|
|
this.init();
|
}
|
|
/**
|
* 初始化系统
|
*/
|
async init() {
|
try {
|
console.log('开始初始化轨迹回放系统...');
|
this.initCesium();
|
console.log('Cesium初始化完成');
|
await this.loadTrajectoryData();
|
console.log('轨迹数据加载完成');
|
this.setupEventListeners();
|
console.log('事件监听器设置完成');
|
this.hideLoading();
|
this.updateStatus('📊 轨迹数据加载完成,共 ' + this.trajectoryData.length + ' 个轨迹点');
|
console.log('系统初始化完成');
|
} catch (error) {
|
console.error('初始化失败:', error);
|
this.updateStatus('❌ 初始化失败: ' + error.message);
|
this.hideLoading();
|
}
|
}
|
|
/**
|
* 初始化Cesium地图
|
*/
|
initCesium() {
|
this.viewer = new Cesium.Viewer('cesiumContainer', {
|
terrain: Cesium.Terrain.fromWorldTerrain(),
|
shadows: true,
|
timeline: false,
|
animation: false,
|
sceneModePicker: false,
|
baseLayerPicker: false,
|
geocoder: false,
|
homeButton: false,
|
infoBox: false,
|
navigationHelpButton: false,
|
selectionIndicator: false
|
// 使用Cesium默认影像提供者
|
});
|
|
// 设置初始视角到西安
|
this.viewer.camera.setView({
|
destination: Cesium.Cartesian3.fromDegrees(108.9398, 34.3416, 5000),
|
orientation: {
|
heading: Cesium.Math.toRadians(0),
|
pitch: Cesium.Math.toRadians(-45),
|
roll: 0.0
|
}
|
});
|
|
// 将viewer暴露到全局作用域
|
window.viewer = this.viewer;
|
}
|
|
/**
|
* 加载轨迹数据
|
*/
|
async loadTrajectoryData() {
|
try {
|
// 使用配置的轨迹JSON路径
|
const response = await fetch(this.config.trajectoryJsonPath);
|
if (!response.ok) {
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
}
|
|
const data = await response.json();
|
this.trajectoryData = data.trajectory_points;
|
|
if (!this.trajectoryData || this.trajectoryData.length === 0) {
|
throw new Error('轨迹数据为空');
|
}
|
|
console.log(`成功加载 ${this.trajectoryData.length} 个轨迹点`);
|
console.log('轨迹配置:', this.config);
|
|
// 创建轨迹线
|
this.createTrajectoryLine();
|
|
// 创建无人机实体
|
this.createDroneEntity();
|
|
// 初始化到第一个位置
|
this.updateToFrame(0);
|
|
// 设置相机跟随第一个轨迹点
|
this.focusOnTrajectory();
|
|
} catch (error) {
|
console.error('加载轨迹数据失败:', error);
|
throw error;
|
}
|
}
|
|
/**
|
* 创建轨迹线
|
*/
|
createTrajectoryLine() {
|
const positions = this.trajectoryData.map(point => {
|
return Cesium.Cartesian3.fromDegrees(
|
point.position.longitude,
|
point.position.latitude,
|
point.position.altitude
|
);
|
});
|
|
// 创建红色虚线轨迹
|
this.trajectoryPolyline = this.viewer.entities.add({
|
name: '飞行轨迹',
|
polyline: {
|
positions: positions,
|
width: 3,
|
material: new Cesium.PolylineDashMaterialProperty({
|
color: Cesium.Color.RED,
|
dashLength: 16.0,
|
dashPattern: 255
|
}),
|
clampToGround: false,
|
show: true
|
}
|
});
|
|
// 为每个轨迹点添加红色球形标记
|
this.trajectoryPoints = [];
|
this.trajectoryData.forEach((point, index) => {
|
const pointEntity = this.viewer.entities.add({
|
name: `轨迹点_${index}`,
|
position: Cesium.Cartesian3.fromDegrees(
|
point.position.longitude,
|
point.position.latitude,
|
point.position.altitude
|
),
|
point: {
|
pixelSize: 8,
|
color: Cesium.Color.RED,
|
outlineColor: Cesium.Color.DARKRED,
|
outlineWidth: 1,
|
heightReference: Cesium.HeightReference.NONE,
|
show: true
|
}
|
});
|
this.trajectoryPoints.push(pointEntity);
|
});
|
}
|
|
/**
|
* 创建无人机实体 - 使用黄色圆形点表示,并添加云台姿态箭头
|
*/
|
createDroneEntity() {
|
this.droneEntity = this.viewer.entities.add({
|
name: '无人机',
|
position: Cesium.Cartesian3.fromDegrees(0, 0, 0),
|
point: {
|
pixelSize: 15,
|
color: Cesium.Color.YELLOW,
|
outlineColor: Cesium.Color.BLACK,
|
outlineWidth: 2,
|
heightReference: Cesium.HeightReference.NONE,
|
show: true
|
}
|
});
|
|
// 创建相机中心视角方向箭头
|
this.createCameraDirection();
|
this.createCameraFrustum();
|
}
|
|
/**
|
* 创建相机中心视角方向箭头
|
*/
|
createCameraDirection() {
|
const arrowLength = 15.0; // 箭头长度
|
const arrowWidth = 3.0; // 箭头线宽
|
|
// 创建红色箭头表示相机中心视角方向
|
this.cameraDirection = this.viewer.entities.add({
|
name: '相机视角方向',
|
polyline: {
|
positions: [
|
Cesium.Cartesian3.fromDegrees(0, 0, 0),
|
Cesium.Cartesian3.fromDegrees(0, 0, 0)
|
],
|
width: arrowWidth,
|
material: Cesium.Color.RED,
|
clampToGround: false,
|
show: true
|
}
|
});
|
}
|
|
/**
|
* 创建相机视锥
|
*/
|
createCameraFrustum() {
|
// 相机实际参数
|
// 水平FOV: 90度, 垂直FOV: 60度
|
// 图像尺寸: 1280 × 720 像素
|
const fovHorizontal = Cesium.Math.toRadians(84); // 水平视野角度90度
|
const fovVertical = Cesium.Math.toRadians(53); // 垂直视野角度60度
|
const imageWidth = 1280; // 图像宽度
|
const imageHeight = 720; // 图像高度
|
const aspectRatio = imageWidth / imageHeight; // 宽高比 = 1280/720 ≈ 1.778
|
|
// 创建视锥线框
|
this.cameraFrustum = {
|
lines: this.viewer.entities.add({
|
name: '相机视锥线框',
|
polyline: {
|
positions: [],
|
width: 1.5,
|
material: Cesium.Color.YELLOW.withAlpha(0.8),
|
clampToGround: false,
|
show: true
|
}
|
}),
|
surface: this.viewer.entities.add({
|
name: '相机视锥表面',
|
polygon: {
|
hierarchy: new Cesium.PolygonHierarchy([]),
|
material: Cesium.Color.WHITE, // 初始为白色,后续会替换为视频帧
|
outline: false,
|
fill: true,
|
show: true,
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
height: 0,
|
extrudedHeight: 0
|
}
|
}),
|
fovHorizontal: fovHorizontal, // 水平FOV
|
fovVertical: fovVertical, // 垂直FOV
|
aspectRatio: aspectRatio, // 宽高比
|
imageWidth: imageWidth, // 图像宽度
|
imageHeight: imageHeight, // 图像高度
|
currentVideoFrame: null // 存储当前视频帧路径
|
};
|
}
|
|
/**
|
* 设置事件监听器
|
*/
|
setupEventListeners() {
|
// 键盘快捷键
|
document.addEventListener('keydown', (event) => {
|
switch (event.code) {
|
case 'Space':
|
event.preventDefault();
|
this.togglePlayback();
|
break;
|
case 'ArrowLeft':
|
event.preventDefault();
|
this.previousFrame();
|
break;
|
case 'ArrowRight':
|
event.preventDefault();
|
this.nextFrame();
|
break;
|
case 'Home':
|
event.preventDefault();
|
this.seekToFrame(0);
|
break;
|
case 'End':
|
event.preventDefault();
|
this.seekToFrame(this.trajectoryData.length - 1);
|
break;
|
}
|
});
|
|
// 进度条拖拽
|
this.setupProgressBarDrag();
|
}
|
|
/**
|
* 设置进度条拖拽功能
|
*/
|
setupProgressBarDrag() {
|
const progressBar = document.getElementById('progressBar');
|
const progressHandle = document.getElementById('progressHandle');
|
let isDragging = false;
|
|
progressHandle.addEventListener('mousedown', (event) => {
|
isDragging = true;
|
event.preventDefault();
|
});
|
|
document.addEventListener('mousemove', (event) => {
|
if (isDragging) {
|
const rect = progressBar.getBoundingClientRect();
|
const percent = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width));
|
const frameIndex = Math.floor(percent * (this.trajectoryData.length - 1));
|
this.seekToFrame(frameIndex);
|
}
|
});
|
|
document.addEventListener('mouseup', () => {
|
isDragging = false;
|
});
|
}
|
|
/**
|
* 切换播放状态
|
*/
|
togglePlayback() {
|
if (this.isPlaying) {
|
this.pause();
|
} else {
|
this.play();
|
}
|
}
|
|
/**
|
* 开始播放
|
*/
|
play() {
|
if (this.isPlaying) return;
|
|
this.isPlaying = true;
|
this.updatePlayButton();
|
|
this.playInterval = setInterval(() => {
|
if (this.currentIndex < this.trajectoryData.length - 1) {
|
this.nextFrame();
|
} else {
|
this.pause(); // 播放完毕自动暂停
|
}
|
}, this.playbackSpeed);
|
}
|
|
/**
|
* 暂停播放
|
*/
|
pause() {
|
this.isPlaying = false;
|
this.updatePlayButton();
|
|
if (this.playInterval) {
|
clearInterval(this.playInterval);
|
this.playInterval = null;
|
}
|
}
|
|
/**
|
* 上一帧
|
*/
|
previousFrame() {
|
if (this.currentIndex > 0) {
|
this.seekToFrame(this.currentIndex - 1);
|
}
|
}
|
|
/**
|
* 下一帧
|
*/
|
nextFrame() {
|
if (this.currentIndex < this.trajectoryData.length - 1) {
|
this.seekToFrame(this.currentIndex + 1);
|
}
|
}
|
|
/**
|
* 跳转到指定帧
|
*/
|
seekToFrame(index) {
|
if (index < 0 || index >= this.trajectoryData.length) return;
|
|
this.currentIndex = index;
|
this.updateToFrame(index);
|
}
|
|
/**
|
* 更新到指定帧
|
*/
|
updateToFrame(index) {
|
const point = this.trajectoryData[index];
|
if (!point) return;
|
|
// 更新无人机位置
|
const position = Cesium.Cartesian3.fromDegrees(
|
point.position.longitude,
|
point.position.latitude,
|
point.position.altitude
|
);
|
this.droneEntity.position = position;
|
|
// 更新相机中心视角方向
|
this.updateCameraDirection(position, point.gimbal_attitude);
|
|
// 更新视锥远平面的视频帧(这会根据图像宽高比自动调整视锥并重新计算几何形状)
|
this.updateFrustumVideoTexture(point);
|
|
// 更新UI显示
|
this.updateUI(point);
|
|
// 更新进度条
|
this.updateProgressBar();
|
}
|
|
/**
|
* 更新相机中心视角方向箭头
|
*/
|
updateCameraDirection(position, gimbalAttitude) {
|
if (!this.cameraDirection || !gimbalAttitude) return;
|
|
const arrowLength = 15.0;
|
|
// 获取云台的绝对姿态角度(转换为弧度)
|
// NED坐标系:X轴指向北(Roll轴),Y轴指向东(Pitch轴),Z轴指向下(Yaw轴)
|
const yaw = Cesium.Math.toRadians(gimbalAttitude.yaw || 0); // 绕Z轴旋转(向下轴)
|
const pitch = Cesium.Math.toRadians(gimbalAttitude.pitch || 0); // 绕Y轴旋转(向东轴)
|
const roll = Cesium.Math.toRadians(gimbalAttitude.roll || 0); // 绕X轴旋转(向北轴)
|
|
// 在NED坐标系中,相机初始朝向为X轴正方向(向北)
|
// 当pitch=-90度时,相机应该垂直向下(Z轴正方向)
|
const nedForward = new Cesium.Cartesian3(arrowLength, 0, 0); // NED坐标系中的初始朝向(向北)
|
|
// 创建NED坐标系的旋转矩阵:
|
// 1. 先绕X轴旋转roll角(横滚)
|
// 2. 再绕Y轴旋转pitch角(俯仰)
|
// 3. 最后绕Z轴旋转yaw角(偏航)
|
const rollMatrix = Cesium.Matrix3.fromRotationX(roll);
|
const pitchMatrix = Cesium.Matrix3.fromRotationY(pitch);
|
const yawMatrix = Cesium.Matrix3.fromRotationZ(yaw);
|
|
// 组合旋转矩阵:按照Roll->Pitch->Yaw的顺序
|
let rotationMatrix = Cesium.Matrix3.multiply(pitchMatrix, rollMatrix, new Cesium.Matrix3());
|
rotationMatrix = Cesium.Matrix3.multiply(yawMatrix, rotationMatrix, rotationMatrix);
|
|
// 应用旋转变换到相机朝向向量
|
const rotatedNEDForward = Cesium.Matrix3.multiplyByVector(rotationMatrix, nedForward, new Cesium.Cartesian3());
|
|
// 将NED坐标系转换为ENU坐标系(Cesium使用的坐标系)
|
// NED -> ENU: X(北) -> Y(北), Y(东) -> X(东), Z(下) -> -Z(上)
|
const enuForward = new Cesium.Cartesian3(
|
rotatedNEDForward.y, // NED的Y(东) -> ENU的X(东)
|
rotatedNEDForward.x, // NED的X(北) -> ENU的Y(北)
|
-rotatedNEDForward.z // NED的Z(下) -> ENU的-Z(上)
|
);
|
|
// 转换到地球固定坐标系
|
const transform = Cesium.Transforms.eastNorthUpToFixedFrame(position);
|
const worldDirection = Cesium.Matrix4.multiplyByPointAsVector(transform, enuForward, new Cesium.Cartesian3());
|
|
// 计算箭头终点位置
|
const endPosition = Cesium.Cartesian3.add(position, worldDirection, new Cesium.Cartesian3());
|
|
// 更新箭头位置
|
this.cameraDirection.polyline.positions = [position, endPosition];
|
}
|
|
/**
|
* 更新相机视锥 - 计算与地面的实际交点
|
*/
|
updateCameraFrustum(position, gimbalAttitude) {
|
if (!this.cameraFrustum || !gimbalAttitude) return;
|
|
// 获取云台的绝对姿态角度(转换为弧度)
|
const yaw = Cesium.Math.toRadians(gimbalAttitude.yaw || 0);
|
const pitch = Cesium.Math.toRadians(gimbalAttitude.pitch || 0);
|
const roll = Cesium.Math.toRadians(gimbalAttitude.roll || 0);
|
|
// 视锥参数
|
const { fovHorizontal, fovVertical } = this.cameraFrustum;
|
const halfFovH = fovHorizontal / 2;
|
const halfFovV = fovVertical / 2;
|
|
// 获取无人机的高度(相对于地面)
|
const droneHeight = position.z || 100; // 如果无法获取高度,默认100米
|
|
// 在NED坐标系中定义视锥的四个方向向量(归一化)
|
const nedTopLeft = new Cesium.Cartesian3(1, -Math.tan(halfFovH), -Math.tan(halfFovV));
|
const nedTopRight = new Cesium.Cartesian3(1, Math.tan(halfFovH), -Math.tan(halfFovV));
|
const nedBottomLeft = new Cesium.Cartesian3(1, -Math.tan(halfFovH), Math.tan(halfFovV));
|
const nedBottomRight = new Cesium.Cartesian3(1, Math.tan(halfFovH), Math.tan(halfFovV));
|
|
// 归一化方向向量
|
Cesium.Cartesian3.normalize(nedTopLeft, nedTopLeft);
|
Cesium.Cartesian3.normalize(nedTopRight, nedTopRight);
|
Cesium.Cartesian3.normalize(nedBottomLeft, nedBottomLeft);
|
Cesium.Cartesian3.normalize(nedBottomRight, nedBottomRight);
|
|
// 创建旋转矩阵
|
const rollMatrix = Cesium.Matrix3.fromRotationX(roll);
|
const pitchMatrix = Cesium.Matrix3.fromRotationY(pitch);
|
const yawMatrix = Cesium.Matrix3.fromRotationZ(yaw);
|
|
let rotationMatrix = Cesium.Matrix3.multiply(pitchMatrix, rollMatrix, new Cesium.Matrix3());
|
rotationMatrix = Cesium.Matrix3.multiply(yawMatrix, rotationMatrix, rotationMatrix);
|
|
// 应用旋转到方向向量
|
const rotatedTopLeft = Cesium.Matrix3.multiplyByVector(rotationMatrix, nedTopLeft, new Cesium.Cartesian3());
|
const rotatedTopRight = Cesium.Matrix3.multiplyByVector(rotationMatrix, nedTopRight, new Cesium.Cartesian3());
|
const rotatedBottomLeft = Cesium.Matrix3.multiplyByVector(rotationMatrix, nedBottomLeft, new Cesium.Cartesian3());
|
const rotatedBottomRight = Cesium.Matrix3.multiplyByVector(rotationMatrix, nedBottomRight, new Cesium.Cartesian3());
|
|
// 转换到ENU坐标系
|
const enuTopLeft = new Cesium.Cartesian3(rotatedTopLeft.y, rotatedTopLeft.x, -rotatedTopLeft.z);
|
const enuTopRight = new Cesium.Cartesian3(rotatedTopRight.y, rotatedTopRight.x, -rotatedTopRight.z);
|
const enuBottomLeft = new Cesium.Cartesian3(rotatedBottomLeft.y, rotatedBottomLeft.x, -rotatedBottomLeft.z);
|
const enuBottomRight = new Cesium.Cartesian3(rotatedBottomRight.y, rotatedBottomRight.x, -rotatedBottomRight.z);
|
|
// 转换到地球固定坐标系
|
const transform = Cesium.Transforms.eastNorthUpToFixedFrame(position);
|
|
const worldTopLeft = Cesium.Matrix4.multiplyByPointAsVector(transform, enuTopLeft, new Cesium.Cartesian3());
|
const worldTopRight = Cesium.Matrix4.multiplyByPointAsVector(transform, enuTopRight, new Cesium.Cartesian3());
|
const worldBottomLeft = Cesium.Matrix4.multiplyByPointAsVector(transform, enuBottomLeft, new Cesium.Cartesian3());
|
const worldBottomRight = Cesium.Matrix4.multiplyByPointAsVector(transform, enuBottomRight, new Cesium.Cartesian3());
|
|
// 计算与地面的交点
|
const groundIntersections = this.calculateGroundIntersections(position, [
|
worldTopLeft, worldTopRight, worldBottomLeft, worldBottomRight
|
]);
|
|
let groundTopLeft, groundTopRight, groundBottomLeft, groundBottomRight;
|
|
if (groundIntersections.length < 4) {
|
console.warn('无法计算完整的地面交点,使用默认30米视锥长度');
|
|
// 使用默认30米距离计算视锥点
|
const defaultDistance = 30.0;
|
|
// 计算30米距离处的视锥点
|
groundTopLeft = Cesium.Cartesian3.add(position,
|
Cesium.Cartesian3.multiplyByScalar(worldTopLeft, defaultDistance, new Cesium.Cartesian3()),
|
new Cesium.Cartesian3());
|
groundTopRight = Cesium.Cartesian3.add(position,
|
Cesium.Cartesian3.multiplyByScalar(worldTopRight, defaultDistance, new Cesium.Cartesian3()),
|
new Cesium.Cartesian3());
|
groundBottomLeft = Cesium.Cartesian3.add(position,
|
Cesium.Cartesian3.multiplyByScalar(worldBottomLeft, defaultDistance, new Cesium.Cartesian3()),
|
new Cesium.Cartesian3());
|
groundBottomRight = Cesium.Cartesian3.add(position,
|
Cesium.Cartesian3.multiplyByScalar(worldBottomRight, defaultDistance, new Cesium.Cartesian3()),
|
new Cesium.Cartesian3());
|
} else {
|
[groundTopLeft, groundTopRight, groundBottomLeft, groundBottomRight] = groundIntersections;
|
}
|
|
// 创建视锥线框(从无人机到地面交点)
|
var frustumLines = null;
|
if(groundIntersections.length < 4){
|
frustumLines= [
|
// 从相机中心到地面交点的射线
|
position, groundTopLeft,
|
position, groundTopRight,
|
position, groundBottomLeft,
|
position, groundBottomRight,
|
// // 地面投影四条边
|
groundTopLeft, groundTopRight, // 顶边
|
groundTopRight, groundBottomRight, // 右边
|
groundBottomRight, groundBottomLeft, // 底边
|
groundBottomLeft, groundTopLeft // 左边
|
];
|
}else{
|
frustumLines= [
|
// 从相机中心到地面交点的射线
|
position, groundTopLeft,
|
position, groundTopRight,
|
position, groundBottomLeft,
|
position, groundBottomRight
|
// // 地面投影四条边
|
// groundTopLeft, groundTopRight, // 顶边
|
// groundTopRight, groundBottomRight, // 右边
|
// groundBottomRight, groundBottomLeft, // 底边
|
// groundBottomLeft, groundTopLeft // 左边
|
];
|
}
|
|
|
// 更新线框
|
this.cameraFrustum.lines.polyline.positions = frustumLines;
|
|
// 创建地面投影表面
|
const frustumSurface = [
|
groundBottomLeft, groundTopLeft, groundTopRight, groundBottomRight
|
];
|
|
// 更新表面
|
this.cameraFrustum.surface.polygon.hierarchy = new Cesium.PolygonHierarchy(frustumSurface);
|
|
console.log('更新地面投影视锥,交点数:', groundIntersections.length);
|
}
|
|
/**
|
* 计算射线与地面的交点
|
*/
|
calculateGroundIntersections(origin, directions) {
|
const intersections = [];
|
|
for (const direction of directions) {
|
// 计算射线与地面(z=0平面在地球表面)的交点
|
const intersection = this.rayGroundIntersection(origin, direction);
|
if (intersection) {
|
intersections.push(intersection);
|
}
|
}
|
|
return intersections;
|
}
|
|
/**
|
* 射线与地面交点计算
|
*/
|
rayGroundIntersection(origin, direction) {
|
try {
|
// 创建射线
|
const ray = new Cesium.Ray(origin, direction);
|
|
// 首先尝试与地球表面(包含地形)的交点
|
const terrainIntersection = this.viewer.scene.globe.pick(ray, this.viewer.scene);
|
if (terrainIntersection) {
|
return terrainIntersection;
|
}
|
|
// 如果没有找到地形交点,计算与椭球面的交点
|
const ellipsoid = this.viewer.scene.globe.ellipsoid;
|
const intersections = Cesium.IntersectionTests.rayEllipsoid(ray, ellipsoid);
|
|
if (intersections) {
|
// 选择第一个交点(进入点)
|
const intersectionPoint = Cesium.Ray.getPoint(ray, intersections.start);
|
return intersectionPoint;
|
}
|
|
// 如果仍然没有交点,使用简化的平面交点计算
|
// 假设地面高度为0(海平面)
|
const groundHeight = 0;
|
const originHeight = Cesium.Cartographic.fromCartesian(origin).height;
|
|
if (direction.z >= 0) {
|
// 如果射线向上,不会与地面相交
|
return null;
|
}
|
|
// 计算射线与平面的交点
|
const t = (groundHeight - originHeight) / direction.z;
|
if (t < 0) {
|
return null; // 交点在射线起点后面
|
}
|
|
const intersectionPoint = Cesium.Cartesian3.add(
|
origin,
|
Cesium.Cartesian3.multiplyByScalar(direction, t, new Cesium.Cartesian3()),
|
new Cesium.Cartesian3()
|
);
|
|
return intersectionPoint;
|
|
} catch (error) {
|
console.warn('计算地面交点失败:', error);
|
return null;
|
}
|
}
|
|
/**
|
* 创建根据角度旋转的图片
|
*/
|
createLabeledImage(originalImageSrc, width, height, rotationAngle = 0) {
|
return new Promise((resolve, reject) => {
|
const canvas = document.createElement('canvas');
|
const ctx = canvas.getContext('2d');
|
|
// 计算旋转后画布的尺寸
|
const cos = Math.abs(Math.cos(rotationAngle));
|
const sin = Math.abs(Math.sin(rotationAngle));
|
const newWidth = width * cos + height * sin;
|
const newHeight = width * sin + height * cos;
|
|
canvas.width = newWidth;
|
canvas.height = newHeight;
|
|
const img = new Image();
|
img.onload = () => {
|
try {
|
// 将坐标原点移到画布中心
|
ctx.translate(newWidth / 2, newHeight / 2);
|
|
// 根据传入的角度进行旋转
|
ctx.rotate(rotationAngle);
|
|
// 绘制旋转后的图片,注意要从负的中心点开始绘制
|
ctx.drawImage(img, -width / 2, -height / 2, width, height);
|
|
// 转换为Base64数据URL
|
const dataURL = canvas.toDataURL('image/png');
|
resolve(dataURL);
|
} catch (error) {
|
reject(error);
|
}
|
};
|
|
img.onerror = () => {
|
reject(new Error('图片加载失败'));
|
};
|
|
img.src = originalImageSrc;
|
});
|
}
|
|
/**
|
* 更新视锥远平面的视频帧纹理
|
*/
|
updateFrustumVideoTexture(point) {
|
if (!this.cameraFrustum || !this.cameraFrustum.surface) return;
|
|
// 获取当前无人机位置
|
const position = Cesium.Cartesian3.fromDegrees(
|
point.position.longitude,
|
point.position.latitude,
|
point.position.altitude
|
);
|
|
// 检查是否有视频帧数据
|
if (point.video_frame && point.video_frame !== this.cameraFrustum.currentVideoFrame) {
|
// 使用配置的视频帧目录路径
|
const imagePath = `${this.config.videoFramesDirectory}${point.video_frame}`;
|
|
// 创建临时图像对象来获取真实尺寸
|
const img = new Image();
|
img.onload = async () => {
|
try {
|
// 计算图像的真实宽高比
|
const imageAspectRatio = img.width / img.height;
|
|
// 更新视锥的宽高比
|
//this.cameraFrustum.aspectRatio = imageAspectRatio;
|
|
// 获取当前云台的yaw角度作为图片旋转角度
|
const rotationAngle = Cesium.Math.toRadians(point.gimbal_attitude?.yaw || 0);
|
|
// 创建根据云台yaw角度旋转的图片
|
const labeledImageDataURL = await this.createLabeledImage(imagePath, img.width, img.height, rotationAngle);
|
|
|
|
// 创建图像材质,使用带标签的图片
|
const imageMaterial = new Cesium.ImageMaterialProperty({
|
image: labeledImageDataURL,
|
transparent: false
|
});
|
|
// 更新远平面材质
|
this.cameraFrustum.surface.polygon.material = imageMaterial;
|
this.cameraFrustum.currentVideoFrame = point.video_frame;
|
|
// 重新计算视锥几何形状以匹配新的宽高比
|
this.updateCameraFrustum(position, point.gimbal_attitude);
|
|
console.log(`更新视锥视频帧: ${point.video_frame}, 宽高比: ${imageAspectRatio.toFixed(2)} (${img.width}x${img.height})`);
|
console.log(`图片旋转角度: ${(point.gimbal_attitude?.yaw || 0).toFixed(1)}°`);
|
} catch (error) {
|
console.error('加载视频帧失败:', error);
|
// 如果图像加载失败,使用半透明红色作为备用
|
this.cameraFrustum.surface.polygon.material = Cesium.Color.RED.withAlpha(0.3);
|
// 仍需要更新视锥几何形状
|
this.updateCameraFrustum(position, point.gimbal_attitude);
|
}
|
};
|
|
img.onerror = () => {
|
console.error('图像加载失败:', imagePath);
|
this.cameraFrustum.surface.polygon.material = Cesium.Color.RED.withAlpha(0.3);
|
// 仍需要更新视锥几何形状
|
this.updateCameraFrustum(position, point.gimbal_attitude);
|
};
|
|
img.src = imagePath;
|
} else if (!point.video_frame) {
|
// 如果没有视频帧,使用半透明红色,恢复默认宽高比
|
this.cameraFrustum.surface.polygon.material = Cesium.Color.RED.withAlpha(0.3);
|
this.cameraFrustum.currentVideoFrame = null;
|
this.cameraFrustum.aspectRatio = 1280 / 720; // 恢复相机的实际宽高比
|
// 更新视锥几何形状
|
this.updateCameraFrustum(position, point.gimbal_attitude);
|
} else {
|
// 视频帧没有变化,但仍需要更新视锥几何形状(因为姿态可能改变)
|
this.updateCameraFrustum(position, point.gimbal_attitude);
|
}
|
}
|
|
/**
|
* 更新UI显示
|
*/
|
updateUI(point) {
|
// 更新位置信息
|
const latElement = document.getElementById('latitude');
|
const lonElement = document.getElementById('longitude');
|
const altElement = document.getElementById('altitude');
|
|
if (latElement) latElement.textContent = point.position.latitude.toFixed(6) + '°';
|
if (lonElement) lonElement.textContent = point.position.longitude.toFixed(6) + '°';
|
if (altElement) altElement.textContent = point.position.altitude.toFixed(1) + 'm';
|
|
// 更新姿态信息
|
const headingElement = document.getElementById('heading');
|
const pitchElement = document.getElementById('pitch');
|
const rollElement = document.getElementById('roll');
|
|
if (headingElement) headingElement.textContent = (point.attitude.heading || 0).toFixed(1) + '°';
|
if (pitchElement) pitchElement.textContent = (point.attitude.pitch || 0).toFixed(1) + '°';
|
if (rollElement) rollElement.textContent = (point.attitude.roll || 0).toFixed(1) + '°';
|
|
// 更新云台信息
|
const gimbalPitchElement = document.getElementById('gimbalPitch');
|
const gimbalYawElement = document.getElementById('gimbalYaw');
|
const gimbalRollElement = document.getElementById('gimbalRoll');
|
|
if (gimbalPitchElement) gimbalPitchElement.textContent = (point.gimbal_attitude?.pitch || 0).toFixed(1) + '°';
|
if (gimbalYawElement) gimbalYawElement.textContent = (point.gimbal_attitude?.yaw || 0).toFixed(1) + '°';
|
if (gimbalRollElement) gimbalRollElement.textContent = (point.gimbal_attitude?.roll || 0).toFixed(1) + '°';
|
|
// 更新时间显示
|
const timeDisplay = document.getElementById('timeDisplay');
|
if (timeDisplay) {
|
timeDisplay.textContent = `${this.currentIndex + 1} / ${this.trajectoryData.length}`;
|
}
|
|
// 更新视频帧
|
this.updateVideoFrame(point);
|
}
|
|
/**
|
* 更新视频帧显示
|
*/
|
updateVideoFrame(point) {
|
const videoFrame = document.getElementById('videoFrame');
|
const videoPlaceholder = document.getElementById('videoPlaceholder');
|
|
if (point.video_frame) {
|
// 使用配置的视频帧目录路径
|
const imagePath = `${this.config.videoFramesDirectory}${point.video_frame}`;
|
|
videoFrame.src = imagePath;
|
videoFrame.style.display = 'block';
|
videoPlaceholder.style.display = 'none';
|
|
videoFrame.onerror = () => {
|
videoFrame.style.display = 'none';
|
videoPlaceholder.style.display = 'block';
|
videoPlaceholder.innerHTML = `
|
<div>📹 视频帧加载失败</div>
|
<div style="font-size: 10px; margin-top: 5px;">${point.video_frame}</div>
|
`;
|
};
|
} else {
|
videoFrame.style.display = 'none';
|
videoPlaceholder.style.display = 'block';
|
videoPlaceholder.innerHTML = `
|
<div>📹 无视频帧数据</div>
|
<div style="font-size: 10px; margin-top: 5px;">时间点: ${point.timestamp}</div>
|
`;
|
}
|
}
|
|
/**
|
* 更新播放按钮
|
*/
|
updatePlayButton() {
|
const playBtn = document.getElementById('playBtn');
|
if (this.isPlaying) {
|
playBtn.innerHTML = '⏸';
|
playBtn.classList.add('active');
|
playBtn.title = '暂停';
|
} else {
|
playBtn.innerHTML = '▶';
|
playBtn.classList.remove('active');
|
playBtn.title = '播放';
|
}
|
}
|
|
/**
|
* 更新进度条
|
*/
|
updateProgressBar() {
|
if (!this.trajectoryData || this.trajectoryData.length === 0) return;
|
|
const progress = this.currentIndex / (this.trajectoryData.length - 1);
|
const progressFill = document.getElementById('progressFill');
|
const progressHandle = document.getElementById('progressHandle');
|
|
progressFill.style.width = (progress * 100) + '%';
|
progressHandle.style.left = (progress * 100) + '%';
|
}
|
|
/**
|
* 改变播放速度
|
*/
|
changePlaybackSpeed() {
|
const speedSelect = document.getElementById('speedSelect');
|
const speed = parseFloat(speedSelect.value);
|
this.playbackSpeed = 1000 / speed; // 转换为间隔时间
|
|
// 如果正在播放,重新启动以应用新速度
|
if (this.isPlaying) {
|
this.pause();
|
this.play();
|
}
|
}
|
|
/**
|
* 聚焦到轨迹
|
*/
|
focusOnTrajectory() {
|
if (!this.trajectoryData || this.trajectoryData.length === 0) return;
|
|
// 计算轨迹边界
|
let minLon = 180, maxLon = -180;
|
let minLat = 90, maxLat = -90;
|
let minAlt = Number.MAX_VALUE, maxAlt = -Number.MAX_VALUE;
|
|
this.trajectoryData.forEach(point => {
|
minLon = Math.min(minLon, point.position.longitude);
|
maxLon = Math.max(maxLon, point.position.longitude);
|
minLat = Math.min(minLat, point.position.latitude);
|
maxLat = Math.max(maxLat, point.position.latitude);
|
minAlt = Math.min(minAlt, point.position.altitude);
|
maxAlt = Math.max(maxAlt, point.position.altitude);
|
});
|
|
// 设置相机视角
|
const centerLon = (minLon + maxLon) / 2;
|
const centerLat = (minLat + maxLat) / 2;
|
const centerAlt = (minAlt + maxAlt) / 2;
|
|
this.viewer.camera.setView({
|
destination: Cesium.Cartesian3.fromDegrees(centerLon, centerLat, centerAlt + 500),
|
orientation: {
|
heading: 0.0,
|
pitch: -0.5,
|
roll: 0.0
|
}
|
});
|
}
|
|
/**
|
* 进度条点击跳转
|
*/
|
seekToPosition(event) {
|
const progressBar = document.getElementById('progressBar');
|
const rect = progressBar.getBoundingClientRect();
|
const percent = (event.clientX - rect.left) / rect.width;
|
const frameIndex = Math.floor(percent * (this.trajectoryData.length - 1));
|
this.seekToFrame(frameIndex);
|
}
|
|
/**
|
* 更新状态显示
|
*/
|
updateStatus(message) {
|
const statusDisplay = document.getElementById('statusDisplay');
|
if (statusDisplay) {
|
statusDisplay.textContent = message;
|
}
|
}
|
|
/**
|
* 隐藏加载提示
|
*/
|
hideLoading() {
|
const loadingIndicator = document.getElementById('loadingIndicator');
|
if (loadingIndicator) {
|
loadingIndicator.style.display = 'none';
|
}
|
}
|
}
|
|
// 全局变量
|
let playbackSystem = null;
|
|
// 页面加载完成后初始化
|
document.addEventListener('DOMContentLoaded', () => {
|
// 默认配置,也可以根据需要传入不同的配置
|
const trajectoryConfig = {
|
trajectoryJsonPath: './flight_1581F6Q8D241J00CGT7R_20250803_162454/trajectory_20250803_162454.json',
|
videoFramesDirectory: './flight_1581F6Q8D241J00CGT7R_20250803_162454/video_frames/'
|
};
|
|
playbackSystem = new TrajectoryPlayback(trajectoryConfig);
|
|
// 示例:如何使用不同的轨迹配置
|
// const alternativeConfig = {
|
// trajectoryJsonPath: './flight_another_flight/trajectory_another.json',
|
// videoFramesDirectory: './flight_another_flight/video_frames/'
|
// };
|
// playbackSystem = new TrajectoryPlayback(alternativeConfig);
|
});
|
|
// 全局函数(供HTML调用)
|
function togglePlayback() {
|
if (playbackSystem) playbackSystem.togglePlayback();
|
}
|
|
function previousFrame() {
|
if (playbackSystem) playbackSystem.previousFrame();
|
}
|
|
function nextFrame() {
|
if (playbackSystem) playbackSystem.nextFrame();
|
}
|
|
function changePlaybackSpeed() {
|
if (playbackSystem) playbackSystem.changePlaybackSpeed();
|
}
|
|
function seekToPosition(event) {
|
if (playbackSystem) playbackSystem.seekToPosition(event);
|
}
|