/**
|
* 实时无人机飞行轨迹显示系统
|
* 连接后端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();
|
}
|
});
|