/** * 实时无人机飞行轨迹显示系统 * 连接后端WebSocket接收实时数据并在Cesium中显示 * 参考trajectory-playback.js和app.js的实现 */ class RealTimeTrajectoryViewer { constructor() { // Cesium 相关 this.viewer = null; this.droneEntity = null; this.cameraDirection = null; // 相机中心视角方向箭头 this.cameraFrustum = null; // 相机视锥 this.trajectoryEntity = null; this.trajectoryPoints = []; this.positionMarkers = []; // WebSocket 连接 this.statusSocket = null; this.videoSocket = null; this.isConnected = false; this.isVideoConnected = false; this.reconnectAttempts = 0; this.maxReconnectAttempts = 10; // 无人机状态数据 - 参考app.js结构 this.currentDroneStatus = { device_sn: '', position: { latitude: 0, longitude: 0, altitude: 0 }, attitude: { heading: 0, pitch: 0, roll: 0 }, gimbalAttitude: { pitch: 0, roll: 0, yaw: 0 }, isOnline: false, batteryLevel: 0, timestamp: 0 }; // 视频帧相关 this.currentVideoFrame = null; this.videoTexture = null; // 状态标记 this.firstDataReceived = false; // 是否已接收到第一个数据 this.firstVideoFrameReceived = false; // 是否已接收到第一个视频帧 // 相机参数 (基于DJI相机的典型参数) this.cameraParams = { fov: 84.0, // 对角视场角度 hfov: 84.0, // 水平视场角度 vfov: 53.0, // 垂直视场角度(基于16:9宽高比计算) aspectRatio: 16/9, nearDistance: 1.0, farDistance: 500.0, projectionDistance: 25.0 // 固定投影距离(米) }; // 配置参数 this.config = { websocketHost: window.location.hostname || 'localhost', statusPort: 8765, videoPort: 8766, reconnectInterval: 3000, trajectoryMaxPoints: 1000, droneModelScale: 2.0 }; } /** * 初始化系统 */ async init() { try { await this.initCesium(); await this.connectWebSockets(); this.setupUI(); this.hideLoading(); console.log('✅ 实时轨迹显示系统初始化完成'); } catch (error) { console.error('❌ 系统初始化失败:', error); this.showError('系统初始化失败: ' + error.message); } } /** * 初始化Cesium场景 */ async initCesium() { // 创建Cesium viewer - 参考trajectory-playback.js配置 this.viewer = new Cesium.Viewer('cesiumContainer', { terrain: Cesium.Terrain.fromWorldTerrain(), shadows: true, homeButton: false, sceneModePicker: false, baseLayerPicker: false, navigationHelpButton: false, animation: false, timeline: false, fullscreenButton: false, vrButton: false, geocoder: false, infoBox: false, selectionIndicator: false }); // 禁用默认的相机控制器行为 this.viewer.cesiumWidget.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); // 设置相机初始位置 (西安上空) 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 } }); // 创建无人机实体和相关显示元素 this.createDroneEntity(); console.log('✅ Cesium场景初始化完成'); } /** * 创建无人机实体 - 参考trajectory-playback.js实现 */ 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 }, label: { text: '🚁', font: '16px sans-serif', fillColor: Cesium.Color.WHITE, outlineColor: Cesium.Color.BLACK, outlineWidth: 2, style: Cesium.LabelStyle.FILL_AND_OUTLINE, pixelOffset: new Cesium.Cartesian2(0, -30), showBackground: true, backgroundColor: Cesium.Color.BLACK.withAlpha(0.7) } }); // 创建相机中心视角方向箭头 this.createCameraDirection(); this.createCameraFrustum(); } /** * 创建相机中心视角方向箭头 - 参考trajectory-playback.js实现 */ 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 } }); } /** * 创建相机视锥 - 参考trajectory-playback.js实现 */ createCameraFrustum() { // 相机视锥参数 - 使用统一的cameraParams配置 const fovHorizontal = Cesium.Math.toRadians(this.cameraParams.hfov); // 水平视场角 const fovVertical = Cesium.Math.toRadians(this.cameraParams.vfov); // 垂直视场角 // 创建视锥线框实体 this.cameraFrustum = { fovHorizontal: fovHorizontal, fovVertical: fovVertical, lines: this.viewer.entities.add({ name: '相机视锥线框', polyline: { positions: [], width: 2, material: Cesium.Color.YELLOW, clampToGround: false, show: true } }), surface: this.viewer.entities.add({ name: '相机视锥表面', polygon: { hierarchy: new Cesium.PolygonHierarchy([]), material: Cesium.Color.YELLOW.withAlpha(0.3), outline: false, // 禁用轮廓线以避免与视频帧图像产生闪烁冲突 fill: true, show: true, heightReference: Cesium.HeightReference.NONE, extrudedHeight: 0 } }) }; } /** * 连接WebSocket服务器 */ async connectWebSockets() { await Promise.all([ this.connectStatusWebSocket(), this.connectVideoWebSocket() ]); } /** * 连接状态数据WebSocket - 参考app.js实现 */ async connectStatusWebSocket() { return new Promise((resolve) => { const wsUrl = `ws://${this.config.websocketHost}:${this.config.statusPort}`; console.log(`📡 连接状态WebSocket: ${wsUrl}`); this.statusSocket = new WebSocket(wsUrl); this.statusSocket.onopen = () => { console.log('✅ 状态WebSocket连接成功'); this.reconnectAttempts = 0; this.isConnected = true; this.updateConnectionStatus(true); resolve(); }; this.statusSocket.onmessage = (event) => { try { const message = JSON.parse(event.data); console.log('📨 收到状态WebSocket消息:', message.type); this.handleStatusMessage(message); } catch (error) { console.error('解析状态消息失败:', error); } }; this.statusSocket.onclose = (event) => { console.log('🔌 状态WebSocket连接断开', event.code, event.reason); this.isConnected = false; this.updateConnectionStatus(false); if (!event.wasClean) { this.scheduleReconnect('status'); } }; this.statusSocket.onerror = (error) => { console.error('❌ 状态WebSocket错误:', error); resolve(); // 即使连接失败也继续 }; }); } /** * 连接视频流WebSocket - 参考app.js实现 */ async connectVideoWebSocket() { return new Promise((resolve) => { const wsUrl = `ws://${this.config.websocketHost}:${this.config.videoPort}`; console.log(`📺 连接视频WebSocket: ${wsUrl}`); this.videoSocket = new WebSocket(wsUrl); this.videoSocket.onopen = () => { console.log('✅ 视频WebSocket连接成功'); this.isVideoConnected = true; resolve(); }; this.videoSocket.onmessage = (event) => { try { const message = JSON.parse(event.data); console.log('📨 收到视频WebSocket消息:', message.type); this.handleVideoMessage(message); } catch (error) { console.error('解析视频消息失败:', error); } }; this.videoSocket.onclose = (event) => { console.log('🔌 视频WebSocket连接断开', event.code, event.reason); this.isVideoConnected = false; if (!event.wasClean) { this.scheduleReconnect('video'); } }; this.videoSocket.onerror = (error) => { console.error('❌ 视频WebSocket错误:', error); resolve(); // 即使连接失败也继续 }; }); } /** * 处理状态消息 - 参考app.js实现 */ handleStatusMessage(message) { console.log('📨 处理状态消息:', message.type, message.data ? Object.keys(message.data) : 'no data'); // 第一次接收到任何状态数据时隐藏加载动画 if (!this.firstDataReceived) { this.firstDataReceived = true; this.hideLoading(); console.log('✅ 首次接收到状态数据,隐藏加载动画'); } switch (message.type) { case 'connection_status': console.log('📡 连接状态:', message.data); break; case 'drone_status': this.updateDroneStatus(message.data); break; case 'coordinates': this.updateDronePosition(message.data); break; case 'gimbal_attitude': this.updateGimbalAttitude(message.data); break; case 'error': console.error('服务器错误:', message.data); break; default: console.log('未知状态消息类型:', message.type); } } /** * 处理视频消息 - 参考app.js实现 */ handleVideoMessage(message) { console.log('📨 处理视频消息:', message.type); switch (message.type) { case 'video_frame': console.log('🎥 收到视频帧,frame大小:', message.data.frame ? message.data.frame.length : 'no frame'); this.updateVideoFrame(message.data); break; case 'connection_established': console.log('📺 视频连接已建立'); break; case 'stats': console.log('📊 视频服务器统计:', message.data); break; default: console.log('未知视频消息类型:', message.type); } } /** * 更新无人机状态 */ updateDroneStatus(data) { this.currentDroneStatus.device_sn = data.device_sn || ''; this.currentDroneStatus.isOnline = data.is_online || false; this.currentDroneStatus.batteryLevel = data.battery_level || 0; this.currentDroneStatus.timestamp = data.timestamp || Date.now() / 1000; // 更新UI this.updateStatusDisplay(); console.log(`🚁 无人机状态更新: ${data.device_sn}, 在线: ${data.is_online}`); } /** * 更新无人机位置 - 参考app.js结构 */ updateDronePosition(data) { if (!data.coordinates) return; const coords = data.coordinates; console.log('📍 位置更新:', coords); // 验证坐标数据有效性 if (!coords || !isFinite(coords.latitude) || !isFinite(coords.longitude) || !isFinite(coords.altitude)) { console.warn('接收到无效的坐标数据,跳过位置更新:', coords); return; } // 第一次接收到数据时隐藏加载动画 if (!this.firstDataReceived) { this.firstDataReceived = true; this.hideLoading(); console.log('✅ 首次接收到数据,隐藏加载动画'); } this.currentDroneStatus.position = { latitude: coords.latitude, longitude: coords.longitude, altitude: coords.altitude }; this.currentDroneStatus.attitude = { heading: isFinite(coords.heading) ? coords.heading : 0, pitch: isFinite(coords.pitch) ? coords.pitch : 0, roll: isFinite(coords.roll) ? coords.roll : 0 }; // 添加轨迹点 this.addTrajectoryPoint(); // 更新无人机位置显示 this.updateDroneEntity(); // 更新相机方向箭头 this.updateCameraDirection(); // 更新相机视锥 this.updateCameraFrustum(); // 不再自动更新相机视角,允许用户自由控制 // this.updateCameraView(); // 更新UI显示 this.updateInfoPanel(); console.log(`📍 位置更新完成: (${coords.latitude.toFixed(6)}, ${coords.longitude.toFixed(6)}, ${coords.altitude.toFixed(1)}m)`); } /** * 更新云台姿态 - 参考app.js实现 */ updateGimbalAttitude(data) { if (!data.gimbal_attitude) return; const gimbal = data.gimbal_attitude; console.log('📹 云台姿态更新:', gimbal); // 验证云台姿态数据有效性 if (!gimbal) { console.warn('接收到空的云台姿态数据'); return; } this.currentDroneStatus.gimbalAttitude = { pitch: isFinite(gimbal.pitch) ? gimbal.pitch : 0, roll: isFinite(gimbal.roll) ? gimbal.roll : 0, yaw: isFinite(gimbal.yaw) ? gimbal.yaw : 0 }; // 更新相机方向箭头 this.updateCameraDirection(); // 更新视锥投影 this.updateCameraFrustum(); const pitchValue = isFinite(gimbal.pitch) ? gimbal.pitch.toFixed(1) : '0.0'; const yawValue = isFinite(gimbal.yaw) ? gimbal.yaw.toFixed(1) : '0.0'; console.log(`📐 云台姿态更新完成: pitch=${pitchValue}°, yaw=${yawValue}°`); } /** * 更新视频帧 */ updateVideoFrame(data) { if (!data.frame) return; // 第一次接收到视频帧时自动定位相机到无人机附近 if (!this.firstVideoFrameReceived) { this.firstVideoFrameReceived = true; console.log('✅ 首次接收到视频帧,检查是否需要自动定位相机'); // 如果有有效的无人机位置,则自动定位相机 const position = this.currentDroneStatus.position; if (position && isFinite(position.latitude) && isFinite(position.longitude) && isFinite(position.altitude) && !(position.latitude === 0 && position.longitude === 0)) { this.flyToInitialDronePosition(position); console.log('✅ 基于视频帧触发相机自动定位到无人机位置'); } } this.currentVideoFrame = { data: data.frame, timestamp: data.timestamp, width: data.width, height: data.height, format: data.format }; // 更新视频预览 this.updateVideoPreview(); // 更新视频投影 this.updateVideoProjection(); console.log(`🎥 视频帧更新: ${data.width}x${data.height}, ${data.frame.length} bytes`); } /** * 添加轨迹点 */ addTrajectoryPoint() { const position = this.currentDroneStatus.position; // 验证位置数据 if (!position || !isFinite(position.latitude) || !isFinite(position.longitude) || !isFinite(position.altitude) || position.latitude === 0 && position.longitude === 0) { console.warn('跳过无效位置的轨迹点添加:', position); return; } const cartesian = Cesium.Cartesian3.fromDegrees( position.longitude, position.latitude, position.altitude ); // 验证计算出的笛卡尔坐标 if (!cartesian || !isFinite(cartesian.x) || !isFinite(cartesian.y) || !isFinite(cartesian.z)) { console.warn('跳过无效笛卡尔坐标的轨迹点:', cartesian); return; } this.trajectoryPoints.push(cartesian); // 限制轨迹点数量 if (this.trajectoryPoints.length > this.config.trajectoryMaxPoints) { this.trajectoryPoints.shift(); // 同时移除对应的位置标记 if (this.positionMarkers.length > 0) { const oldMarker = this.positionMarkers.shift(); this.viewer.entities.remove(oldMarker); } } // 创建位置标记 const marker = this.viewer.entities.add({ position: cartesian, point: { pixelSize: 8, color: Cesium.Color.RED, outlineColor: Cesium.Color.DARKRED, outlineWidth: 2, heightReference: Cesium.HeightReference.NONE // 使用绝对高度,与轨迹线保持一致 } }); this.positionMarkers.push(marker); // 更新轨迹线 this.updateTrajectoryLine(); } /** * 更新轨迹线 */ updateTrajectoryLine() { if (this.trajectoryPoints.length < 2) return; // 过滤掉无效的轨迹点 const validTrajectoryPoints = this.trajectoryPoints.filter(point => { return point && isFinite(point.x) && isFinite(point.y) && isFinite(point.z); }); if (validTrajectoryPoints.length < 2) { console.warn('有效轨迹点不足,跳过轨迹线更新'); return; } if (this.trajectoryEntity) { this.viewer.entities.remove(this.trajectoryEntity); } try { this.trajectoryEntity = this.viewer.entities.add({ polyline: { positions: validTrajectoryPoints, width: 3, material: new Cesium.PolylineDashMaterialProperty({ color: Cesium.Color.RED, dashLength: 16.0, dashPattern: 255 }), clampToGround: false } }); } catch (error) { console.error('创建轨迹线失败:', error); } } /** * 更新无人机实体 */ updateDroneEntity() { const position = this.currentDroneStatus.position; const attitude = this.currentDroneStatus.attitude; if (position.latitude === 0 && position.longitude === 0) return; const cartesian = Cesium.Cartesian3.fromDegrees( position.longitude, position.latitude, position.altitude ); // 更新无人机位置 this.droneEntity.position = cartesian; // 更新标签文本 if (this.droneEntity.label) { this.droneEntity.label.text = `🚁 GIS小丸子一号`; } // 设置无人机姿态 this.droneEntity.orientation = Cesium.Transforms.headingPitchRollQuaternion( cartesian, new Cesium.HeadingPitchRoll( Cesium.Math.toRadians(attitude.heading), Cesium.Math.toRadians(attitude.pitch), Cesium.Math.toRadians(attitude.roll) ) ); } /** * 更新相机方向箭头 - 参考trajectory-playback.js实现 */ updateCameraDirection() { if (!this.cameraDirection) return; const position = this.currentDroneStatus.position; const gimbalAttitude = this.currentDroneStatus.gimbalAttitude; if (position.latitude === 0 && position.longitude === 0) return; const droneCartesian = Cesium.Cartesian3.fromDegrees( position.longitude, position.latitude, position.altitude ); 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轴正方向(向北) 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(droneCartesian); const worldDirection = Cesium.Matrix4.multiplyByPointAsVector(transform, enuForward, new Cesium.Cartesian3()); // 计算箭头终点位置 const endPosition = Cesium.Cartesian3.add(droneCartesian, worldDirection, new Cesium.Cartesian3()); // 验证计算结果的有效性 if (!endPosition || !isFinite(endPosition.x) || !isFinite(endPosition.y) || !isFinite(endPosition.z)) { console.warn('相机方向箭头终点坐标无效,跳过更新:', endPosition); return; } // 更新箭头位置 try { this.cameraDirection.polyline.positions = [droneCartesian, endPosition]; } catch (error) { console.error('更新相机方向箭头失败:', error); } } /** * 更新相机视锥 - 固定在25米距离的平面上 */ updateCameraFrustum() { if (!this.cameraFrustum) return; const position = this.currentDroneStatus.position; const gimbalAttitude = this.currentDroneStatus.gimbalAttitude; // 验证位置数据有效性 if (!position || !isFinite(position.latitude) || !isFinite(position.longitude) || !isFinite(position.altitude) || position.latitude === 0 && position.longitude === 0) { console.warn('无效的位置数据,跳过视锥更新:', position); return; } // 验证云台姿态数据有效性 if (!gimbalAttitude) { console.warn('缺少云台姿态数据,跳过视锥更新'); return; } // 确保角度值有效,如果无效则使用默认值0 const yawValue = isFinite(gimbalAttitude.yaw) ? gimbalAttitude.yaw : 0; const pitchValue = isFinite(gimbalAttitude.pitch) ? gimbalAttitude.pitch : 0; const rollValue = isFinite(gimbalAttitude.roll) ? gimbalAttitude.roll : 0; const droneCartesian = Cesium.Cartesian3.fromDegrees( position.longitude, position.latitude, position.altitude ); // 验证计算出的笛卡尔坐标 if (!droneCartesian || !isFinite(droneCartesian.x) || !isFinite(droneCartesian.y) || !isFinite(droneCartesian.z)) { console.warn('无效的无人机笛卡尔坐标,跳过视锥更新:', droneCartesian); return; } // 获取云台的绝对姿态角度(转换为弧度) const yaw = Cesium.Math.toRadians(yawValue); const pitch = Cesium.Math.toRadians(pitchValue); const roll = Cesium.Math.toRadians(rollValue); // 视锥参数 const hfov = this.cameraParams.hfov || 84.0; // 默认水平视场角 const vfov = this.cameraParams.vfov || 53.0; // 默认垂直视场角 // 验证视场角参数 if (!isFinite(hfov) || !isFinite(vfov) || hfov <= 0 || vfov <= 0) { console.warn('无效的视场角参数,跳过视锥更新:', { hfov, vfov }); return; } const fovHorizontal = Cesium.Math.toRadians(hfov); const fovVertical = Cesium.Math.toRadians(vfov); const halfFovH = fovHorizontal / 2; const halfFovV = fovVertical / 2; // 验证计算出的角度值 if (!isFinite(halfFovH) || !isFinite(halfFovV)) { console.warn('无效的半视场角计算结果,跳过视锥更新:', { halfFovH, halfFovV }); return; } // 使用配置的投影距离 const projectionDistance = this.cameraParams.projectionDistance || 25.0; // 在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)); // 验证NED向量的有效性 const nedVectors = [nedTopLeft, nedTopRight, nedBottomLeft, nedBottomRight]; for (const vector of nedVectors) { if (!vector || !isFinite(vector.x) || !isFinite(vector.y) || !isFinite(vector.z)) { console.warn('无效的NED方向向量,跳过视锥更新:', vector); return; } } // 归一化方向向量 Cesium.Cartesian3.normalize(nedTopLeft, nedTopLeft); Cesium.Cartesian3.normalize(nedTopRight, nedTopRight); Cesium.Cartesian3.normalize(nedBottomLeft, nedBottomLeft); Cesium.Cartesian3.normalize(nedBottomRight, nedBottomRight); // 验证归一化后的向量 for (const vector of nedVectors) { if (!vector || !isFinite(vector.x) || !isFinite(vector.y) || !isFinite(vector.z)) { console.warn('归一化后的NED向量无效,跳过视锥更新:', vector); return; } } // 创建旋转矩阵 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); // 验证旋转矩阵的有效性 if (!rotationMatrix || rotationMatrix.toString().includes('NaN')) { console.warn('无效的旋转矩阵,跳过视锥更新:', rotationMatrix); return; } // 应用旋转到方向向量 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()); // 验证旋转后的向量 const rotatedVectors = [rotatedTopLeft, rotatedTopRight, rotatedBottomLeft, rotatedBottomRight]; for (const vector of rotatedVectors) { if (!vector || !isFinite(vector.x) || !isFinite(vector.y) || !isFinite(vector.z)) { console.warn('旋转后的方向向量无效,跳过视锥更新:', vector); return; } } // 转换到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(droneCartesian); 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 worldVectors = [worldTopLeft, worldTopRight, worldBottomLeft, worldBottomRight]; for (const vector of worldVectors) { if (!vector || !isFinite(vector.x) || !isFinite(vector.y) || !isFinite(vector.z)) { console.warn('世界坐标系方向向量无效,跳过视锥更新:', vector); return; } } // 计算固定距离25米处的投影平面四个角点 const projectionTopLeft = Cesium.Cartesian3.add(droneCartesian, Cesium.Cartesian3.multiplyByScalar(worldTopLeft, projectionDistance, new Cesium.Cartesian3()), new Cesium.Cartesian3()); const projectionTopRight = Cesium.Cartesian3.add(droneCartesian, Cesium.Cartesian3.multiplyByScalar(worldTopRight, projectionDistance, new Cesium.Cartesian3()), new Cesium.Cartesian3()); const projectionBottomLeft = Cesium.Cartesian3.add(droneCartesian, Cesium.Cartesian3.multiplyByScalar(worldBottomLeft, projectionDistance, new Cesium.Cartesian3()), new Cesium.Cartesian3()); const projectionBottomRight = Cesium.Cartesian3.add(droneCartesian, Cesium.Cartesian3.multiplyByScalar(worldBottomRight, projectionDistance, new Cesium.Cartesian3()), new Cesium.Cartesian3()); // 验证计算结果 const frustumPoints = [projectionTopLeft, projectionTopRight, projectionBottomLeft, projectionBottomRight]; for (const point of frustumPoints) { if (!point || !isFinite(point.x) || !isFinite(point.y) || !isFinite(point.z)) { console.warn('视锥投影平面计算结果包含无效坐标,跳过更新'); return; } } // 创建视锥线框(从无人机到投影平面四个角点) const frustumLines = [ // 从相机中心到投影平面四个角点的射线 droneCartesian, projectionTopLeft, droneCartesian, projectionTopRight, droneCartesian, projectionBottomLeft, droneCartesian, projectionBottomRight, // 投影平面四条边 projectionTopLeft, projectionTopRight, // 顶边 projectionTopRight, projectionBottomRight, // 右边 projectionBottomRight, projectionBottomLeft, // 底边 projectionBottomLeft, projectionTopLeft // 左边 ]; // 更新线框 this.cameraFrustum.lines.polyline.positions = frustumLines; // 创建投影平面表面 - 按正确顺序排列顶点 const frustumSurface = [ projectionBottomLeft, projectionTopLeft, projectionTopRight, projectionBottomRight ]; // 验证投影平面顶点的有效性 for (const point of frustumSurface) { if (!point || !isFinite(point.x) || !isFinite(point.y) || !isFinite(point.z)) { console.warn('投影平面顶点无效,跳过表面更新:', point); return; } } // 更新表面 this.cameraFrustum.surface.polygon.hierarchy = new Cesium.PolygonHierarchy(frustumSurface); console.log(`✅ 固定${projectionDistance}米距离投影平面视锥更新完成 - 顶点:`, frustumSurface.length); } /** * 更新视频帧投影到固定25米平面 */ updateVideoProjection() { if (!this.currentVideoFrame || !this.cameraFrustum || !this.cameraFrustum.surface) return; const position = this.currentDroneStatus.position; const gimbalAttitude = this.currentDroneStatus.gimbalAttitude; if (position.latitude === 0 && position.longitude === 0) return; try { // 直接使用base64数据,与视频预览相同的方式 const imageDataURL = `data:image/jpeg;base64,${this.currentVideoFrame.data}`; // 创建临时图像对象来获取真实尺寸 const img = new Image(); img.onload = () => { // 计算图像的真实宽高比 const imageAspectRatio = img.width / img.height; // 获取当前云台的yaw角度作为图片旋转角度 const rotationAngle = Cesium.Math.toRadians(gimbalAttitude.yaw || 0); // 创建根据云台yaw角度旋转的图片 this.createRotatedVideoImage(imageDataURL, img.width, img.height, rotationAngle) .then(rotatedImageDataURL => { // 获取当前材质 const currentMaterial = this.cameraFrustum.surface.polygon.material; // 检查是否已经存在ImageMaterialProperty材质 if (currentMaterial && currentMaterial instanceof Cesium.ImageMaterialProperty) { // 复用现有材质,只更新图像属性 currentMaterial.image = rotatedImageDataURL; console.log(`🔄 复用材质更新25米平面视频投影: ${img.width}x${img.height}, 宽高比: ${imageAspectRatio.toFixed(2)}, 旋转角度: ${(gimbalAttitude.yaw || 0).toFixed(1)}°`); } else { // 首次创建材质或材质类型不匹配时才创建新材质 const imageMaterial = new Cesium.ImageMaterialProperty({ image: rotatedImageDataURL, transparent: false }); this.cameraFrustum.surface.polygon.material = imageMaterial; console.log(`� 首次创建材质: ${img.width}x${img.height}, 宽高比: ${imageAspectRatio.toFixed(2)}, 旋转角度: ${(gimbalAttitude.yaw || 0).toFixed(1)}°`); } }) .catch(error => { console.error('创建旋转视频图像失败:', error); // 如果旋转失败,直接使用原图更新材质 const currentMaterial = this.cameraFrustum.surface.polygon.material; if (currentMaterial && currentMaterial instanceof Cesium.ImageMaterialProperty) { // 复用现有材质,只更新图像 currentMaterial.image = imageDataURL; } else { // 创建新材质 const imageMaterial = new Cesium.ImageMaterialProperty({ image: imageDataURL, transparent: false }); this.cameraFrustum.surface.polygon.material = imageMaterial; } }); }; img.onerror = () => { console.error('视频帧图像加载失败'); // 如果图像加载失败,使用半透明红色作为备用 this.cameraFrustum.surface.polygon.material = Cesium.Color.RED.withAlpha(0.3); }; img.src = imageDataURL; } catch (error) { console.error('25米平面视频投影处理失败:', error); // 如果处理失败,使用半透明红色作为备用 if (this.cameraFrustum.surface) { this.cameraFrustum.surface.polygon.material = Cesium.Color.RED.withAlpha(0.3); } } } /** * 创建根据云台角度旋转的视频图像 - 参考trajectory-playback.js实现 */ createRotatedVideoImage(originalImageSrc, width, height, rotationAngle = 0) { return new Promise((resolve, reject) => { // 验证输入参数 if (!originalImageSrc || typeof originalImageSrc !== 'string') { reject(new Error('无效的图像源')); return; } if (!isFinite(width) || !isFinite(height) || width <= 0 || height <= 0) { reject(new Error('无效的图像尺寸')); return; } if (!isFinite(rotationAngle)) { console.warn('旋转角度无效,使用默认值0'); rotationAngle = 0; } 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; // 验证计算结果的有效性 if (!isFinite(newWidth) || !isFinite(newHeight) || newWidth <= 0 || newHeight <= 0) { reject(new Error('计算出的画布尺寸无效')); return; } canvas.width = newWidth; canvas.height = newHeight; // 创建图像对象 const img = new Image(); img.onload = () => { try { // 清除画布 ctx.clearRect(0, 0, newWidth, newHeight); // 移动到画布中心 ctx.translate(newWidth / 2, newHeight / 2); // 应用旋转 ctx.rotate(rotationAngle); // 绘制图像(中心对齐) ctx.drawImage(img, -width / 2, -height / 2, width, height); // 重置变换 ctx.setTransform(1, 0, 0, 1, 0, 0); // 将画布转换为数据URL const rotatedImageDataURL = canvas.toDataURL('image/jpeg', 0.9); resolve(rotatedImageDataURL); } catch (error) { reject(error); } }; img.onerror = () => { reject(new Error('图像加载失败')); }; img.src = originalImageSrc; }); } /** * 更新相机视角 */ updateCameraView() { const position = this.currentDroneStatus.position; // 验证位置数据的有效性 if (!position || !isFinite(position.latitude) || !isFinite(position.longitude) || !isFinite(position.altitude) || (position.latitude === 0 && position.longitude === 0)) { console.warn('updateCameraView: 无效的位置数据,跳过相机视角更新'); return; } try { // 自动跟随无人机 const droneCartesian = Cesium.Cartesian3.fromDegrees( position.longitude, position.latitude, position.altitude + 100 // 在无人机上方100米 ); // 验证计算结果 if (!droneCartesian || !isFinite(droneCartesian.x) || !isFinite(droneCartesian.y) || !isFinite(droneCartesian.z)) { console.warn('updateCameraView: 无人机位置坐标转换结果无效'); return; } this.viewer.camera.lookAt( droneCartesian, new Cesium.Cartesian3(0, 0, 50) ); } catch (error) { console.error('updateCameraView: 相机视角更新失败:', error); } } /** * 首次收到位置数据时飞行到无人机位置 */ flyToInitialDronePosition(coords) { // 验证坐标数据 if (!coords || !isFinite(coords.latitude) || !isFinite(coords.longitude) || !isFinite(coords.altitude)) { console.warn('flyToInitialDronePosition: 无效的坐标数据', coords); return; } try { // 计算无人机位置 const droneCartesian = Cesium.Cartesian3.fromDegrees( coords.longitude, coords.latitude, coords.altitude ); // 验证计算结果 if (!droneCartesian || !isFinite(droneCartesian.x) || !isFinite(droneCartesian.y) || !isFinite(droneCartesian.z)) { console.warn('flyToInitialDronePosition: 无人机位置坐标转换结果无效'); return; } // 计算相机位置:在无人机西南方向500米,高度比无人机高200米 const cameraDistance = 500; // 相机距离无人机的距离 const cameraHeightOffset = 200; // 相机比无人机高的高度 // 计算相机位置(西南方向,45度角) const heading = Cesium.Math.toRadians(225); // 西南方向 const pitch = Cesium.Math.toRadians(-30); // 向下30度角看无人机 const cameraPosition = Cesium.Cartesian3.fromDegrees( coords.longitude - 0.005, // 向西偏移约500米 coords.latitude - 0.005, // 向南偏移约500米 coords.altitude + cameraHeightOffset ); // 使用flyTo平滑飞行到目标位置 this.viewer.camera.flyTo({ destination: cameraPosition, orientation: { heading: heading, pitch: pitch, roll: 0 }, duration: 3.0, // 3秒飞行时间 complete: () => { console.log('✅ 相机已定位到无人机位置'); // 飞行完成后,让相机看向无人机 this.viewer.camera.lookAt(droneCartesian, new Cesium.Cartesian3(0, 0, 100)); } }); console.log(`📹 正在飞行到无人机位置: (${coords.latitude.toFixed(6)}, ${coords.longitude.toFixed(6)}, ${coords.altitude.toFixed(1)}m)`); } catch (error) { console.error('flyToInitialDronePosition: 飞行到无人机位置失败:', error); } } /** * 更新状态显示 */ updateStatusDisplay() { const statusElement = document.getElementById('statusDisplay'); if (statusElement) { const status = this.currentDroneStatus; statusElement.textContent = status.isOnline ? `📡 ${status.device_sn} 在线 | 电量: ${status.batteryLevel}%` : '📴 设备离线'; } } /** * 更新信息面板 */ updateInfoPanel() { const position = this.currentDroneStatus.position; const attitude = this.currentDroneStatus.attitude; const gimbal = this.currentDroneStatus.gimbalAttitude; // 更新各个信息字段 this.updateElement('latitude', position.latitude.toFixed(6) + '°'); this.updateElement('longitude', position.longitude.toFixed(6) + '°'); this.updateElement('altitude', position.altitude.toFixed(1) + 'm'); this.updateElement('heading', attitude.heading.toFixed(1) + '°'); this.updateElement('pitch', attitude.pitch.toFixed(1) + '°'); this.updateElement('roll', attitude.roll.toFixed(1) + '°'); this.updateElement('gimbalYaw', gimbal.yaw.toFixed(1) + '°'); this.updateElement('gimbalPitch', gimbal.pitch.toFixed(1) + '°'); this.updateElement('gimbalRoll', gimbal.roll.toFixed(1) + '°'); this.updateElement('battery', this.currentDroneStatus.batteryLevel + '%'); } /** * 更新视频预览 */ updateVideoPreview() { const videoFrame = document.getElementById('videoFrame'); const videoPlaceholder = document.getElementById('videoPlaceholder'); if (videoFrame && videoPlaceholder && this.currentVideoFrame) { videoFrame.src = 'data:image/jpeg;base64,' + this.currentVideoFrame.data; videoFrame.style.display = 'block'; videoPlaceholder.style.display = 'none'; } } /** * 更新元素内容 */ updateElement(id, value) { const element = document.getElementById(id); if (element) { element.textContent = value; } } /** * 更新连接状态 */ updateConnectionStatus(connected) { const statusElement = document.getElementById('connectionStatus'); if (statusElement) { statusElement.textContent = connected ? '🟢 已连接' : '🔴 连接断开'; } } /** * 安排重连 */ scheduleReconnect(type) { const key = `${type}ReconnectTimeout`; if (this[key]) { clearTimeout(this[key]); } this[key] = setTimeout(() => { console.log(`尝试重连 ${type}...`); if (type === 'status') { this.connectStatusWebSocket(); } else if (type === 'video') { this.connectVideoWebSocket(); } }, 3000); } /** * 设置UI */ setupUI() { // 添加连接状态显示 const statusDiv = document.createElement('div'); statusDiv.id = 'connectionStatus'; statusDiv.style.position = 'absolute'; statusDiv.style.top = '10px'; statusDiv.style.right = '10px'; statusDiv.style.zIndex = '1000'; statusDiv.style.color = 'white'; statusDiv.style.background = 'rgba(0,0,0,0.7)'; statusDiv.style.padding = '5px 10px'; statusDiv.style.borderRadius = '5px'; document.body.appendChild(statusDiv); } /** * 隐藏加载界面 */ hideLoading() { const loading = document.getElementById('loadingIndicator'); if (loading) { loading.style.display = 'none'; } } /** * 显示错误信息 */ showError(message) { console.error('错误:', message); alert('错误: ' + message); } /** * 清理资源 */ cleanup() { if (this.statusSocket) { this.statusSocket.close(); } if (this.videoSocket) { this.videoSocket.close(); } if (this.statusReconnectTimeout) { clearTimeout(this.statusReconnectTimeout); } if (this.videoReconnectTimeout) { clearTimeout(this.videoReconnectTimeout); } if (this.viewer) { this.viewer.destroy(); } } } // 全局变量 let realTimeViewer = null; // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', async () => { try { realTimeViewer = new RealTimeTrajectoryViewer(); await realTimeViewer.init(); } catch (error) { console.error('系统启动失败:', error); } }); // 页面卸载时清理资源 window.addEventListener('beforeunload', () => { if (realTimeViewer) { realTimeViewer.cleanup(); } });