<!DOCTYPE html>
|
<html lang="zh-CN">
|
<head>
|
<meta charset="UTF-8">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>DJI无人机实时监控系统</title>
|
<!-- CesiumJS -->
|
<script src="https://cesium.com/downloads/cesiumjs/releases/1.131/Build/Cesium/Cesium.js"></script>
|
<link href="https://cesium.com/downloads/cesiumjs/releases/1.131/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
|
|
<!-- 相机视锥投影模块 -->
|
<script src="camera-frustum-projection.js"></script>
|
|
<style>
|
* {
|
margin: 0;
|
padding: 0;
|
box-sizing: border-box;
|
}
|
|
html, body {
|
width: 100%;
|
height: 100%;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
overflow: hidden;
|
}
|
|
/* Cesium地图容器 - 全屏铺底 */
|
#cesiumContainer {
|
position: absolute;
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 100%;
|
z-index: 1;
|
}
|
|
/* 顶部状态栏 */
|
.status-bar {
|
position: absolute;
|
top: 20px;
|
left: 20px;
|
z-index: 1000;
|
display: flex;
|
gap: 15px;
|
align-items: center;
|
}
|
|
.status-item {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
padding: 8px 16px;
|
background: rgba(0, 0, 0, 0.8);
|
border-radius: 20px;
|
font-size: 14px;
|
font-weight: 500;
|
color: white;
|
backdrop-filter: blur(10px);
|
}
|
|
.status-indicator {
|
width: 12px;
|
height: 12px;
|
border-radius: 50%;
|
animation: pulse 2s infinite;
|
}
|
|
.status-online { background: #52c41a; }
|
.status-offline { background: #ff4d4f; }
|
.status-connecting { background: #1890ff; }
|
|
@keyframes pulse {
|
0% { opacity: 1; }
|
50% { opacity: 0.5; }
|
100% { opacity: 1; }
|
}
|
|
/* 右侧信息面板 */
|
.info-panel {
|
position: absolute;
|
top: 0;
|
right: 0;
|
width: 400px;
|
height: 100%;
|
background: rgba(0, 0, 0, 0.9);
|
backdrop-filter: blur(10px);
|
color: white;
|
z-index: 1000;
|
transform: translateX(0);
|
transition: transform 0.3s ease;
|
border-left: 2px solid rgba(255, 255, 255, 0.2);
|
}
|
|
.info-panel.collapsed {
|
transform: translateX(350px);
|
}
|
|
/* 面板切换按钮 */
|
.panel-toggle {
|
position: absolute;
|
left: -40px;
|
top: 50%;
|
transform: translateY(-50%);
|
width: 40px;
|
height: 80px;
|
background: rgba(0, 0, 0, 0.9);
|
border: none;
|
color: white;
|
cursor: pointer;
|
border-radius: 8px 0 0 8px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
font-size: 18px;
|
transition: all 0.3s ease;
|
}
|
|
.panel-toggle:hover {
|
background: rgba(0, 0, 0, 1);
|
}
|
|
/* 面板头部 */
|
.panel-header {
|
padding: 20px;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
background: rgba(0, 0, 0, 0.3);
|
}
|
|
.panel-title {
|
font-size: 20px;
|
font-weight: 600;
|
margin-bottom: 10px;
|
}
|
|
/* 面板内容 */
|
.panel-content {
|
height: calc(100% - 80px);
|
overflow-y: auto;
|
padding: 20px;
|
}
|
|
/* 信息卡片 */
|
.info-card {
|
background: rgba(255, 255, 255, 0.1);
|
border-radius: 12px;
|
padding: 20px;
|
margin-bottom: 20px;
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
}
|
|
.info-card h3 {
|
font-size: 16px;
|
margin-bottom: 15px;
|
color: #4CAF50;
|
border-bottom: 1px solid rgba(76, 175, 80, 0.3);
|
padding-bottom: 8px;
|
}
|
|
.info-grid {
|
display: grid;
|
grid-template-columns: 1fr 1fr;
|
gap: 15px;
|
}
|
|
.info-item {
|
text-align: center;
|
}
|
|
.info-label {
|
font-size: 12px;
|
color: rgba(255, 255, 255, 0.7);
|
margin-bottom: 5px;
|
text-transform: uppercase;
|
letter-spacing: 0.5px;
|
}
|
|
.info-value {
|
font-size: 18px;
|
font-weight: 600;
|
color: white;
|
}
|
|
/* 视频预览区域 */
|
.video-preview {
|
background: rgba(255, 255, 255, 0.1);
|
border-radius: 12px;
|
padding: 15px;
|
margin-bottom: 20px;
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
text-align: center;
|
min-height: 200px;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.video-display {
|
max-width: 100%;
|
max-height: 150px;
|
border-radius: 8px;
|
}
|
|
.video-placeholder {
|
color: rgba(255, 255, 255, 0.7);
|
}
|
|
/* 控制按钮 */
|
.controls-section {
|
background: rgba(255, 255, 255, 0.1);
|
border-radius: 12px;
|
padding: 20px;
|
margin-bottom: 20px;
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
}
|
|
.btn {
|
width: 100%;
|
padding: 12px;
|
margin-bottom: 10px;
|
border: none;
|
border-radius: 8px;
|
font-size: 14px;
|
font-weight: 600;
|
cursor: pointer;
|
transition: all 0.3s ease;
|
text-transform: uppercase;
|
letter-spacing: 0.5px;
|
}
|
|
.btn-primary {
|
background: linear-gradient(135deg, #3498db, #2980b9);
|
color: white;
|
}
|
|
.btn-success {
|
background: linear-gradient(135deg, #27ae60, #229954);
|
color: white;
|
}
|
|
.btn-danger {
|
background: linear-gradient(135deg, #e74c3c, #c0392b);
|
color: white;
|
}
|
|
.btn:hover {
|
transform: translateY(-2px);
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
}
|
|
.btn:disabled {
|
opacity: 0.5;
|
cursor: not-allowed;
|
transform: none !important;
|
}
|
|
/* 滚动条样式 */
|
.panel-content::-webkit-scrollbar {
|
width: 8px;
|
}
|
|
.panel-content::-webkit-scrollbar-track {
|
background: rgba(255, 255, 255, 0.1);
|
border-radius: 4px;
|
}
|
|
.panel-content::-webkit-scrollbar-thumb {
|
background: rgba(255, 255, 255, 0.3);
|
border-radius: 4px;
|
}
|
|
/* 消息提示 */
|
.message {
|
position: fixed;
|
top: 100px;
|
right: 20px;
|
padding: 15px 20px;
|
border-radius: 8px;
|
color: white;
|
font-weight: 500;
|
z-index: 2000;
|
transform: translateX(100%);
|
transition: transform 0.3s ease;
|
backdrop-filter: blur(10px);
|
}
|
|
.message.show {
|
transform: translateX(0);
|
}
|
|
.message.success {
|
background: rgba(39, 174, 96, 0.9);
|
}
|
|
.message.error {
|
background: rgba(231, 76, 60, 0.9);
|
}
|
|
.message.info {
|
background: rgba(52, 152, 219, 0.9);
|
}
|
|
/* 加载动画 */
|
.loading {
|
display: inline-block;
|
width: 20px;
|
height: 20px;
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
border-radius: 50%;
|
border-top-color: #fff;
|
animation: spin 1s ease-in-out infinite;
|
margin-left: 10px;
|
}
|
|
@keyframes spin {
|
to { transform: rotate(360deg); }
|
}
|
|
/* 响应式设计 */
|
@media (max-width: 768px) {
|
.info-panel {
|
width: 100%;
|
}
|
|
.info-panel.collapsed {
|
transform: translateX(100%);
|
}
|
|
.status-bar {
|
flex-direction: column;
|
align-items: flex-start;
|
}
|
}
|
</style>
|
</head>
|
<body>
|
<!-- Cesium地图容器 -->
|
<div id="cesiumContainer"></div>
|
|
<!-- 顶部状态栏 -->
|
<div class="status-bar">
|
<div class="status-item">
|
<div class="status-indicator status-offline" id="wsStatus"></div>
|
<span id="wsStatusText">WebSocket连接中...</span>
|
</div>
|
<div class="status-item">
|
<div class="status-indicator status-offline" id="mqttStatus"></div>
|
<span id="mqttStatusText">MQTT离线</span>
|
</div>
|
<div class="status-item">
|
<div class="status-indicator status-offline" id="videoStatus"></div>
|
<span id="videoStatusText">视频离线</span>
|
</div>
|
</div>
|
|
<!-- 右侧信息面板 -->
|
<div class="info-panel" id="infoPanel">
|
<button class="panel-toggle" onclick="togglePanel()">
|
<span id="toggleIcon">◀</span>
|
</button>
|
|
<div class="panel-header">
|
<div class="panel-title">� 无人机监控</div>
|
<div style="font-size: 14px; opacity: 0.8;">实时飞行数据</div>
|
</div>
|
|
<div class="panel-content">
|
<!-- 设备信息 -->
|
<div class="info-card">
|
<h3>📱 设备信息</h3>
|
<div class="info-item">
|
<div class="info-label">设备SN</div>
|
<div class="info-value" id="deviceSn">-</div>
|
</div>
|
</div>
|
|
<!-- 位置坐标 -->
|
<div class="info-card">
|
<h3>📍 位置坐标</h3>
|
<div class="info-grid">
|
<div class="info-item">
|
<div class="info-label">纬度</div>
|
<div class="info-value" id="latitude">-</div>
|
</div>
|
<div class="info-item">
|
<div class="info-label">经度</div>
|
<div class="info-value" id="longitude">-</div>
|
</div>
|
<div class="info-item">
|
<div class="info-label">海拔高度</div>
|
<div class="info-value" id="altitude">-</div>
|
</div>
|
<div class="info-item">
|
<div class="info-label">电池电量</div>
|
<div class="info-value" id="battery">-</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 姿态角度 -->
|
<div class="info-card">
|
<h3>🧭 飞行姿态</h3>
|
<div class="info-grid">
|
<div class="info-item">
|
<div class="info-label">航向角</div>
|
<div class="info-value" id="heading">-</div>
|
</div>
|
<div class="info-item">
|
<div class="info-label">俯仰角</div>
|
<div class="info-value" id="pitch">-</div>
|
</div>
|
<div class="info-item">
|
<div class="info-label">横滚角</div>
|
<div class="info-value" id="roll">-</div>
|
</div>
|
<div class="info-item">
|
<div class="info-label">飞行模式</div>
|
<div class="info-value" id="flightMode">-</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 云台姿态 -->
|
<div class="info-card">
|
<h3>📹 云台姿态</h3>
|
<div class="info-grid">
|
<div class="info-item">
|
<div class="info-label">云台俯仰</div>
|
<div class="info-value" id="gimbalPitch">-</div>
|
</div>
|
<div class="info-item">
|
<div class="info-label">云台横滚</div>
|
<div class="info-value" id="gimbalRoll">-</div>
|
</div>
|
<div class="info-item">
|
<div class="info-label">云台偏航</div>
|
<div class="info-value" id="gimbalYaw">-</div>
|
</div>
|
<div class="info-item">
|
<div class="info-label">相机状态</div>
|
<div class="info-value" id="cameraStatus">-</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 视频预览 -->
|
<div class="video-preview">
|
<div class="video-placeholder" id="videoPlaceholder">
|
<h3>🎥 视频预览</h3>
|
<p>等待视频流...</p>
|
<div class="loading"></div>
|
</div>
|
<img class="video-display" id="videoDisplay" style="display: none;" alt="无人机视频流">
|
</div>
|
|
<!-- 控制按钮 -->
|
<div class="controls-section">
|
<h3>🎮 控制操作</h3>
|
<button class="btn btn-primary" onclick="reconnectWebSocket()">
|
🔄 重新连接
|
</button>
|
<button class="btn btn-success" id="startStreamBtn" onclick="startStream()" disabled>
|
▶️ 开始直播
|
</button>
|
<button class="btn btn-danger" id="stopStreamBtn" onclick="stopStream()" disabled>
|
⏹️ 停止直播
|
</button>
|
<button class="btn btn-primary" onclick="focusOnDrone()">
|
🎯 定位无人机
|
</button>
|
<button class="btn btn-primary" onclick="window.clearTrajectory()">
|
🧹 清除轨迹
|
</button>
|
<button class="btn btn-primary" onclick="toggleTrajectoryDisplay()" id="toggleTrajectoryBtn">
|
👁️ 隐藏轨迹
|
</button>
|
<button class="btn btn-warning" onclick="toggleCameraFrustum()" id="toggleFrustumBtn">
|
📹 切换视锥
|
</button>
|
<button class="btn btn-info" onclick="testGimbalMovement()">
|
🎮 测试云台
|
</button>
|
</div>
|
</div>
|
</div>
|
|
<!-- 消息容器 -->
|
<div id="messageContainer"></div>
|
|
<script>
|
// 面板切换功能
|
function togglePanel() {
|
const panel = document.getElementById('infoPanel');
|
const icon = document.getElementById('toggleIcon');
|
|
panel.classList.toggle('collapsed');
|
icon.textContent = panel.classList.contains('collapsed') ? '▶' : '◀';
|
}
|
|
// 切换轨迹显示的按钮处理
|
function toggleTrajectoryDisplay() {
|
const isVisible = window.toggleTrajectory();
|
const btn = document.getElementById('toggleTrajectoryBtn');
|
if (btn) {
|
btn.textContent = isVisible ? '👁️ 隐藏轨迹' : '👁️ 显示轨迹';
|
}
|
}
|
|
// 初始化Cesium地图
|
let viewer;
|
let droneEntity;
|
let dronePosition = null;
|
let trajectoryEntity = null;
|
let trajectoryPoints = [];
|
let maxTrajectoryPoints = 100; // 最大轨迹点数量
|
|
// 全局云台数据存储 - 用于相机视锥投影
|
window.currentGimbalData = {
|
gimbalPitch: 0, // 云台俯仰角
|
gimbalRoll: 0, // 云台滚转角
|
gimbalYaw: 0 // 云台偏航角
|
};
|
|
function initCesium() {
|
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默认影像提供者
|
});
|
|
|
// 设置初始视角到西安
|
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
|
}
|
});
|
|
// 初始化飞行轨迹线
|
trajectoryEntity = viewer.entities.add({
|
name: '飞行轨迹',
|
polyline: {
|
positions: [],
|
width: 3,
|
material: new Cesium.PolylineGlowMaterialProperty({
|
glowPower: 0.2,
|
color: Cesium.Color.CYAN
|
}),
|
clampToGround: false,
|
show: true
|
}
|
});
|
|
// 将viewer暴露到全局作用域供app.js使用
|
window.viewer = viewer;
|
|
// 初始化相机视锥投影模块
|
if (typeof CameraFrustumProjection !== 'undefined') {
|
window.cameraFrustum = new CameraFrustumProjection(viewer);
|
|
// 设置DJI M30系列相机参数
|
window.cameraFrustum.setCameraParams({
|
fov: 84, // DJI M30典型视场角
|
aspectRatio: 16/9,
|
near: 1.0,
|
far: 300.0 // 300米探测距离
|
});
|
|
console.log('📹 相机视锥投影模块初始化完成');
|
}
|
|
console.log('🗺️ Cesium地图初始化完成');
|
}
|
|
// 更新无人机位置并记录轨迹
|
function updateDronePosition(latitude, longitude, altitude) {
|
if (!viewer || !latitude || !longitude) return;
|
|
dronePosition = { latitude, longitude, altitude };
|
|
const position = Cesium.Cartesian3.fromDegrees(longitude, latitude, altitude);
|
|
// 添加轨迹点
|
trajectoryPoints.push(position);
|
|
// 限制轨迹点数量,移除最老的点
|
if (trajectoryPoints.length > maxTrajectoryPoints) {
|
trajectoryPoints.shift(); // 移除第一个(最老的)点
|
}
|
|
// 更新轨迹线
|
if (trajectoryEntity) {
|
trajectoryEntity.polyline.positions = trajectoryPoints;
|
}
|
|
if (!droneEntity) {
|
// 创建无人机实体
|
droneEntity = viewer.entities.add({
|
name: '无人机',
|
position: position,
|
point: {
|
pixelSize: 15,
|
color: Cesium.Color.YELLOW,
|
outlineColor: Cesium.Color.BLACK,
|
outlineWidth: 2,
|
heightReference: Cesium.HeightReference.NONE
|
},
|
label: {
|
text: '🚁 无人机',
|
font: '14pt sans-serif',
|
pixelOffset: new Cesium.Cartesian2(0, -50),
|
fillColor: Cesium.Color.WHITE,
|
outlineColor: Cesium.Color.BLACK,
|
outlineWidth: 2,
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE
|
}
|
});
|
} else {
|
// 更新无人机位置
|
droneEntity.position = position;
|
}
|
|
// 更新相机视锥投影 (如果有云台数据)
|
if (window.cameraFrustum && window.currentGimbalData) {
|
const droneData = {
|
longitude: longitude,
|
latitude: latitude,
|
altitude: altitude
|
};
|
|
window.cameraFrustum.updateCameraFrustum(
|
droneData,
|
window.currentGimbalData,
|
'primary_drone'
|
);
|
}
|
|
console.log(`🛩️ 无人机位置更新: ${latitude.toFixed(6)}, ${longitude.toFixed(6)}, ${altitude.toFixed(1)}m (轨迹点: ${trajectoryPoints.length})`);
|
}
|
|
// 定位到无人机
|
function focusOnDrone() {
|
if (droneEntity && dronePosition) {
|
viewer.camera.flyTo({
|
destination: Cesium.Cartesian3.fromDegrees(
|
dronePosition.longitude,
|
dronePosition.latitude,
|
dronePosition.altitude + 1000
|
),
|
orientation: {
|
heading: Cesium.Math.toRadians(0),
|
pitch: Cesium.Math.toRadians(-45),
|
roll: 0.0
|
},
|
duration: 2.0
|
});
|
}
|
}
|
|
// 更新云台姿态数据 - 供相机视锥投影使用
|
function updateGimbalData(pitch, roll, yaw) {
|
if (window.currentGimbalData) {
|
window.currentGimbalData.gimbalPitch = pitch || 0;
|
window.currentGimbalData.gimbalRoll = roll || 0;
|
window.currentGimbalData.gimbalYaw = yaw || 0;
|
|
console.log(`📹 云台姿态更新: Pitch=${pitch?.toFixed(1)}°, Roll=${roll?.toFixed(1)}°, Yaw=${yaw?.toFixed(1)}°`);
|
|
// 如果无人机位置存在,立即更新视锥
|
if (window.cameraFrustum && dronePosition) {
|
window.cameraFrustum.updateCameraFrustum(
|
dronePosition,
|
window.currentGimbalData,
|
'primary_drone'
|
);
|
}
|
}
|
}
|
|
// 清除飞行轨迹
|
function clearTrajectory() {
|
trajectoryPoints = [];
|
if (trajectoryEntity) {
|
trajectoryEntity.polyline.positions = [];
|
}
|
console.log('🧹 飞行轨迹已清除');
|
}
|
|
// 切换相机视锥显示
|
function toggleCameraFrustum() {
|
if (!window.cameraFrustum) {
|
console.warn('相机视锥投影模块未初始化');
|
return;
|
}
|
|
const btn = document.getElementById('toggleFrustumBtn');
|
const frustumEntity = window.cameraFrustum.frustumEntities.get('primary_drone');
|
|
if (frustumEntity) {
|
// 切换显示状态
|
const isVisible = frustumEntity.show;
|
frustumEntity.show = !isVisible;
|
btn.textContent = isVisible ? '📹 显示视锥' : '📹 隐藏视锥';
|
|
console.log(`📹 相机视锥${isVisible ? '隐藏' : '显示'}`);
|
} else {
|
// 如果没有视锥,尝试创建
|
if (dronePosition && window.currentGimbalData) {
|
window.cameraFrustum.updateCameraFrustum(
|
dronePosition,
|
window.currentGimbalData,
|
'primary_drone'
|
);
|
btn.textContent = '📹 隐藏视锥';
|
}
|
}
|
}
|
|
// 测试云台运动 - 演示相机视锥变化
|
function testGimbalMovement() {
|
if (!window.cameraFrustum || !dronePosition) {
|
console.warn('相机视锥模块或无人机位置未准备好');
|
return;
|
}
|
|
console.log('🎮 开始云台运动测试...');
|
|
// 模拟云台运动序列
|
const movements = [
|
{ pitch: -30, roll: 0, yaw: 0 }, // 向下看
|
{ pitch: -45, roll: 0, yaw: 30 }, // 向右下
|
{ pitch: -30, roll: 0, yaw: 60 }, // 向右
|
{ pitch: -15, roll: 0, yaw: 30 }, // 向右上
|
{ pitch: -30, roll: 0, yaw: 0 }, // 回到向下
|
{ pitch: -30, roll: 0, yaw: -30 }, // 向左下
|
{ pitch: -30, roll: 0, yaw: -60 }, // 向左
|
{ pitch: -30, roll: 0, yaw: 0 } // 回到中心
|
];
|
|
let index = 0;
|
const interval = setInterval(() => {
|
if (index >= movements.length) {
|
clearInterval(interval);
|
console.log('🎮 云台运动测试完成');
|
return;
|
}
|
|
const movement = movements[index];
|
updateGimbalData(movement.pitch, movement.roll, movement.yaw);
|
|
// 更新UI显示
|
document.getElementById('gimbalPitch').textContent = `${movement.pitch.toFixed(1)}°`;
|
document.getElementById('gimbalRoll').textContent = `${movement.roll.toFixed(1)}°`;
|
document.getElementById('gimbalYaw').textContent = `${movement.yaw.toFixed(1)}°`;
|
|
index++;
|
}, 1000); // 每秒更新一次
|
}
|
|
// 切换轨迹显示
|
function toggleTrajectory() {
|
if (trajectoryEntity) {
|
const isVisible = trajectoryEntity.polyline.show;
|
trajectoryEntity.polyline.show = !isVisible;
|
console.log(`👁️ 轨迹显示: ${!isVisible ? '开启' : '关闭'}`);
|
return !isVisible;
|
}
|
return false;
|
}
|
|
// 暴露函数到全局作用域
|
window.updateDronePosition = updateDronePosition;
|
window.updateGimbalData = updateGimbalData;
|
window.focusOnDrone = focusOnDrone;
|
window.clearTrajectory = clearTrajectory;
|
window.toggleTrajectory = toggleTrajectory;
|
window.toggleCameraFrustum = toggleCameraFrustum;
|
window.testGimbalMovement = testGimbalMovement;
|
|
// 页面加载完成后初始化
|
document.addEventListener('DOMContentLoaded', function() {
|
initCesium();
|
});
|
</script>
|
|
<script src="app.js"></script>
|
</body>
|
</html>
|