/** * 无人机轨迹回放系统 * 纯前端实现,基于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 = `
📹 视频帧加载失败
${point.video_frame}
`; }; } else { videoFrame.style.display = 'none'; videoPlaceholder.style.display = 'block'; videoPlaceholder.innerHTML = `
📹 无视频帧数据
时间点: ${point.timestamp}
`; } } /** * 更新播放按钮 */ 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); }