QDAirPortTestSystemBackend/tools/map_websocket.html
2026-01-27 15:24:05 +08:00

740 lines
25 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>机场车辆监控</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-rotatedmarker/leaflet.rotatedMarker.js"></script>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
}
.container {
display: flex;
gap: 20px;
}
#map {
height: 800px;
width: 60%;
border: 1px solid #ccc;
}
#messages {
width: 40%;
height: 800px;
overflow-y: auto;
border: 1px solid #ccc;
padding: 10px;
font-family: monospace;
}
.controls {
margin-top: 10px;
}
.error {
color: red;
}
.success {
color: green;
}
.info {
color: blue;
}
.position {
color: #666;
}
.warning {
color: #f90;
}
.command {
color: #800080;
}
.vehicle-icon {
width: 20px;
height: 20px;
background-color: black;
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
border: 2px solid white;
}
.aircraft-icon {
width: 50px;
height: 50px;
background-color: rgba(128, 0, 128, 0.5);
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
border: 2px solid white;
position: relative;
}
.aircraft-icon::after {
content: '';
position: absolute;
width: 6px;
height: 6px;
background-color: black;
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.special-vehicle-icon {
width: 20px;
height: 20px;
background-color: orange;
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
border: 2px solid white;
}
.intersection-icon {
width: 30px;
height: 30px;
background-color: #666;
clip-path: polygon(40% 0%, 60% 0%, 60% 40%, 100% 40%, 100% 60%, 60% 60%, 60% 100%, 40% 100%, 40% 60%, 0% 60%, 0% 40%, 40% 40%);
border: 2px solid white;
}
.traffic-light {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid white;
z-index: 1000;
}
.traffic-light-red {
background-color: red;
}
.traffic-light-green {
background-color: green;
}
.traffic-light-yellow {
background-color: yellow;
}
.countdown-label {
background: #000;
border: 1px solid #666;
border-radius: 2px;
font-family: "Digital-7", "DSEG7 Classic", Monaco, monospace;
font-weight: normal;
font-size: 12px;
text-align: center;
white-space: nowrap;
padding: 1px 2px;
line-height: 14px;
min-width: 28px;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
height: 14px;
}
.countdown-red {
color: #ff3333;
}
.countdown-green {
color: #33ff33;
}
.countdown-yellow {
color: #ffff33;
}
.distance-label {
background: none;
border: none;
color: #666;
font-size: 12px;
text-align: center;
white-space: nowrap;
}
.command-text {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 10px;
font-weight: bold;
pointer-events: none;
z-index: 1000;
}
.safety-border {
width: 100%;
height: 100%;
border: 2px solid;
background: none;
}
.emergency-border {
border-color: rgba(255, 0, 0, 0.8);
}
.core-border {
border-color: rgba(255, 165, 0, 0.6);
}
.warning-border {
border-color: rgba(255, 255, 0, 0.4);
}
</style>
</head>
<body>
<h2>机场车辆监控系统</h2>
<div class="container">
<div id="map"></div>
<div id="messages"></div>
</div>
<div class="controls">
<button onclick="connect()">连接</button>
<button onclick="disconnect()">断开</button>
<button onclick="clearMessages()">清空日志</button>
</div>
<script>
let ws = null;
const messagesDiv = document.getElementById('messages');
// 初始化地图
const map = L.map('map').setView([36.35305878, 120.08558121], 17);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// 定义路口坐标
const T1_INTERSECTION = {
latitude: 36.35496367,
longitude: 120.0868853
};
const T2_INTERSECTION = {
latitude: 36.35448347,
longitude: 120.08502054
};
const T3_INTERSECTION = {
latitude: 36.35406879,
longitude: 120.08341044
};
const T4_INTERSECTION = {
latitude: 36.35305878,
longitude: 120.08558121
};
const T6_INTERSECTION = {
latitude: 36.35074527,
longitude: 120.08649105
};
const T7_INTERSECTION = {
latitude: 36.35052372,
longitude: 120.08562915
};
const T8_INTERSECTION = {
latitude: 36.35004529,
longitude: 120.08676664
};
const T10_INTERSECTION = {
latitude: 36.34917893,
longitude: 120.08710569
};
const T11_INTERSECTION = {
latitude: 36.3509885,
longitude: 120.0873865
};
// 存储所有标记
const markers = new Map();
const intersectionMarkers = new Map();
const intersections = new Map();
// 创建自定义图标
function createIcon(className, command = '') {
let size;
if (className.includes('aircraft')) {
size = [50, 50]; // 50米正方形
} else if (className.includes('vehicle')) {
size = [20, 20]; // 10米正方形
} else if (className.includes('traffic-light')) {
size = [12, 12]; // 10像素的红绿灯
} else {
size = [20, 20]; // 其他图标保持原样
}
// 如果有指令,创建带指令的图标
if (command && className === 'vehicle-icon') {
const html = `
<div style="width:${size[0]}px;height:${size[1]}px;position:relative;">
<div class="${className}"></div>
<div class="command-text">${command}</div>
</div>`;
return L.divIcon({
html: html,
className: '',
iconSize: size,
iconAnchor: [size[0] / 2, size[1] / 2]
});
}
// 没有指令时,创建普通图标
return L.divIcon({
className: className,
iconSize: size,
iconAnchor: [size[0] / 2, size[1] / 2]
});
}
function log(message, type = 'info') {
const div = document.createElement('div');
div.className = type;
// 将换行符转换为 HTML 换行
div.innerHTML = `${new Date().toLocaleTimeString()} - ${message.replace(/\n/g, '<br>')}`;
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function clearMessages() {
messagesDiv.innerHTML = '';
}
function updatePosition(data) {
const id = data.object_id;
const position = [data.position.latitude, data.position.longitude];
let iconClass;
// 根据ID前缀确定图标类型
if (data.object_type === 'aircraft') {
iconClass = 'aircraft-icon';
// 创建或更新安全边框
const borders = [
{ size: 250, class: 'warning-border' }, // 外围预警区
{ size: 150, class: 'core-border' }, // 核心安全区
{ size: 100, class: 'emergency-border' } // 紧急制动区
];
borders.forEach(border => {
const borderId = id + '_' + border.class;
let borderMarker = markers.get(borderId);
if (!borderMarker) {
// 创建边框标记
borderMarker = L.marker(position, {
icon: L.divIcon({
className: 'safety-border ' + border.class,
iconSize: [border.size, border.size],
iconAnchor: [border.size / 2, border.size / 2]
}),
rotationAngle: data.heading || 0,
rotationOrigin: 'center center'
}).addTo(map);
markers.set(borderId, borderMarker);
} else {
// 更新边框位置和方向
borderMarker.setLatLng(position);
if (data.heading !== undefined) {
borderMarker.setRotationAngle(data.heading);
}
}
});
} else if (id.startsWith('TQ')) {
iconClass = 'special-vehicle-icon';
} else {
iconClass = 'vehicle-icon';
}
let marker = markers.get(id);
if (!marker) {
// 创建新标记
marker = L.marker(position, {
icon: createIcon(iconClass),
rotationAngle: data.heading || 0,
rotationOrigin: 'center center'
}).addTo(map);
marker.bindTooltip(id);
markers.set(id, marker);
} else {
// 更新现有标记位置和航向
marker.setLatLng(position);
if (data.heading !== undefined) {
marker.setRotationAngle(data.heading);
}
}
}
// 添加红绿灯状态和倒计时变量
// let lastTrafficLightState = null;
// let countdownInterval = null;
// let countdown = 10;
// New function to update intersection traffic lights
function updateIntersectionTrafficLights(data) {
const intersectionId = data.intersection_id;
const position = [data.position.latitude, data.position.longitude];
const nsStatus = data.ns_status; // 0: red, 1: green, 2: yellow
const ewStatus = data.ew_status; // 0: red, 1: green, 2: yellow
const nsColor = nsStatus === 0 ? 'red' : nsStatus === 1 ? 'green' : 'yellow';
const ewColor = ewStatus === 0 ? 'red' : ewStatus === 1 ? 'green' : 'yellow';
// Calculate offset positions for NS and EW lights
const nsPositionOffset = [position[0] + 0.00003, position[1]]; // Slightly north
const ewPositionOffset = [position[0], position[1] + 0.00003]; // Slightly east
let currentMarkers = intersectionMarkers.get(intersectionId);
if (!currentMarkers) {
// Create new marker group
currentMarkers = {};
currentMarkers.nsMarker = L.marker(nsPositionOffset, {
icon: createIcon(`traffic-light traffic-light-${nsColor}`),
pane: 'markerPane' // Ensure lights are above roads
}).addTo(map).bindTooltip(`Intersection ${intersectionId} (NS)`);
currentMarkers.ewMarker = L.marker(ewPositionOffset, {
icon: createIcon(`traffic-light traffic-light-${ewColor}`),
pane: 'markerPane'
}).addTo(map).bindTooltip(`Intersection ${intersectionId} (EW)`);
intersectionMarkers.set(intersectionId, currentMarkers);
} else {
// Update existing markers
currentMarkers.nsMarker.setLatLng(nsPositionOffset);
currentMarkers.nsMarker.setIcon(createIcon(`traffic-light traffic-light-${nsColor}`));
currentMarkers.ewMarker.setLatLng(ewPositionOffset);
currentMarkers.ewMarker.setIcon(createIcon(`traffic-light traffic-light-${ewColor}`));
}
}
function updateVehicleCommand(vehicleId, commandType) {
console.log('更新车辆指令:', vehicleId, commandType);
// 只处理无人车
if (!vehicleId.startsWith('QN')) {
return;
}
// 如果是 SIGNAL 指令,不更新显示
if (commandType === 'SIGNAL') {
console.log('忽略 SIGNAL 指令');
return;
}
// 获取指令字母
let commandText = '';
switch (commandType) {
case 'ALERT':
commandText = 'A';
break;
case 'WARNING':
commandText = 'W';
break;
case 'RESUME':
commandText = 'R';
break;
default:
commandText = '';
}
console.log('指令文本:', commandText);
// 更新图标
const marker = markers.get(vehicleId);
if (marker && commandText) {
console.log('设置新标:', vehicleId, commandText);
marker.setIcon(createIcon('vehicle-icon', commandText));
} else if (marker) {
marker.setIcon(createIcon('vehicle-icon'));
}
}
function connect() {
if (ws) {
log('已经连接,请先断开', 'error');
return;
}
try {
ws = new WebSocket('ws://localhost:8010');
ws.onopen = () => {
log('连接成功', 'success');
};
ws.onclose = () => {
log('连接关闭', 'info');
ws = null;
};
ws.onerror = (error) => {
log('发生错误: ' + error, 'error');
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// 忽略心跳响应
if (data.type === 'heartbeat') {
return;
}
let type = 'info'; // 默认类型
let message = '';
switch (data.type) {
case 'position_update':
type = 'position';
updatePosition(data);
message = `位置更新: ${data.object_id} (${data.object_type})\n` +
`位置: (${data.position.longitude.toFixed(6)}, ${data.position.latitude.toFixed(6)})\n` +
`航向: ${data.heading !== undefined ? data.heading.toFixed(2) + '°' : 'N/A'}\n` +
`速度: ${data.speed !== undefined ? data.speed.toFixed(2) + ' m/s' : 'N/A'}`;
break;
case 'intersection_traffic_light_status':
type = 'info';
updateIntersectionTrafficLights(data);
message = `路口交通灯状态更新:\n` +
`路口 ID: ${data.intersection_id}\n` +
`南北状态: ${data.ns_status === 0 ? '红' : data.ns_status === 1 ? '绿' : '黄'}\n` +
`东西状态: ${data.ew_status === 0 ? '红' : data.ew_status === 1 ? '绿' : '黄'}`;
break;
case 'collision_warning':
type = 'warning';
message = '收到碰撞预警:\n' + JSON.stringify(data, null, 2);
break;
case 'vehicle_command':
type = 'command';
console.log('收到指令消息:', data); // 调试日志
updateVehicleCommand(data.vehicleId, data.commandType);
// 为控制指令添加中文描述
const commandTypes = {
'SIGNAL': '信号灯',
'ALERT': '告警',
'WARNING': '预警',
'RESUME': '恢复',
'PARKING': '安全停靠'
};
const reasons = {
'TRAFFIC_LIGHT': '红绿灯控制',
'AIRCRAFT_CROSSING': '航空器交叉',
'SPECIAL_VEHICLE': '特勤车辆',
'AIRCRAFT_PUSH': '航空器推出',
'RESUME_TRAFFIC': '恢复通行',
'PARKING_SIDE': '安全停靠'
};
message = `收到车辆控制指令:\n车辆: ${data.vehicleId}\n` +
`指令类型: ${commandTypes[data.commandType] || data.commandType}\n` +
`原因: ${reasons[data.reason] || data.reason}\n` +
(data.targetLatitude !== undefined ? `目标位置: (${data.targetLatitude}, ${data.targetLongitude})\n` : '') +
(data.signalState ? `信号灯状态: ${data.signalState}\n` : '') +
(data.intersectionId ? `路口ID: ${data.intersectionId}\n` : '') +
`时间戳: ${new Date(data.timestamp / 1000000).toLocaleString()}`;
break;
default:
message = '收到未知类型消息:\n' + JSON.stringify(data, null, 2);
}
log(message, type);
} catch (e) {
log('消息解析错误: ' + e.message, 'error');
}
};
} catch (error) {
log('连接失败: ' + error, 'error');
}
}
function disconnect() {
if (!ws) {
log('未连接', 'error');
return;
}
// 清除倒计时
// if (countdownInterval) {
// clearInterval(countdownInterval);
// countdownInterval = null;
// }
// lastTrafficLightState = null;
// countdown = 10;
// 清除倒计时标记
// trafficLights.forEach(light => {
// if (light.countdownMarker) {
// map.removeLayer(light.countdownMarker);
// light.countdownMarker = null;
// }
// });
ws.close();
ws = null;
// 清除所有车辆和安全边框标记
markers.forEach((marker, key) => {
map.removeLayer(marker);
});
markers.clear();
// 清除旧的红绿灯标记
// trafficLights.forEach(light => map.removeLayer(light));
// trafficLights.clear();
// 清除路口红绿灯标记
intersectionMarkers.forEach((markers, id) => {
if (markers.nsMarker) map.removeLayer(markers.nsMarker);
if (markers.ewMarker) map.removeLayer(markers.ewMarker);
});
intersectionMarkers.clear();
}
// 添加道路刻度标记函数
function addRoadMarks(startPoint, endPoint) {
// 计算点之间的距离(米)
const lat1 = startPoint[0];
const lon1 = startPoint[1];
const lat2 = endPoint[0];
const lon2 = endPoint[1];
// 计算道路角度(考虑经纬度投影)
const latMid = (lat1 + lat2) / 2; // 使用中点纬度来算经度缩放
const lonScale = Math.cos(latMid * Math.PI / 180); // 经度缩放因子
const dx = (lon2 - lon1) * lonScale;
const dy = lat2 - lat1;
const angle = Math.atan2(dy, dx);
// 计算垂直于道路的方向(只在右侧显示刻度)
const perpAngle = angle + Math.PI / 2;
const markLength = 0.00005; // 保持您设置的较短刻度线长度
const offset = 0.00004; // 向右偏移一点,避免与道路重叠
// 计算总距离
const dist = Math.sqrt(dx * dx + dy * dy);
// 每50米一个刻度
const step = 0.0005; // 约50米
const steps = Math.floor(dist / step);
for (let i = 0; i <= steps; i++) {
// 计算刻度位置
const ratio = i / steps;
// 在经纬度坐标系中正插值
const pos = [
lat1 + dy * ratio,
lon1 + (dx / lonScale) * ratio
];
// 计算垂直偏移(考虑经纬度投影)
const offsetPos = [
pos[0] + Math.sin(perpAngle) * offset,
pos[1] + Math.cos(perpAngle) * offset / lonScale
];
// 计算刻度线终点(考虑经纬度投影)
const markEnd = [
offsetPos[0] + Math.sin(perpAngle) * markLength,
offsetPos[1] + Math.cos(perpAngle) * markLength / lonScale
];
// 添加刻度线
L.polyline([offsetPos, markEnd], {
color: '#666',
weight: 1.5
}).addTo(map);
// 添加距离标签
const distance = Math.round(i * 50);
if (distance > 0) {
const label = L.divIcon({
className: 'distance-label',
html: distance + 'm',
iconSize: [40, 20],
iconAnchor: [-5, 10]
});
L.marker(markEnd, {
icon: label,
interactive: false
}).addTo(map);
}
}
}
// 添加道路
// T2 到 T10主路
const mainRoadEW = L.polyline([
[T2_INTERSECTION.latitude, T2_INTERSECTION.longitude],
[T10_INTERSECTION.latitude, T10_INTERSECTION.longitude]
], {
color: '#999',
weight: 8
}).addTo(map);
// T1 到 T3道路
const westRoadNS = L.polyline([
[T1_INTERSECTION.latitude, T1_INTERSECTION.longitude],
[T3_INTERSECTION.latitude, T3_INTERSECTION.longitude]
], {
color: '#999',
weight: 8
}).addTo(map);
// T7 到 T11道路
const eastRoadNS = L.polyline([
[T7_INTERSECTION.latitude, T7_INTERSECTION.longitude],
[T11_INTERSECTION.latitude, T11_INTERSECTION.longitude]
], {
color: '#999',
weight: 8
}).addTo(map);
// 添加刻度标记
// T1 到 T3路刻度
addRoadMarks(
[T1_INTERSECTION.latitude, T1_INTERSECTION.longitude],
[T3_INTERSECTION.latitude, T3_INTERSECTION.longitude]
);
// T7 到 T11路刻度
addRoadMarks(
[T7_INTERSECTION.latitude, T7_INTERSECTION.longitude],
[T11_INTERSECTION.latitude, T11_INTERSECTION.longitude]
);
// T2 到 T10路刻度
addRoadMarks(
[T2_INTERSECTION.latitude, T2_INTERSECTION.longitude],
[T10_INTERSECTION.latitude, T10_INTERSECTION.longitude]
);
</script>
</body>
</html>