Add path conflict display toggle and clear aircraft alerts
This commit is contained in:
parent
3be4f2a90b
commit
cd95a16904
11
.codex/environments/environment.toml
Normal file
11
.codex/environments/environment.toml
Normal file
@ -0,0 +1,11 @@
|
||||
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
|
||||
version = 1
|
||||
name = "airport-qingdao-vue3"
|
||||
|
||||
[setup]
|
||||
script = ""
|
||||
|
||||
[[actions]]
|
||||
name = "运行"
|
||||
icon = "run"
|
||||
command = "npm run build:prod"
|
||||
Binary file not shown.
@ -21,6 +21,10 @@
|
||||
<div class="settings-item" @click="openWarningDistance">修改预警距离</div>
|
||||
<div class="settings-item" @click="openRouteModify">查询路由</div>
|
||||
<div class="settings-item" @click="drawTestRoute">绘制测试路由</div>
|
||||
<div class="settings-item settings-switch-item" @click.stop>
|
||||
<span>路径冲突显示</span>
|
||||
<el-switch v-model="pathConflictDisplayEnabled" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VehicleRunningEditDialog :visible="showVehicleEditDialog" @close="showVehicleEditDialog = false" />
|
||||
@ -58,8 +62,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch } from "vue";
|
||||
import { computed, ref, onMounted, onUnmounted, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import useSettingsStore from '@/store/modules/settings';
|
||||
import LayerSwitcher from "./LayerSwitcher.vue";
|
||||
import VehicleRunningEditDialog from "./VehicleRunningEditDialog.vue";
|
||||
import WarningDistanceDialog from "./WarningDistanceDialog.vue";
|
||||
@ -102,6 +107,19 @@ const showSettingsPanel = ref(false);
|
||||
const showVehicleEditDialog = ref(false);
|
||||
const showWarningDistanceDialog = ref(false);
|
||||
const showRouteModifyDialog = ref(false);
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const pathConflictDisplayEnabled = computed({
|
||||
get: () => settingsStore.pathConflictDisplayEnabled !== false,
|
||||
set: (val) => {
|
||||
settingsStore.changeSetting({ key: 'pathConflictDisplayEnabled', value: val });
|
||||
const layoutSetting = JSON.parse(localStorage.getItem('layout-setting')) || {};
|
||||
localStorage.setItem('layout-setting', JSON.stringify({
|
||||
...layoutSetting,
|
||||
pathConflictDisplayEnabled: val
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// 地图旋转角度 (弧度转换为度)
|
||||
const rotation = ref(0);
|
||||
@ -466,6 +484,12 @@ defineExpose({
|
||||
background: rgba(0, 150, 255, 0.12);
|
||||
}
|
||||
|
||||
.settings-switch-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.settings-item.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
||||
@ -65,8 +65,8 @@ function createNewLabel(vehicleId, coordinates, speed, alertInfo) {
|
||||
const overlay = new Overlay({
|
||||
element: labelElement,
|
||||
position: coordinates,
|
||||
offset: [0, -30], // 偏移量,使标签位于图标上方
|
||||
positioning: 'bottom-center',
|
||||
offset: [34, -34], // 位于图标右上方,并留出间距
|
||||
positioning: 'bottom-left',
|
||||
stopEvent: false,
|
||||
insertFirst: true, // 确保在DOM中优先插入
|
||||
autoPan: false, // 禁用自动平移
|
||||
@ -353,7 +353,7 @@ defineExpose({
|
||||
/* 标签容器样式 */
|
||||
.vehicle-label {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -100%);
|
||||
transform: none;
|
||||
z-index: 1000;
|
||||
pointer-events: auto;
|
||||
}
|
||||
@ -364,7 +364,8 @@ defineExpose({
|
||||
color: #000;
|
||||
padding: 10px;
|
||||
padding-bottom: 0px;
|
||||
min-width: 170px;
|
||||
min-width: 280px;
|
||||
max-width: 380px;
|
||||
box-shadow: 0 10px 15px rgba(2, 2, 2, 0.25);
|
||||
border: 1px solid #40506a;
|
||||
}
|
||||
@ -372,6 +373,8 @@ defineExpose({
|
||||
/* 简化标签样式 */
|
||||
.simple-label {
|
||||
padding: 6px 10px;
|
||||
min-width: 150px;
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
/* 标签头部样式 */
|
||||
@ -405,11 +408,15 @@ defineExpose({
|
||||
.tab-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 8px;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 6px;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
border: none;
|
||||
width: 24px;
|
||||
@ -488,23 +495,37 @@ defineExpose({
|
||||
}
|
||||
|
||||
.tab-pane.active {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 告警消息样式 */
|
||||
.alert-message {
|
||||
font-size: 12px;
|
||||
font-weight: Medium;
|
||||
line-height: 1.4;
|
||||
padding: 5px;
|
||||
line-height: 1.2;
|
||||
padding: 2px 0;
|
||||
border-radius: 2px;
|
||||
background-color: transparent;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
/* 具体告警提示 */
|
||||
.alert{
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 5px;
|
||||
margin: 0 8px 0 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.alert img {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* 超速信息样式 */
|
||||
|
||||
@ -44,10 +44,38 @@
|
||||
ref="styleManager"
|
||||
:vehicles="vehicles"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="pathConflictDisplayEnabled && currentPathConflict"
|
||||
class="path-conflict-card"
|
||||
:class="`level-${currentPathConflict.level}`"
|
||||
>
|
||||
<div class="path-conflict-header">
|
||||
<span>{{ currentPathConflict.title }}</span>
|
||||
<span class="path-conflict-level">{{ getConflictLevelText(currentPathConflict.level) }}</span>
|
||||
</div>
|
||||
<div class="path-conflict-body">
|
||||
<div>{{ currentPathConflict.aircraftName || '未知航空器' }} / {{ currentPathConflict.vehicleName || '未知车辆' }}</div>
|
||||
<div v-if="currentPathConflict.directionLockText" class="path-conflict-muted">
|
||||
方向锁定:{{ currentPathConflict.directionLockText }}
|
||||
</div>
|
||||
<div>飞机距离:{{ formatConflictDistance(currentPathConflict.aircraftDistance) }}</div>
|
||||
<div>车辆距离:{{ formatConflictDistance(currentPathConflict.vehicleDistance) }}</div>
|
||||
<div v-if="hasConflictForwardDistance(currentPathConflict)" class="path-conflict-debug">
|
||||
前向距离:对象1 {{ formatConflictDistance(currentPathConflict.object1ForwardDistance) }} / 对象2 {{ formatConflictDistance(currentPathConflict.object2ForwardDistance) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="currentPathConflict.directionLockFailed && currentPathConflict.directionLockReason"
|
||||
class="path-conflict-reason"
|
||||
>
|
||||
原因:{{ currentPathConflict.directionLockReason }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, onActivated, onDeactivated, defineProps, defineEmits } from 'vue';
|
||||
import { ref, computed, onMounted, onUnmounted, watch, onActivated, onDeactivated, defineProps, defineEmits } from 'vue';
|
||||
import { Vector as VectorSource } from 'ol/source';
|
||||
import { Vector as VectorLayer } from 'ol/layer';
|
||||
import { Style, Icon, Stroke, Fill, Circle, Text } from 'ol/style';
|
||||
@ -61,6 +89,8 @@ import { register } from 'ol/proj/proj4.js';
|
||||
import { get as getProj } from 'ol/proj';
|
||||
import WebSocketService, { createWebSocket, resetWebSocketInstance } from '../../../utils/websocket.js';
|
||||
import useVehicleDisplayStore from '@/store/modules/vehicleDisplay';
|
||||
import useSettingsStore from '@/store/modules/settings';
|
||||
import { getPathConflictDisplayEnabled, isNewPathConflictPayload, normalizePathConflictMessage } from '@/utils/pathConflictDisplay.mjs';
|
||||
|
||||
// 导入子组件
|
||||
import VehicleAnimationSystem from './VehicleAnimationSystem.vue';
|
||||
@ -92,6 +122,9 @@ aircraftRouteIconImg.src = aircraftRouteIcon;
|
||||
// const aircraftOutIconImg = new Image();
|
||||
// aircraftOutIconImg.src = aircraftOutIcon;
|
||||
|
||||
const DEFAULT_VEHICLE_ICON_SCALE = 0.75;
|
||||
const AIRCRAFT_ICON_SCALE = DEFAULT_VEHICLE_ICON_SCALE * 1.3;
|
||||
|
||||
// 为SockJS提供polyfill
|
||||
if (typeof window !== 'undefined' && !window.global) {
|
||||
window.global = window;
|
||||
@ -126,6 +159,8 @@ const labelSystem = ref(null);
|
||||
const styleManager = ref(null);
|
||||
const alarmNotification = ref(null); // 告警通知组件引用
|
||||
const vehicleDisplayStore = useVehicleDisplayStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const pathConflictDisplayEnabled = computed(() => getPathConflictDisplayEnabled(settingsStore));
|
||||
|
||||
// 气象监测站数据
|
||||
const weatherStationVisible = ref(true);
|
||||
@ -161,6 +196,45 @@ let reconnectTimer = null;
|
||||
const vehicles = ref({});
|
||||
let vehicleLayer = null;
|
||||
let vehicleSource = null;
|
||||
let pathConflictLayer = null;
|
||||
let pathConflictSource = null;
|
||||
const pathConflicts = ref({});
|
||||
|
||||
const conflictLevelPriority = {
|
||||
EMERGENCY: 4,
|
||||
CRITICAL: 3,
|
||||
WARNING: 2,
|
||||
STATUS: 1
|
||||
};
|
||||
|
||||
const currentPathConflict = computed(() => {
|
||||
const list = Object.values(pathConflicts.value || {});
|
||||
if (!list.length) return null;
|
||||
return list.sort((a, b) => {
|
||||
const levelDiff = (conflictLevelPriority[b.level] || 0) - (conflictLevelPriority[a.level] || 0);
|
||||
if (levelDiff) return levelDiff;
|
||||
return (b.updatedAt || 0) - (a.updatedAt || 0);
|
||||
})[0];
|
||||
});
|
||||
|
||||
function getConflictLevelText(level) {
|
||||
if (level === 'EMERGENCY') return '紧急';
|
||||
if (level === 'CRITICAL') return '告警';
|
||||
if (level === 'WARNING') return '预警';
|
||||
return '计算';
|
||||
}
|
||||
|
||||
function formatConflictDistance(value) {
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num)) return '--';
|
||||
return `${Math.round(num)}米`;
|
||||
}
|
||||
|
||||
function hasConflictForwardDistance(conflict) {
|
||||
if (!conflict) return false;
|
||||
return Number.isFinite(Number(conflict.object1ForwardDistance)) ||
|
||||
Number.isFinite(Number(conflict.object2ForwardDistance));
|
||||
}
|
||||
|
||||
function refreshVehicleVisibility() {
|
||||
const all = vehicles.value || {};
|
||||
@ -263,8 +337,11 @@ function clearConflictStatus(vehicleId) {
|
||||
const v = vehicles.value?.[vehicleId];
|
||||
if (!v) return;
|
||||
|
||||
clearStatusTimer(`conflict:${vehicleId}`);
|
||||
v.warning = false;
|
||||
v.alarm = false;
|
||||
v.lastConflictLevel = undefined;
|
||||
v.lastConflictAt = undefined;
|
||||
|
||||
if (v.position && labelSystem.value) {
|
||||
labelSystem.value.removeVehicleLabel(vehicleId);
|
||||
@ -300,6 +377,11 @@ function markConflictStatus(vehicleId, level, message, ttlMs) {
|
||||
}
|
||||
|
||||
const timerKey = `conflict:${vehicleId}`;
|
||||
if (!(Number(ttlMs) > 0)) {
|
||||
clearStatusTimer(timerKey);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatusTimer(
|
||||
timerKey,
|
||||
() => {
|
||||
@ -429,22 +511,35 @@ function formatDateTime(date) {
|
||||
function handleAlertShow({ message, type, duration = 5000 }) {
|
||||
if (alertTimer) {
|
||||
clearTimeout(alertTimer);
|
||||
alertTimer = null;
|
||||
}
|
||||
|
||||
alertMessage.value = message;
|
||||
alertType.value = type;
|
||||
|
||||
if (Number(duration) > 0) {
|
||||
alertTimer = setTimeout(() => {
|
||||
alertMessage.value = '';
|
||||
alertType.value = '';
|
||||
alertTimer = null;
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示告警/预警消息
|
||||
function showAlert(message, type, duration = 5000) {
|
||||
handleAlertShow({ message, type, duration });
|
||||
}
|
||||
|
||||
function clearAlertMessage() {
|
||||
if (alertTimer) {
|
||||
clearTimeout(alertTimer);
|
||||
alertTimer = null;
|
||||
}
|
||||
alertMessage.value = '';
|
||||
alertType.value = '';
|
||||
}
|
||||
|
||||
// 创建车辆图层
|
||||
function createVehicleLayer() {
|
||||
if (!props.map) return;
|
||||
@ -485,6 +580,37 @@ function createAircraftRouteLayer() {
|
||||
props.map.addLayer(aircraftRouteLayer);
|
||||
}
|
||||
|
||||
function createPathConflictLayer() {
|
||||
if (!props.map) return;
|
||||
if (pathConflictLayer) return;
|
||||
|
||||
pathConflictSource = new VectorSource();
|
||||
pathConflictLayer = new VectorLayer({
|
||||
source: pathConflictSource,
|
||||
zIndex: 2100,
|
||||
});
|
||||
props.map.addLayer(pathConflictLayer);
|
||||
}
|
||||
|
||||
function getPathConflictStyle(conflict) {
|
||||
const color = '#3b82f6';
|
||||
|
||||
return new Style({
|
||||
image: new Circle({
|
||||
radius: conflict.kind === 'alert' ? 10 : 8,
|
||||
fill: new Fill({ color }),
|
||||
stroke: new Stroke({ color: '#ffffff', width: 3 })
|
||||
}),
|
||||
text: new Text({
|
||||
text: conflict.title,
|
||||
offsetY: -22,
|
||||
font: '12px sans-serif',
|
||||
fill: new Fill({ color: '#ffffff' }),
|
||||
stroke: new Stroke({ color: 'rgba(0,0,0,0.75)', width: 3 })
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// 处理地图点击事件
|
||||
function handleMapClick(event) {
|
||||
const feature = props.map.forEachFeatureAtPixel(event.pixel, function(feature) {
|
||||
@ -633,7 +759,7 @@ function updateVehiclePosition(vehicleData) {
|
||||
iconStyle = new Style({
|
||||
image: new Icon({
|
||||
src: iconSrc,
|
||||
scale: 0.75,
|
||||
scale: isAircraft ? AIRCRAFT_ICON_SCALE : DEFAULT_VEHICLE_ICON_SCALE,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: rotationRad, // 使用计算好的旋转角度
|
||||
})
|
||||
@ -744,7 +870,7 @@ function updateVehiclePosition(vehicleData) {
|
||||
? new Style({
|
||||
image: new Icon({
|
||||
src: iconSrc,
|
||||
scale: 0.75,
|
||||
scale: isAircraft ? AIRCRAFT_ICON_SCALE : DEFAULT_VEHICLE_ICON_SCALE,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: rotationRad,
|
||||
}),
|
||||
@ -854,10 +980,19 @@ function handleWsMessage(message) {
|
||||
handlePathConflictAlert(data.payload);
|
||||
break;
|
||||
|
||||
case 'path_conflict_status':
|
||||
console.log(`路径冲突计算状态: ${data.payload?.aircraftName || ''} ${data.payload?.vehicleName || ''}`);
|
||||
handlePathConflictMessage(data);
|
||||
break;
|
||||
|
||||
case 'path_conflict_alert':
|
||||
// 处理冲突告警和预警
|
||||
console.log(`冲突告警/预警: ${data.payload?.object1?.objectName || data.payload?.messageType}`);
|
||||
if (isNewPathConflictPayload(data.payload)) {
|
||||
handlePathConflictMessage(data);
|
||||
} else {
|
||||
handlePathConflictAlert(data.payload);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'rule_violation':
|
||||
@ -1681,6 +1816,123 @@ function removeAircraftRoute(flightNo) {
|
||||
console.log(`航班 ${flightNo} 的路线已完全移除`);
|
||||
}
|
||||
|
||||
function handlePathConflictMessage(message) {
|
||||
if (!pathConflictDisplayEnabled.value) {
|
||||
clearPathConflictDisplay();
|
||||
return;
|
||||
}
|
||||
|
||||
const conflict = normalizePathConflictMessage(message);
|
||||
if (!conflict) return;
|
||||
|
||||
if (conflict.kind === 'resume') {
|
||||
const matchedConflicts = getPathConflictsToClear(conflict.key || conflict.vehicleName);
|
||||
clearPathConflict(conflict.key || conflict.vehicleName);
|
||||
const statusIds = new Set([conflict.vehicleName, conflict.aircraftName]);
|
||||
matchedConflicts.forEach(item => {
|
||||
statusIds.add(item.vehicleName);
|
||||
statusIds.add(item.aircraftName);
|
||||
});
|
||||
statusIds.forEach(id => {
|
||||
if (id) clearConflictStatus(id);
|
||||
});
|
||||
if (!currentPathConflict.value) clearAlertMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = pathConflicts.value?.[conflict.key];
|
||||
upsertPathConflict(conflict);
|
||||
|
||||
if (conflict.kind === 'alert') {
|
||||
const alarmLevel = conflict.level === 'WARNING' ? 'medium' : 'high';
|
||||
if (!previous || previous.kind !== 'alert' || previous.level !== conflict.level) {
|
||||
addAlarm({
|
||||
carId: conflict.vehicleName || '未知车辆',
|
||||
carType: vehicles.value[conflict.vehicleName]?.type || '路径冲突',
|
||||
time: `${formatTimeRange(new Date())} ${conflict.aircraftName || '航空器'}与${conflict.vehicleName || '车辆'}`,
|
||||
description: conflict.title,
|
||||
level: alarmLevel,
|
||||
type: 'car',
|
||||
rawData: conflict.rawData
|
||||
});
|
||||
}
|
||||
|
||||
showAlert(`${conflict.title}:${conflict.aircraftName || '航空器'} 与 ${conflict.vehicleName || '车辆'}`, conflict.level === 'WARNING' ? 'warning' : 'alarm', 0);
|
||||
markConflictStatus(conflict.vehicleName, conflict.level === 'WARNING' ? 'warning' : 'alarm', conflict.title);
|
||||
markConflictStatus(conflict.aircraftName, conflict.level === 'WARNING' ? 'warning' : 'alarm', conflict.title);
|
||||
}
|
||||
}
|
||||
|
||||
function upsertPathConflict(conflict) {
|
||||
const key = conflict.key || `${Date.now()}`;
|
||||
const next = {
|
||||
...(pathConflicts.value[key] || {}),
|
||||
...conflict,
|
||||
key,
|
||||
updatedAt: Date.now()
|
||||
};
|
||||
pathConflicts.value = {
|
||||
...pathConflicts.value,
|
||||
[key]: next
|
||||
};
|
||||
|
||||
renderPathConflictFeature(next);
|
||||
}
|
||||
|
||||
function renderPathConflictFeature(conflict) {
|
||||
if (!pathConflictSource || !conflict.conflictPoint) return;
|
||||
|
||||
const latitude = Number(conflict.conflictPoint.latitude);
|
||||
const longitude = Number(conflict.conflictPoint.longitude);
|
||||
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return;
|
||||
|
||||
const coordinates = transform([longitude, latitude], 'EPSG:4326', 'EPSG:4528');
|
||||
let feature = pathConflictSource.getFeatureById(conflict.key);
|
||||
if (!feature) {
|
||||
feature = new Feature({
|
||||
geometry: new Point(coordinates),
|
||||
conflictType: 'path_conflict'
|
||||
});
|
||||
feature.setId(conflict.key);
|
||||
pathConflictSource.addFeature(feature);
|
||||
} else {
|
||||
feature.getGeometry().setCoordinates(coordinates);
|
||||
}
|
||||
|
||||
feature.setStyle(getPathConflictStyle(conflict));
|
||||
}
|
||||
|
||||
function clearPathConflict(key) {
|
||||
const next = { ...(pathConflicts.value || {}) };
|
||||
const keys = Object.keys(next).filter(itemKey => {
|
||||
const item = next[itemKey];
|
||||
return itemKey === key || item.vehicleName === key || item.aircraftName === key;
|
||||
});
|
||||
|
||||
keys.forEach(itemKey => {
|
||||
if (pathConflictSource) {
|
||||
const feature = pathConflictSource.getFeatureById(itemKey);
|
||||
if (feature) pathConflictSource.removeFeature(feature);
|
||||
}
|
||||
delete next[itemKey];
|
||||
});
|
||||
|
||||
pathConflicts.value = next;
|
||||
}
|
||||
|
||||
function getPathConflictsToClear(key) {
|
||||
return Object.entries(pathConflicts.value || {})
|
||||
.filter(([itemKey, item]) => itemKey === key || item.vehicleName === key || item.aircraftName === key)
|
||||
.map(([, item]) => item);
|
||||
}
|
||||
|
||||
function clearPathConflictDisplay() {
|
||||
pathConflicts.value = {};
|
||||
if (pathConflictSource) {
|
||||
pathConflictSource.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 处理冲突告警和预警
|
||||
function handlePathConflictAlert(payload) {
|
||||
if (!payload) {
|
||||
@ -1696,7 +1948,6 @@ function handlePathConflictAlert(payload) {
|
||||
const otherVehicleId = object2.objectName || '未知车辆';
|
||||
const distance = payload.object2Distance || 0;
|
||||
const message = payload.message || `与${otherVehicleId}可能发生冲突`;
|
||||
|
||||
const isAlert = (payload.alertType === 'CONFLICT_ALERT') || (payload.alert === true) || (payload.alertLevel === 'CRITICAL');
|
||||
const isWarning = (payload.alertType === 'CONFLICT_WARNING') || (payload.warning === true);
|
||||
if (!isAlert && isWarning) {
|
||||
@ -2085,7 +2336,7 @@ function getDefaultVehicleStyle(vehicle) {
|
||||
return new Style({
|
||||
image: new Icon({
|
||||
src: iconSrc,
|
||||
scale: 0.75,
|
||||
scale: isAircraft ? AIRCRAFT_ICON_SCALE : DEFAULT_VEHICLE_ICON_SCALE,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: rotationRad,
|
||||
})
|
||||
@ -2181,7 +2432,7 @@ function sendSubscribe() {
|
||||
if (wsService) {
|
||||
const message = JSON.stringify({
|
||||
type: 'subscribe',
|
||||
topics: ['position_update', 'collision_warning', 'rule_violation'],
|
||||
topics: ['position_update', 'collision_warning', 'path_conflict_status', 'path_conflict_alert', 'rule_violation'],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
wsService.send(message);
|
||||
@ -2235,8 +2486,15 @@ function cleanup() {
|
||||
aircraftRouteLayer = null;
|
||||
}
|
||||
|
||||
if (pathConflictLayer && props.map) {
|
||||
props.map.removeLayer(pathConflictLayer);
|
||||
pathConflictLayer = null;
|
||||
pathConflictSource = null;
|
||||
}
|
||||
|
||||
// 清空飞机路线数据
|
||||
aircraftRoutes.value = {};
|
||||
pathConflicts.value = {};
|
||||
|
||||
// 移除标签
|
||||
if (props.map && labelSystem.value) {
|
||||
@ -2258,6 +2516,7 @@ onMounted(() => {
|
||||
if (props.map) {
|
||||
createVehicleLayer();
|
||||
createAircraftRouteLayer(); // 创建飞机路线图层
|
||||
createPathConflictLayer();
|
||||
}
|
||||
|
||||
// 连接WebSocket
|
||||
@ -2344,6 +2603,14 @@ watch(() => props.map, (newMap) => {
|
||||
if (newMap) {
|
||||
createVehicleLayer();
|
||||
createAircraftRouteLayer(); // 创建飞机路线图层
|
||||
createPathConflictLayer();
|
||||
}
|
||||
});
|
||||
|
||||
watch(pathConflictDisplayEnabled, (enabled) => {
|
||||
if (!enabled) {
|
||||
clearPathConflictDisplay();
|
||||
clearAlertMessage();
|
||||
}
|
||||
});
|
||||
|
||||
@ -2640,7 +2907,7 @@ function createDefaultStyle(iconSrc, heading) {
|
||||
return new Style({
|
||||
image: new Icon({
|
||||
src: iconSrc, // 使用传入的图标源
|
||||
scale: 0.75,
|
||||
scale: (iconSrc === aircraftIcon || iconSrc === aircraftRouteIcon) ? AIRCRAFT_ICON_SCALE : DEFAULT_VEHICLE_ICON_SCALE,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: rotationRad, // 应用计算后的旋转角度
|
||||
})
|
||||
@ -2692,4 +2959,60 @@ function createDefaultStyle(iconSrc, heading) {
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.path-conflict-card {
|
||||
position: absolute;
|
||||
left: 28px;
|
||||
top: calc(23% + 160px);
|
||||
width: 246px;
|
||||
color: #fff;
|
||||
z-index: 1000;
|
||||
background: rgba(41, 44, 56, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 15px rgba(2, 2, 2, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.path-conflict-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 7px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
background: rgba(52, 55, 68, 0.96);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
|
||||
.path-conflict-level {
|
||||
padding: 1px 7px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
background: rgba(59, 130, 246, 0.28);
|
||||
}
|
||||
|
||||
.path-conflict-body {
|
||||
padding: 8px 10px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.path-conflict-muted,
|
||||
.path-conflict-debug {
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.path-conflict-reason {
|
||||
color: rgba(255, 214, 165, 0.95);
|
||||
}
|
||||
|
||||
.path-conflict-card.level-WARNING .path-conflict-level {
|
||||
background: rgba(245, 158, 11, 0.35);
|
||||
}
|
||||
|
||||
.path-conflict-card.level-CRITICAL .path-conflict-level,
|
||||
.path-conflict-card.level-EMERGENCY .path-conflict-level {
|
||||
background: rgba(239, 68, 68, 0.38);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -27,6 +27,9 @@ const props = defineProps({
|
||||
vehicles: Object
|
||||
});
|
||||
|
||||
const DEFAULT_ICON_SCALE = 1.0;
|
||||
const AIRCRAFT_ICON_SCALE = DEFAULT_ICON_SCALE * 1.3;
|
||||
|
||||
// 获取车辆样式
|
||||
function getVehicleStyle(id, speed, heading) {
|
||||
const vehicle = props.vehicles[id];
|
||||
@ -47,11 +50,11 @@ function getVehicleStyle(id, speed, heading) {
|
||||
}
|
||||
|
||||
// 创建样式
|
||||
return createDefaultStyle(iconSrc, heading);
|
||||
return createDefaultStyle(iconSrc, heading, vehicle.isAircraft ? AIRCRAFT_ICON_SCALE : DEFAULT_ICON_SCALE);
|
||||
}
|
||||
|
||||
// 创建默认样式
|
||||
function createDefaultStyle(iconSrc, heading) {
|
||||
function createDefaultStyle(iconSrc, heading, scale = DEFAULT_ICON_SCALE) {
|
||||
// 确保heading是有效的数值
|
||||
const validHeading = heading !== undefined ? Number(heading) : 0;
|
||||
|
||||
@ -64,7 +67,7 @@ function createDefaultStyle(iconSrc, heading) {
|
||||
return new Style({
|
||||
image: new Icon({
|
||||
src: iconSrc,
|
||||
scale: 1.0,
|
||||
scale,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: rotationRad, // 使用统一的旋转角度计算
|
||||
})
|
||||
|
||||
@ -70,6 +70,13 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>路径冲突显示</span>
|
||||
<span class="comp-style">
|
||||
<el-switch v-model="pathConflictDisplayEnabled" class="drawer-switch" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
|
||||
@ -141,6 +148,13 @@ const dynamicTitle = computed({
|
||||
}
|
||||
})
|
||||
|
||||
const pathConflictDisplayEnabled = computed({
|
||||
get: () => storeSettings.value.pathConflictDisplayEnabled,
|
||||
set: (val) => {
|
||||
settingsStore.changeSetting({ key: 'pathConflictDisplayEnabled', value: val })
|
||||
}
|
||||
})
|
||||
|
||||
function themeChange(val) {
|
||||
settingsStore.changeSetting({ key: 'theme', value: val })
|
||||
theme.value = val;
|
||||
@ -158,6 +172,7 @@ function saveSetting() {
|
||||
"fixedHeader": storeSettings.value.fixedHeader,
|
||||
"sidebarLogo": storeSettings.value.sidebarLogo,
|
||||
"dynamicTitle": storeSettings.value.dynamicTitle,
|
||||
"pathConflictDisplayEnabled": storeSettings.value.pathConflictDisplayEnabled,
|
||||
"sideTheme": storeSettings.value.sideTheme,
|
||||
"theme": storeSettings.value.theme
|
||||
};
|
||||
|
||||
@ -37,6 +37,11 @@ export default {
|
||||
*/
|
||||
dynamicTitle: false,
|
||||
|
||||
/**
|
||||
* 是否显示路径冲突点、过程状态和告警
|
||||
*/
|
||||
pathConflictDisplayEnabled: true,
|
||||
|
||||
/**
|
||||
* @type {string | array} 'production' | ['production', 'development']
|
||||
* @description Need show err logs component.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import defaultSettings from '@/settings'
|
||||
import { useDynamicTitle } from '@/utils/dynamicTitle'
|
||||
|
||||
const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle } = defaultSettings
|
||||
const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle, pathConflictDisplayEnabled } = defaultSettings
|
||||
|
||||
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
|
||||
|
||||
@ -17,7 +17,8 @@ const useSettingsStore = defineStore(
|
||||
tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView,
|
||||
fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader,
|
||||
sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo,
|
||||
dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle
|
||||
dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle,
|
||||
pathConflictDisplayEnabled: storageSetting.pathConflictDisplayEnabled === undefined ? pathConflictDisplayEnabled : storageSetting.pathConflictDisplayEnabled
|
||||
}),
|
||||
actions: {
|
||||
// 修改布局设置
|
||||
|
||||
103
src/utils/pathConflictDisplay.mjs
Normal file
103
src/utils/pathConflictDisplay.mjs
Normal file
@ -0,0 +1,103 @@
|
||||
export function getPathConflictDisplayEnabled(settings = {}) {
|
||||
return settings.pathConflictDisplayEnabled === undefined
|
||||
? true
|
||||
: settings.pathConflictDisplayEnabled !== false
|
||||
}
|
||||
|
||||
export function normalizePathConflictMessage(message) {
|
||||
const payload = message?.payload || {}
|
||||
|
||||
if (message?.type === 'path_conflict_alert' && payload.messageType === 'PATH_CONFLICT_RESUME') {
|
||||
return {
|
||||
kind: 'resume',
|
||||
key: getConflictKey(payload) || payload.vehicleName || payload.object1?.objectName || '',
|
||||
aircraftName: payload.aircraftName || payload.object1?.objectName || '',
|
||||
vehicleName: payload.vehicleName || payload.object2?.objectName || payload.object1?.objectName || '',
|
||||
rawData: payload,
|
||||
}
|
||||
}
|
||||
|
||||
if (message?.type === 'path_conflict_status') {
|
||||
return buildConflict('status', payload, {
|
||||
title: '计算中',
|
||||
level: 'STATUS',
|
||||
})
|
||||
}
|
||||
|
||||
if (message?.type === 'path_conflict_alert') {
|
||||
const level = payload.alertLevel || (payload.alertType === 'CONFLICT_ALERT' || payload.alert === true ? 'CRITICAL' : 'WARNING')
|
||||
return buildConflict('alert', payload, {
|
||||
title: getAlertTitle(level),
|
||||
level,
|
||||
})
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function isNewPathConflictPayload(payload) {
|
||||
if (!payload) return false
|
||||
return Boolean(
|
||||
payload.messageType === 'PATH_CONFLICT_RESUME' ||
|
||||
payload.aircraftName ||
|
||||
payload.vehicleName ||
|
||||
payload.aircraftDistanceToConflictMeters !== undefined ||
|
||||
payload.vehicleDistanceToConflictMeters !== undefined ||
|
||||
payload.aircraftAlertThresholdMeters !== undefined ||
|
||||
payload.vehicleAlertThresholdMeters !== undefined ||
|
||||
payload.vehicleMovingTowardConflictPoint !== undefined ||
|
||||
payload.conflictPoint ||
|
||||
payload.position
|
||||
)
|
||||
}
|
||||
|
||||
function buildConflict(kind, payload, extra) {
|
||||
const aircraftName = payload.aircraftName || payload.object1?.objectName || ''
|
||||
const vehicleName = payload.vehicleName || payload.object2?.objectName || payload.object1?.objectName || ''
|
||||
|
||||
return {
|
||||
kind,
|
||||
...extra,
|
||||
key: getConflictKey(payload) || [aircraftName, vehicleName].filter(Boolean).join('::'),
|
||||
conflictPoint: payload.conflictPoint || payload.position || null,
|
||||
aircraftName,
|
||||
vehicleName,
|
||||
aircraftDistance: payload.aircraftDistanceToConflictMeters,
|
||||
vehicleDistance: payload.vehicleDistanceToConflictMeters || payload.object2Distance,
|
||||
aircraftThreshold: payload.aircraftAlertThresholdMeters,
|
||||
vehicleThreshold: payload.vehicleAlertThresholdMeters,
|
||||
vehicleMovingToward: payload.vehicleMovingTowardConflictPoint,
|
||||
directionLockStatus: payload.directionLockStatus,
|
||||
calculationStatus: payload.calculationStatus,
|
||||
directionLockText: getDirectionLockText(payload),
|
||||
directionLockFailed: isDirectionLockFailed(payload),
|
||||
directionLockReason: payload.directionLockReason || '',
|
||||
object1ForwardDistance: payload.object1ForwardDistanceMeters,
|
||||
object2ForwardDistance: payload.object2ForwardDistanceMeters,
|
||||
rawData: payload,
|
||||
}
|
||||
}
|
||||
|
||||
function getConflictKey(payload) {
|
||||
const aircraftName = payload.aircraftName || ''
|
||||
const vehicleName = payload.vehicleName || payload.object2?.objectName || payload.object1?.objectName || ''
|
||||
return [aircraftName, vehicleName].filter(Boolean).join('::')
|
||||
}
|
||||
|
||||
function getAlertTitle(level) {
|
||||
if (level === 'EMERGENCY') return '紧急告警'
|
||||
if (level === 'CRITICAL') return '冲突告警'
|
||||
return '冲突预警'
|
||||
}
|
||||
|
||||
function isDirectionLockFailed(payload) {
|
||||
return payload.directionLockStatus === 'DIRECTION_INVALID' ||
|
||||
payload.calculationStatus === 'DIRECTION_LOCK_FAILED'
|
||||
}
|
||||
|
||||
function getDirectionLockText(payload) {
|
||||
if (payload.directionLockStatus === undefined && payload.calculationStatus === undefined) return ''
|
||||
if (isDirectionLockFailed(payload)) return '方向无法锁定'
|
||||
if (payload.directionLockStatus === 'DIRECTION_LOCKED') return '方向已锁定'
|
||||
return ''
|
||||
}
|
||||
93
tests/pathConflictDisplay.test.mjs
Normal file
93
tests/pathConflictDisplay.test.mjs
Normal file
@ -0,0 +1,93 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import {
|
||||
getPathConflictDisplayEnabled,
|
||||
isNewPathConflictPayload,
|
||||
normalizePathConflictMessage,
|
||||
} from '../src/utils/pathConflictDisplay.mjs'
|
||||
|
||||
assert.equal(getPathConflictDisplayEnabled({}), true)
|
||||
assert.equal(getPathConflictDisplayEnabled({ pathConflictDisplayEnabled: false }), false)
|
||||
|
||||
const status = normalizePathConflictMessage({
|
||||
type: 'path_conflict_status',
|
||||
payload: {
|
||||
conflictPoint: { latitude: 36.1, longitude: 120.2 },
|
||||
aircraftName: 'A01',
|
||||
vehicleName: 'V01',
|
||||
aircraftDistanceToConflictMeters: 120,
|
||||
vehicleDistanceToConflictMeters: 80,
|
||||
aircraftAlertThresholdMeters: 200,
|
||||
vehicleAlertThresholdMeters: 100,
|
||||
vehicleMovingTowardConflictPoint: true,
|
||||
directionLockStatus: 'DIRECTION_LOCKED',
|
||||
calculationStatus: 'MONITORING',
|
||||
directionLockReason: '',
|
||||
object1ForwardDistanceMeters: 118,
|
||||
object2ForwardDistanceMeters: 78,
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(status.kind, 'status')
|
||||
assert.equal(status.title, '计算中')
|
||||
assert.deepEqual(status.conflictPoint, { latitude: 36.1, longitude: 120.2 })
|
||||
assert.equal(status.key, 'A01::V01')
|
||||
assert.equal(status.directionLockText, '方向已锁定')
|
||||
assert.equal(status.directionLockFailed, false)
|
||||
assert.equal(status.directionLockReason, '')
|
||||
assert.equal(status.object1ForwardDistance, 118)
|
||||
assert.equal(status.object2ForwardDistance, 78)
|
||||
|
||||
const directionInvalid = normalizePathConflictMessage({
|
||||
type: 'path_conflict_status',
|
||||
payload: {
|
||||
aircraftName: 'A01',
|
||||
vehicleName: 'V01',
|
||||
directionLockStatus: 'DIRECTION_INVALID',
|
||||
calculationStatus: 'DIRECTION_LOCK_FAILED',
|
||||
directionLockReason: '冲突点在两侧',
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(directionInvalid.directionLockText, '方向无法锁定')
|
||||
assert.equal(directionInvalid.directionLockFailed, true)
|
||||
assert.equal(directionInvalid.directionLockReason, '冲突点在两侧')
|
||||
|
||||
const resume = normalizePathConflictMessage({
|
||||
type: 'path_conflict_alert',
|
||||
payload: {
|
||||
messageType: 'PATH_CONFLICT_RESUME',
|
||||
aircraftName: 'A01',
|
||||
vehicleName: 'V01',
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(resume.kind, 'resume')
|
||||
assert.equal(resume.key, 'A01::V01')
|
||||
assert.equal(resume.aircraftName, 'A01')
|
||||
assert.equal(resume.vehicleName, 'V01')
|
||||
|
||||
const alert = normalizePathConflictMessage({
|
||||
type: 'path_conflict_alert',
|
||||
payload: {
|
||||
alertLevel: 'EMERGENCY',
|
||||
conflictPoint: { latitude: 36.1, longitude: 120.2 },
|
||||
aircraftName: 'A01',
|
||||
vehicleName: 'V01',
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(alert.kind, 'alert')
|
||||
assert.equal(alert.level, 'EMERGENCY')
|
||||
assert.equal(alert.title, '紧急告警')
|
||||
|
||||
assert.equal(isNewPathConflictPayload({
|
||||
object1: { objectName: 'V01' },
|
||||
object2: { objectName: 'V02' },
|
||||
alertType: 'CONFLICT_WARNING',
|
||||
}), false)
|
||||
|
||||
assert.equal(isNewPathConflictPayload({
|
||||
aircraftName: 'A01',
|
||||
vehicleName: 'V01',
|
||||
conflictPoint: { latitude: 36.1, longitude: 120.2 },
|
||||
}), true)
|
||||
Loading…
Reference in New Issue
Block a user