655 lines
23 KiB
HTML
655 lines
23 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>
|
||
<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;
|
||
}
|
||
.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;
|
||
}
|
||
.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;
|
||
}
|
||
</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 trafficLights = 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;
|
||
div.textContent = `${new Date().toLocaleTimeString()} - ${message}`;
|
||
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'; // 六形
|
||
} 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)
|
||
}).addTo(map);
|
||
marker.bindTooltip(id); // 添加标签显示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; // 10秒倒计时
|
||
|
||
function updateTrafficLight(data) {
|
||
const id = data.id;
|
||
const position = [data.position.latitude, data.position.longitude];
|
||
const state = data.status === 0 ? 'red' : 'green';
|
||
|
||
// 检查是否是西路口(TL001)的红绿灯状态变化
|
||
if (id === 'TL001' && lastTrafficLightState !== state) {
|
||
lastTrafficLightState = state;
|
||
// 重置倒计时
|
||
countdown = 10;
|
||
// 清除现有的倒计时
|
||
if (countdownInterval) {
|
||
clearInterval(countdownInterval);
|
||
}
|
||
// 启动新的倒计时
|
||
countdownInterval = setInterval(() => {
|
||
countdown = Math.max(0, countdown - 1);
|
||
// 更新红绿灯标签显示
|
||
const light = trafficLights.get(id);
|
||
if (light) {
|
||
// 格式化倒计时为 0:SS 格式
|
||
const countdownStr = `0:${countdown.toString().padStart(2, '0')}`;
|
||
const label = L.divIcon({
|
||
className: `countdown-label countdown-${state}`,
|
||
html: countdownStr,
|
||
iconSize: [32, 16],
|
||
iconAnchor: [16, 30]
|
||
});
|
||
// 更新或创建倒计时标签
|
||
if (!light.countdownMarker) {
|
||
light.countdownMarker = L.marker(position, {
|
||
icon: label,
|
||
interactive: false
|
||
}).addTo(map);
|
||
} else {
|
||
light.countdownMarker.setIcon(label);
|
||
}
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
let light = trafficLights.get(id);
|
||
if (!light) {
|
||
light = L.marker(position, {
|
||
icon: createIcon(`traffic-light traffic-light-${state}`)
|
||
}).addTo(map);
|
||
// 为西路口添加倒计时显示
|
||
if (id === 'TL001') {
|
||
const countdownStr = `0:${countdown.toString().padStart(2, '0')}`;
|
||
const label = L.divIcon({
|
||
className: `countdown-label countdown-${state}`,
|
||
html: countdownStr,
|
||
iconSize: [32, 16],
|
||
iconAnchor: [16, 30]
|
||
});
|
||
light.countdownMarker = L.marker(position, {
|
||
icon: label,
|
||
interactive: false
|
||
}).addTo(map);
|
||
}
|
||
trafficLights.set(id, light);
|
||
} else {
|
||
light.setIcon(createIcon(`traffic-light traffic-light-${state}`));
|
||
// 更新西路口的倒计时显示
|
||
if (id === 'TL001' && light.countdownMarker) {
|
||
const countdownStr = `0:${countdown.toString().padStart(2, '0')}`;
|
||
const label = L.divIcon({
|
||
className: `countdown-label countdown-${state}`,
|
||
html: countdownStr,
|
||
iconSize: [32, 16],
|
||
iconAnchor: [16, 30]
|
||
});
|
||
light.countdownMarker.setIcon(label);
|
||
}
|
||
}
|
||
}
|
||
|
||
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);
|
||
let type = 'info'; // 默认类型
|
||
let message = '';
|
||
|
||
switch (data.type) {
|
||
case 'position_update':
|
||
type = 'position';
|
||
updatePosition(data);
|
||
break;
|
||
case 'traffic_light_status':
|
||
type = 'info';
|
||
updateTrafficLight(data);
|
||
message = `红绿灯状态更新:\n信号灯: ${data.id}\n状态: ${data.status === 0 ? '红灯' : '绿灯'}`;
|
||
break;
|
||
case 'collision_warning':
|
||
type = 'warning';
|
||
break;
|
||
case 'vehicle_command':
|
||
type = 'command';
|
||
console.log('收到指令消息:', data); // 调试日志
|
||
updateVehicleCommand(data.vehicleId, data.commandType);
|
||
// 为控制指令添加中文描述
|
||
const commandTypes = {
|
||
'SIGNAL': '信号灯指令',
|
||
'ALERT': '告警指令',
|
||
'WARNING': '预警指令',
|
||
'RESUME': '恢复指令'
|
||
};
|
||
const reasons = {
|
||
'TRAFFIC_LIGHT': '红绿灯控制',
|
||
'AIRCRAFT_CROSSING': '航空器交叉',
|
||
'SPECIAL_VEHICLE': '特勤车辆',
|
||
'AIRCRAFT_PUSH': '航空器推出',
|
||
'RESUME_TRAFFIC': '恢复通行'
|
||
};
|
||
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;
|
||
}
|
||
|
||
// 如果没有特定消息,使用格式化的数据
|
||
if (!message) {
|
||
message = '收到消息:\n' + JSON.stringify(data, null, 2);
|
||
}
|
||
log(message, type);
|
||
} catch (e) {
|
||
log('收到消息: ' + event.data, 'info');
|
||
}
|
||
};
|
||
} 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 => map.removeLayer(marker));
|
||
markers.clear();
|
||
trafficLights.forEach(light => map.removeLayer(light));
|
||
trafficLights.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> |