Browse Source

冲突1.1

mh
menghao 3 weeks ago
parent
commit
6bdf9f2530
  1. 3
      ruoyi-ui/package.json
  2. 4
      ruoyi-ui/src/utils/conflictDetection.js
  3. 287
      ruoyi-ui/src/views/childRoom/index.vue

3
ruoyi-ui/package.json

@ -71,7 +71,8 @@
"sass-loader": "10.1.1", "sass-loader": "10.1.1",
"script-ext-html-webpack-plugin": "2.1.5", "script-ext-html-webpack-plugin": "2.1.5",
"svg-sprite-loader": "5.1.1", "svg-sprite-loader": "5.1.1",
"vue-template-compiler": "2.6.12" "vue-template-compiler": "2.6.12",
"worker-loader": "^3.0.8"
}, },
"engines": { "engines": {
"node": ">=8.9", "node": ">=8.9",

4
ruoyi-ui/src/utils/conflictDetection.js

@ -17,6 +17,10 @@ export const defaultConflictConfig = {
// 时间 // 时间
timeWindowOverlapMinutes: 0, // 时间窗重叠判定:两航线时间窗重叠即报(0=任意重叠) timeWindowOverlapMinutes: 0, // 时间窗重叠判定:两航线时间窗重叠即报(0=任意重叠)
resourceBufferMinutes: 0, // 资源占用前后缓冲(分钟) resourceBufferMinutes: 0, // 资源占用前后缓冲(分钟)
/** 到达时间容差(秒):早于或晚于计划在此秒数内不报冲突,超出才报 */
timeToleranceSeconds: 5,
/** 速度容差(km/h):与所需/建议速度差在此范围内不报冲突,超出才报 */
speedToleranceKmh: 20,
// 空间 // 空间
minTrackSeparationMeters: 5000, // 推演中两平台航迹最小间隔(米) minTrackSeparationMeters: 5000, // 推演中两平台航迹最小间隔(米)
minPlatformPlacementMeters: 3000, // 摆放平台图标最小间距(米) minPlatformPlacementMeters: 3000, // 摆放平台图标最小间距(米)

287
ruoyi-ui/src/views/childRoom/index.vue

@ -520,15 +520,10 @@ import GanttDrawer from './GanttDrawer.vue';
import { import {
CONFLICT_TYPE, CONFLICT_TYPE,
defaultConflictConfig, defaultConflictConfig,
detectTimeWindowOverlap,
detectTrackSeparation,
detectPlatformPlacementTooClose,
detectRestrictedZoneIntrusion,
parseRestrictedZonesFromDrawings,
detectSpectrumConflicts,
createSpectrumLedgerEntry, createSpectrumLedgerEntry,
normalizeConflictList normalizeConflictList
} from '@/utils/conflictDetection'; } from '@/utils/conflictDetection';
import ConflictCheckWorker from 'worker-loader!@/workers/conflictCheck.worker.js'
export default { export default {
name: 'MissionPlanningView', name: 'MissionPlanningView',
components: { components: {
@ -747,6 +742,8 @@ export default {
conflictConfig: { ...defaultConflictConfig }, conflictConfig: { ...defaultConflictConfig },
/** 频谱资源台账(用于频谱冲突检测),可后续从接口或界面维护 */ /** 频谱资源台账(用于频谱冲突检测),可后续从接口或界面维护 */
spectrumLedger: [], spectrumLedger: [],
/** 冲突检测 worker(非响应式对象,仅占位,实际实例挂在 this._conflictWorker) */
_conflictWorkerInited: false,
// //
activePlatformTab: 'air', activePlatformTab: 'air',
@ -1036,6 +1033,11 @@ export default {
clearInterval(this.playbackInterval); clearInterval(this.playbackInterval);
this.playbackInterval = null; this.playbackInterval = null;
} }
// worker
if (this._conflictWorker && this._conflictWorker.terminate) {
try { this._conflictWorker.terminate(); } catch (_) {}
this._conflictWorker = null;
}
}, },
created() { created() {
this.currentRoomId = this.$route.query.roomId; this.currentRoomId = this.$route.query.roomId;
@ -1292,7 +1294,8 @@ export default {
if (segmentMode != null) addPayload.segmentMode = segmentMode; if (segmentMode != null) addPayload.segmentMode = segmentMode;
if (segmentTargetMinutes != null) addPayload.segmentTargetMinutes = segmentTargetMinutes; if (segmentTargetMinutes != null) addPayload.segmentTargetMinutes = segmentTargetMinutes;
if (segmentTargetSpeed != null) addPayload.segmentTargetSpeed = segmentTargetSpeed; if (segmentTargetSpeed != null) addPayload.segmentTargetSpeed = segmentTargetSpeed;
const addRes = await addWaypoints(addPayload); const roomIdParam = this.currentRoomId != null ? { roomId: this.currentRoomId } : {};
const addRes = await addWaypoints(addPayload, roomIdParam);
await this.getList(); await this.getList();
let updated = this.routes.find(r => r.id === routeId); let updated = this.routes.find(r => r.id === routeId);
if (!updated || !updated.waypoints || updated.waypoints.length !== count) { if (!updated || !updated.waypoints || updated.waypoints.length !== count) {
@ -1351,7 +1354,7 @@ export default {
if (isNewlyInserted && segmentTargetMinutes != null) updatePayload.segmentTargetMinutes = segmentTargetMinutes; if (isNewlyInserted && segmentTargetMinutes != null) updatePayload.segmentTargetMinutes = segmentTargetMinutes;
if (isNewlyInserted && segmentTargetSpeed != null) updatePayload.segmentTargetSpeed = segmentTargetSpeed; if (isNewlyInserted && segmentTargetSpeed != null) updatePayload.segmentTargetSpeed = segmentTargetSpeed;
if (isNewlyInserted && segmentMode === 'fixed_speed') updatePayload.startTime = startTime; if (isNewlyInserted && segmentMode === 'fixed_speed') updatePayload.startTime = startTime;
await updateWaypoints(updatePayload); await updateWaypoints(updatePayload, roomIdParam);
} }
await this.getList(); await this.getList();
updated = this.routes.find(r => r.id === routeId); updated = this.routes.find(r => r.id === routeId);
@ -1464,7 +1467,8 @@ export default {
} else { } else {
payload.turnRadius = 0; payload.turnRadius = 0;
} }
const response = await updateWaypoints(payload); const roomIdParam = this.currentRoomId != null ? { roomId: this.currentRoomId } : {};
const response = await updateWaypoints(payload, roomIdParam);
if (response.code !== 200) throw new Error(response.msg || '更新失败'); if (response.code !== 200) throw new Error(response.msg || '更新失败');
const merged = { ...wp, ...payload }; const merged = { ...wp, ...payload };
const routeInList = this.routes.find(r => r.id === routeId); const routeInList = this.routes.find(r => r.id === routeId);
@ -1571,7 +1575,8 @@ export default {
seq: wp.seq, seq: wp.seq,
lat: Number(lat), lat: Number(lat),
lng: Number(lng), lng: Number(lng),
alt: Number(alt), // 5000 -> 4999.999...
alt: wp.alt,
speed: wp.speed, speed: wp.speed,
startTime: (wp.startTime != null && wp.startTime !== '') ? wp.startTime : 'K+00:00:00', startTime: (wp.startTime != null && wp.startTime !== '') ? wp.startTime : 'K+00:00:00',
turnAngle: wp.turnAngle turnAngle: wp.turnAngle
@ -1589,8 +1594,9 @@ export default {
const prevWp = idx > 0 ? waypoints[idx - 1] : null; const prevWp = idx > 0 ? waypoints[idx - 1] : null;
payload.turnRadius = this.$refs.cesiumMap.getTurnRadiusFromPrevSpeed(prevWp, wp.turnAngle); payload.turnRadius = this.$refs.cesiumMap.getTurnRadiusFromPrevSpeed(prevWp, wp.turnAngle);
} }
const roomIdParam = this.currentRoomId != null ? { roomId: this.currentRoomId } : {};
try { try {
const response = await updateWaypoints(payload); const response = await updateWaypoints(payload, roomIdParam);
if (response.code === 200) { if (response.code === 200) {
const merged = { ...wp, ...payload }; const merged = { ...wp, ...payload };
if (idx !== -1) waypoints.splice(idx, 1, merged); if (idx !== -1) waypoints.splice(idx, 1, merged);
@ -1614,7 +1620,7 @@ export default {
const startPayload = { ...merged, startTime: newStartTime }; const startPayload = { ...merged, startTime: newStartTime };
if (merged.segmentMode != null) startPayload.segmentMode = merged.segmentMode; if (merged.segmentMode != null) startPayload.segmentMode = merged.segmentMode;
try { try {
const r2 = await updateWaypoints(startPayload); const r2 = await updateWaypoints(startPayload, roomIdParam);
if (r2.code === 200) { if (r2.code === 200) {
Object.assign(merged, { startTime: newStartTime }); Object.assign(merged, { startTime: newStartTime });
if (idx !== -1) waypoints.splice(idx, 1, merged); if (idx !== -1) waypoints.splice(idx, 1, merged);
@ -1635,7 +1641,7 @@ export default {
const speedPayload = { ...prev, speed: speedVal }; const speedPayload = { ...prev, speed: speedVal };
if (prev.segmentMode != null) speedPayload.segmentMode = prev.segmentMode; if (prev.segmentMode != null) speedPayload.segmentMode = prev.segmentMode;
try { try {
const r2 = await updateWaypoints(speedPayload); const r2 = await updateWaypoints(speedPayload, roomIdParam);
if (r2.code === 200) { if (r2.code === 200) {
Object.assign(prev, { speed: speedVal }); Object.assign(prev, { speed: speedVal });
const prevIdx = idx - 1; const prevIdx = idx - 1;
@ -1673,7 +1679,7 @@ export default {
if (merged.pixelSize != null) currPayload.pixelSize = merged.pixelSize; if (merged.pixelSize != null) currPayload.pixelSize = merged.pixelSize;
if (merged.outlineColor != null) currPayload.outlineColor = merged.outlineColor; if (merged.outlineColor != null) currPayload.outlineColor = merged.outlineColor;
try { try {
const r2 = await updateWaypoints(currPayload); const r2 = await updateWaypoints(currPayload, roomIdParam);
if (r2.code === 200) { if (r2.code === 200) {
Object.assign(merged, { speed: speedVal }); Object.assign(merged, { speed: speedVal });
waypoints.splice(idx, 1, merged); waypoints.splice(idx, 1, merged);
@ -2260,7 +2266,7 @@ export default {
} else { } else {
payload.turnRadius = 0; payload.turnRadius = 0;
} }
await updateWaypoints(payload); await updateWaypoints(payload, this.currentRoomId != null ? { roomId: this.currentRoomId } : {});
} }
const mergedWaypoints = (newRouteData.waypoints || []).map((oldWp) => { const mergedWaypoints = (newRouteData.waypoints || []).map((oldWp) => {
const fromDialog = updatedRoute.waypoints.find((w) => w.id === oldWp.id); const fromDialog = updatedRoute.waypoints.find((w) => w.id === oldWp.id);
@ -2685,7 +2691,8 @@ export default {
if (updatedWaypoint.pixelSize != null) payload.pixelSize = updatedWaypoint.pixelSize; if (updatedWaypoint.pixelSize != null) payload.pixelSize = updatedWaypoint.pixelSize;
if (updatedWaypoint.color != null) payload.color = updatedWaypoint.color; if (updatedWaypoint.color != null) payload.color = updatedWaypoint.color;
if (updatedWaypoint.outlineColor != null) payload.outlineColor = updatedWaypoint.outlineColor; if (updatedWaypoint.outlineColor != null) payload.outlineColor = updatedWaypoint.outlineColor;
const response = await updateWaypoints(payload); const roomIdParam = this.currentRoomId != null ? { roomId: this.currentRoomId } : {};
const response = await updateWaypoints(payload, roomIdParam);
if (response.code === 200) { if (response.code === 200) {
const roomId = this.currentRoomId; const roomId = this.currentRoomId;
const index = sd.waypoints.findIndex(p => p.id === updatedWaypoint.id); const index = sd.waypoints.findIndex(p => p.id === updatedWaypoint.id);
@ -2719,7 +2726,7 @@ export default {
if (prev.pixelSize != null) prevPayload.pixelSize = prev.pixelSize; if (prev.pixelSize != null) prevPayload.pixelSize = prev.pixelSize;
if (prev.outlineColor != null) prevPayload.outlineColor = prev.outlineColor; if (prev.outlineColor != null) prevPayload.outlineColor = prev.outlineColor;
try { try {
const r2 = await updateWaypoints(prevPayload); const r2 = await updateWaypoints(prevPayload, roomIdParam);
if (r2.code === 200) { if (r2.code === 200) {
Object.assign(prev, { speed: speedVal }); Object.assign(prev, { speed: speedVal });
sd.waypoints.splice(index - 1, 1, prev); sd.waypoints.splice(index - 1, 1, prev);
@ -2755,7 +2762,7 @@ export default {
if (merged.color != null) currPayload.color = merged.color; if (merged.color != null) currPayload.color = merged.color;
if (merged.outlineColor != null) currPayload.outlineColor = merged.outlineColor; if (merged.outlineColor != null) currPayload.outlineColor = merged.outlineColor;
try { try {
const r2 = await updateWaypoints(currPayload); const r2 = await updateWaypoints(currPayload, roomIdParam);
if (r2.code === 200) { if (r2.code === 200) {
Object.assign(merged, { speed: speedVal }); Object.assign(merged, { speed: speedVal });
sd.waypoints.splice(index, 1, merged); sd.waypoints.splice(index, 1, merged);
@ -2804,7 +2811,7 @@ export default {
if (wp.pixelSize != null) cascadePayload.pixelSize = wp.pixelSize; if (wp.pixelSize != null) cascadePayload.pixelSize = wp.pixelSize;
if (wp.outlineColor != null) cascadePayload.outlineColor = wp.outlineColor; if (wp.outlineColor != null) cascadePayload.outlineColor = wp.outlineColor;
try { try {
const rCascade = await updateWaypoints(cascadePayload); const rCascade = await updateWaypoints(cascadePayload, roomIdParam);
if (rCascade.code === 200) { if (rCascade.code === 200) {
Object.assign(wp, { startTime: newStartTime, ...(wp.segmentMode === 'fixed_time' && { segmentTargetMinutes: newArrival }) }); Object.assign(wp, { startTime: newStartTime, ...(wp.segmentMode === 'fixed_time' && { segmentTargetMinutes: newArrival }) });
sd.waypoints.splice(i, 1, wp); sd.waypoints.splice(i, 1, wp);
@ -3969,15 +3976,9 @@ export default {
// /退 // /退
this.toggleWhiteboardMode(); this.toggleWhiteboardMode();
} else if (item.id === 'start') { } else if (item.id === 'start') {
// 线线 // Web Worker线线
this.showConflictDrawer = true; this.showConflictDrawer = true;
this.conflictCheckRunning = true;
this.$nextTick(() => {
setTimeout(() => {
this.runConflictCheck(); this.runConflictCheck();
this.conflictCheckRunning = false;
}, 0);
});
} else if (item.id === 'insert') { } else if (item.id === 'insert') {
// //
if (this.activeRightTab === 'platform' && !this.isRightPanelHidden) { if (this.activeRightTab === 'platform' && !this.isRightPanelHidden) {
@ -4347,6 +4348,9 @@ export default {
const path = pathData && pathData.path; const path = pathData && pathData.path;
const segmentEndIndices = pathData && pathData.segmentEndIndices; const segmentEndIndices = pathData && pathData.segmentEndIndices;
const holdArcRanges = pathData && pathData.holdArcRanges || {}; const holdArcRanges = pathData && pathData.holdArcRanges || {};
const config = this.conflictConfig || defaultConflictConfig;
const timeTolMin = (config.timeToleranceSeconds != null ? config.timeToleranceSeconds : 5) / 60;
const speedTol = (config.speedToleranceKmh != null ? config.speedToleranceKmh : 20);
let skipNextLeg = false; let skipNextLeg = false;
for (let i = 0; i < points.length - 1; i++) { for (let i = 0; i < points.length - 1; i++) {
if (skipNextLeg) { if (skipNextLeg) {
@ -4489,7 +4493,8 @@ export default {
const scheduled = points[i + 1].minutes; const scheduled = points[i + 1].minutes;
if (travelMin > 0 && scheduled - points[i].minutes > 0) { if (travelMin > 0 && scheduled - points[i].minutes > 0) {
const requiredSpeedKmh = (dist / 1000) / ((scheduled - points[i].minutes) / 60); const requiredSpeedKmh = (dist / 1000) / ((scheduled - points[i].minutes) / 60);
if (actualArrival > scheduled) { const needFasterBy = requiredSpeedKmh - speedKmh;
if (actualArrival > scheduled + timeTolMin && needFasterBy > speedTol) {
warnings.push( warnings.push(
`某航段:距离约 ${(dist / 1000).toFixed(1)}km,计划 ${(scheduled - points[i].minutes).toFixed(0)} 分钟,当前速度 ${speedKmh}km/h 无法按时到达,约需 ≥${Math.ceil(requiredSpeedKmh)}km/h,请调整相对K时或速度。` `某航段:距离约 ${(dist / 1000).toFixed(1)}km,计划 ${(scheduled - points[i].minutes).toFixed(0)} 分钟,当前速度 ${speedKmh}km/h 无法按时到达,约需 ≥${Math.ceil(requiredSpeedKmh)}km/h,请调整相对K时或速度。`
); );
@ -4498,9 +4503,11 @@ export default {
fromName: waypoints[i].name, fromName: waypoints[i].name,
toName: waypoints[i + 1].name, toName: waypoints[i + 1].name,
requiredSpeedKmh: Math.ceil(requiredSpeedKmh), requiredSpeedKmh: Math.ceil(requiredSpeedKmh),
speedKmh speedKmh,
actualArrival,
scheduled
}); });
} else if (actualArrival < scheduled - 0.5) { } else if (actualArrival < scheduled - timeTolMin) {
warnings.push('存在航段将提前到达下一航点,平台将在该点等待至计划时间再飞往下一段。'); warnings.push('存在航段将提前到达下一航点,平台将在该点等待至计划时间再飞往下一段。');
} }
} }
@ -4528,8 +4535,22 @@ export default {
const travelMin = (dist / 1000) * (60 / speedKmh); const travelMin = (dist / 1000) * (60 / speedKmh);
const actualArrival = effectiveTime[i] + travelMin; const actualArrival = effectiveTime[i] + travelMin;
const scheduled = points[i + 1].minutes; const scheduled = points[i + 1].minutes;
if (travelMin > 0 && scheduled - points[i].minutes > 0 && actualArrival < scheduled - 0.5) { const earlyMinutes = scheduled - actualArrival;
earlyArrivalLegs.push({ legIndex: i, scheduled, actualArrival, fromName: waypoints[i].name, toName: waypoints[i + 1].name }); const planMinutes = scheduled - points[i].minutes;
const suggestedSpeedKmh = planMinutes > 0.01 ? Math.round((dist / 1000) * (60 / planMinutes) * 10) / 10 : null;
const tooFastBy = suggestedSpeedKmh != null ? speedKmh - suggestedSpeedKmh : 0;
if (travelMin > 0 && planMinutes > 0 && actualArrival < scheduled - timeTolMin && (suggestedSpeedKmh == null || tooFastBy > speedTol)) {
earlyArrivalLegs.push({
legIndex: i,
scheduled,
actualArrival,
fromName: waypoints[i].name,
toName: waypoints[i + 1].name,
dist,
speedKmh,
earlyMinutes,
suggestedSpeedKmh
});
} }
} }
return { segments, warnings, earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts }; return { segments, warnings, earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts };
@ -4819,6 +4840,7 @@ export default {
const startTime = this.addHoldForm.startTimeMinutes !== '' && this.addHoldForm.startTimeMinutes != null && !Number.isNaN(Number(this.addHoldForm.startTimeMinutes)) const startTime = this.addHoldForm.startTimeMinutes !== '' && this.addHoldForm.startTimeMinutes != null && !Number.isNaN(Number(this.addHoldForm.startTimeMinutes))
? this.minutesToStartTime(Number(this.addHoldForm.startTimeMinutes)) ? this.minutesToStartTime(Number(this.addHoldForm.startTimeMinutes))
: (nextWp.startTime || 'K+01:00'); : (nextWp.startTime || 'K+01:00');
const roomIdParam = this.currentRoomId != null ? { roomId: this.currentRoomId } : {};
try { try {
await addWaypoints({ await addWaypoints({
routeId, routeId,
@ -4831,12 +4853,12 @@ export default {
startTime, startTime,
pointType: this.addHoldForm.holdType, pointType: this.addHoldForm.holdType,
holdParams: JSON.stringify(holdParams) holdParams: JSON.stringify(holdParams)
}); }, roomIdParam);
await delWaypoints(nextWp.id); await delWaypoints(nextWp.id, roomIdParam);
for (let i = legIndex + 2; i < waypoints.length; i++) { for (let i = legIndex + 2; i < waypoints.length; i++) {
const w = waypoints[i]; const w = waypoints[i];
if (w.id) { if (w.id) {
await updateWaypoints({ ...w, seq: baseSeq + (i - legIndex) }); await updateWaypoints({ ...w, seq: baseSeq + (i - legIndex) }, roomIdParam);
} }
} }
this.showAddHoldDialog = false; this.showAddHoldDialog = false;
@ -5221,20 +5243,36 @@ export default {
}, },
// 线 // 线
runConflictCheck() { async runConflictCheck() {
const routeIds = this.activeRouteIds && this.activeRouteIds.length > 0 ? this.activeRouteIds : this.routes.map(r => r.id); // requestId
if (!this._conflictWorkerInited) {
this._initConflictWorkerOnce();
this._conflictWorkerInited = true;
}
const requestId = this._nextConflictRequestId();
this.conflictCheckRunning = true;
try {
const routeIdsAll = (this.activeRouteIds && this.activeRouteIds.length > 0) ? this.activeRouteIds : this.routes.map(r => r.id);
const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); const { minMinutes, maxMinutes } = this.getDeductionTimeRange();
const config = this.conflictConfig || defaultConflictConfig; const config = this.conflictConfig || defaultConflictConfig;
const waypointStartTimeToMinutes = (s) => this.waypointStartTimeToMinutes(s);
const allRaw = []; const allRaw = [];
/** 按航线缓存 timeline(segments+path),供航迹间隔检测复用,避免每 (routeId,t) 重复 buildRouteTimeline 导致航线多时卡死 */
const routeIdToTimeline = {}; const routeIdToTimeline = {};
const routeIdsWithTimeline = [];
// ---------- 线---------- // ---------- 线----------
routeIds.forEach(routeId => { // timelinesegments+path worker
for (let idx = 0; idx < routeIdsAll.length; idx++) {
const routeId = routeIdsAll[idx];
const route = this.routes.find(r => r.id === routeId); const route = this.routes.find(r => r.id === routeId);
if (!route || !route.waypoints || route.waypoints.length < 2) return; if (!route || !route.waypoints || route.waypoints.length < 2) continue;
const cache = this._getConflictTimelineCache(routeId, route.waypoints, minMinutes, maxMinutes);
if (cache) {
routeIdToTimeline[routeId] = cache;
routeIdsWithTimeline.push(routeId);
} else {
let pathData = null; let pathData = null;
if (this.$refs.cesiumMap && this.$refs.cesiumMap.getRoutePathWithSegmentIndices) { if (this.$refs.cesiumMap && this.$refs.cesiumMap.getRoutePathWithSegmentIndices) {
const ret = this.$refs.cesiumMap.getRoutePathWithSegmentIndices(route.waypoints); const ret = this.$refs.cesiumMap.getRoutePathWithSegmentIndices(route.waypoints);
@ -5243,15 +5281,21 @@ export default {
} }
} }
const timeline = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData); const timeline = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData);
const { earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts } = timeline;
routeIdToTimeline[routeId] = { routeIdToTimeline[routeId] = {
segments: timeline.segments, segments: timeline.segments,
path: pathData && pathData.path ? pathData.path : null, path: pathData && pathData.path ? pathData.path : null,
segmentEndIndices: pathData && pathData.segmentEndIndices ? pathData.segmentEndIndices : null segmentEndIndices: pathData && pathData.segmentEndIndices ? pathData.segmentEndIndices : null
}; };
this._setConflictTimelineCache(routeId, route.waypoints, minMinutes, maxMinutes, routeIdToTimeline[routeId]);
routeIdsWithTimeline.push(routeId);
const { earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts } = timeline;
const routeName = route.name || `航线${route.id}`; const routeName = route.name || `航线${route.id}`;
(earlyArrivalLegs || []).forEach(leg => { (earlyArrivalLegs || []).forEach(leg => {
// K=//K/ const earlyMin = leg.earlyMinutes != null ? Math.round(leg.earlyMinutes * 10) / 10 : 0;
const earlyStr = earlyMin >= 0.1 ? `${earlyMin} 分钟` : `${Math.round(earlyMin * 60)}`;
const speedStr = leg.suggestedSpeedKmh != null && Number.isFinite(leg.suggestedSpeedKmh) ? `${leg.suggestedSpeedKmh} km/h` : '(按计划时间反算)';
const kTimeStr = this.minutesToStartTime(leg.scheduled);
allRaw.push({ allRaw.push({
type: CONFLICT_TYPE.TIME, type: CONFLICT_TYPE.TIME,
subType: 'early_arrival', subType: 'early_arrival',
@ -5261,12 +5305,13 @@ export default {
fromWaypoint: leg.fromName, fromWaypoint: leg.fromName,
toWaypoint: leg.toName, toWaypoint: leg.toName,
time: this.minutesToStartTime(leg.actualArrival), time: this.minutesToStartTime(leg.actualArrival),
suggestion: '① 降低本段速度 ② 下一航点为盘旋点时依靠盘旋等待 ③ 将下一航点相对K时调晚', suggestion: `① 将本段速度降至 ${speedStr} ② 若下一航点为盘旋点,可盘旋等待 ${earlyStr} ③ 将下一航点相对K时调至 ${kTimeStr} 或更晚`,
severity: 'high' severity: 'high'
}); });
}); });
(lateArrivalLegs || []).forEach(leg => { (lateArrivalLegs || []).forEach(leg => {
// KK/ const kTimeStr = leg.actualArrival != null && Number.isFinite(leg.actualArrival) ? this.minutesToStartTime(leg.actualArrival) : '';
const part2 = kTimeStr ? ` ② 或将下一航点相对K时调至 ${kTimeStr} 或更晚` : ' ② 或将下一航点相对K时调晚';
allRaw.push({ allRaw.push({
type: CONFLICT_TYPE.TIME, type: CONFLICT_TYPE.TIME,
subType: 'late_arrival', subType: 'late_arrival',
@ -5275,12 +5320,11 @@ export default {
routeIds: [routeId], routeIds: [routeId],
fromWaypoint: leg.fromName, fromWaypoint: leg.fromName,
toWaypoint: leg.toName, toWaypoint: leg.toName,
suggestion: `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h ② 将下一航点相对K时调早 ③ 调整上游航段速度或时间`, suggestion: `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h${part2} ③ 调整上游航段速度或时间`,
severity: 'high' severity: 'high'
}); });
}); });
(holdDelayConflicts || []).forEach(conf => { (holdDelayConflicts || []).forEach(conf => {
// =K-K/
allRaw.push({ allRaw.push({
type: CONFLICT_TYPE.TIME, type: CONFLICT_TYPE.TIME,
subType: 'hold_delay', subType: 'hold_delay',
@ -5299,43 +5343,37 @@ export default {
positionAlt: conf.holdCenter && conf.holdCenter.alt positionAlt: conf.holdCenter && conf.holdCenter.alt
}); });
}); });
});
// /
// detectTimeWindowOverlap
// ---------- 使 timeline (routeId,t) buildRouteTimeline----------
const getPositionAtMinutesForConflict = (routeId, minutesFromK) => {
const c = routeIdToTimeline[routeId];
if (!c || !c.segments || c.segments.length === 0) return null;
const pos = this.getPositionFromTimeline(c.segments, minutesFromK, c.path, c.segmentEndIndices);
return pos || null;
};
const trackConflicts = detectTrackSeparation(routeIds, minMinutes, maxMinutes, getPositionAtMinutesForConflict, config);
trackConflicts.forEach(c => allRaw.push(c));
// ---------- ----------
const platformIcons = this.$refs.cesiumMap && this.$refs.cesiumMap.getPlatformIconPositions ? this.$refs.cesiumMap.getPlatformIconPositions() : [];
const placementConflicts = detectPlatformPlacementTooClose(platformIcons, config);
placementConflicts.forEach(c => allRaw.push(c));
// ---------- ----------
let restrictedZones = [];
if (this.$refs.cesiumMap && this.$refs.cesiumMap.getFrontendDrawingsData) {
const drawings = this.$refs.cesiumMap.getFrontendDrawingsData();
const entities = (drawings && drawings.entities) || [];
restrictedZones = parseRestrictedZonesFromDrawings(entities, config.restrictedZoneNameKeywords || defaultConflictConfig.restrictedZoneNameKeywords);
restrictedZones = restrictedZones.map(z => ({ ...z, points: z.points || (z.data && z.data.points) || [] })).filter(z => z.points && z.points.length >= 3);
} }
const restrictedConflicts = detectRestrictedZoneIntrusion(routeIds, minMinutes, maxMinutes, getPositionAtMinutesForConflict, restrictedZones, config);
restrictedConflicts.forEach(c => allRaw.push(c));
// ---------- ---------- // 线线worker
if (this.spectrumLedger && this.spectrumLedger.length >= 2) { if (idx % 8 === 7) await new Promise(r => setTimeout(r, 0));
const spectrumConflicts = detectSpectrumConflicts(this.spectrumLedger, config); }
spectrumConflicts.forEach(c => allRaw.push(c));
// ---------- // worker ----------
const platformIcons = (this.$refs.cesiumMap && this.$refs.cesiumMap.getPlatformIconPositions)
? this.$refs.cesiumMap.getPlatformIconPositions()
: [];
const drawingsEntities = (this.$refs.cesiumMap && this.$refs.cesiumMap.getFrontendDrawingsData)
? (((this.$refs.cesiumMap.getFrontendDrawingsData() || {}).entities) || [])
: [];
const workerRaw = await this._runConflictWorkerOnce({
requestId,
routeIds: routeIdsWithTimeline,
minMinutes,
maxMinutes,
config,
routeTimelines: routeIdToTimeline,
platformIcons,
drawingsEntities,
spectrumLedger: this.spectrumLedger || []
});
if (workerRaw && Array.isArray(workerRaw)) {
workerRaw.forEach(c => allRaw.push(c));
} }
// UI
this.conflicts = normalizeConflictList(allRaw, 1); this.conflicts = normalizeConflictList(allRaw, 1);
this.conflictCount = this.conflicts.length; this.conflictCount = this.conflicts.length;
if (this.conflicts.length > 0) { if (this.conflicts.length > 0) {
@ -5343,6 +5381,97 @@ export default {
} else { } else {
this.$message.success('未发现冲突'); this.$message.success('未发现冲突');
} }
} catch (e) {
console.warn('runConflictCheck failed', e);
this.$message.error('冲突检测失败,请稍后重试');
} finally {
// request loading
if (this._isLatestConflictRequestId(requestId)) {
this.conflictCheckRunning = false;
}
}
},
_initConflictWorkerOnce() {
// worker this Vue
this._conflictWorker = new ConflictCheckWorker();
this._conflictWorkerCallbacks = {};
this._conflictRequestSeq = 0;
this._conflictLatestRequestId = null;
this._conflictTimelineCache = {}; // routeId -> { key, data }
this._conflictWorker.onmessage = (evt) => {
const data = evt && evt.data ? evt.data : {};
const { requestId } = data;
const cb = this._conflictWorkerCallbacks && requestId ? this._conflictWorkerCallbacks[requestId] : null;
if (!cb) return;
delete this._conflictWorkerCallbacks[requestId];
cb(data);
};
this._conflictWorker.onerror = (err) => {
console.warn('conflict worker error', err);
};
},
_nextConflictRequestId() {
this._conflictRequestSeq = (this._conflictRequestSeq || 0) + 1;
const id = `conflict_${Date.now()}_${this._conflictRequestSeq}`;
this._conflictLatestRequestId = id;
return id;
},
_isLatestConflictRequestId(requestId) {
return requestId && this._conflictLatestRequestId && requestId === this._conflictLatestRequestId;
},
_runConflictWorkerOnce(payload) {
return new Promise((resolve, reject) => {
if (!this._conflictWorker) return reject(new Error('worker not ready'));
const requestId = payload && payload.requestId;
if (!requestId) return reject(new Error('missing requestId'));
this._conflictWorkerCallbacks[requestId] = (msg) => {
if (!this._isLatestConflictRequestId(requestId)) return resolve(null); //
if (msg && msg.ok) return resolve(msg.conflicts || []);
reject(new Error((msg && msg.error) ? msg.error : 'worker failed'));
};
this._conflictWorker.postMessage(payload);
});
},
_hashWaypointsForConflict(waypoints) {
// hash stringify
// startTimespeed
const wps = Array.isArray(waypoints) ? waypoints : [];
let h = 5381;
const take = (v) => (v == null ? '' : String(v));
const n = wps.length;
const step = n > 60 ? Math.ceil(n / 60) : 1; // 60
for (let i = 0; i < n; i += step) {
const w = wps[i] || {};
const s = `${take(w.lng)},${take(w.lat)},${take(w.alt)},${take(w.startTime)},${take(w.speed)},${take(w.pointType || w.point_type)},${take(w.segmentMode)},${take(w.segmentTargetMinutes || (w.displayStyle && w.displayStyle.segmentTargetMinutes))}`
for (let k = 0; k < s.length; k++) {
h = ((h << 5) + h) ^ s.charCodeAt(k);
}
}
return (h >>> 0).toString(16) + '_' + n;
},
_makeConflictTimelineCacheKey(waypoints, minMinutes, maxMinutes) {
return `${minMinutes}|${maxMinutes}|${this._hashWaypointsForConflict(waypoints)}`;
},
_getConflictTimelineCache(routeId, waypoints, minMinutes, maxMinutes) {
const cache = this._conflictTimelineCache && this._conflictTimelineCache[routeId];
if (!cache) return null;
const key = this._makeConflictTimelineCacheKey(waypoints, minMinutes, maxMinutes);
if (cache.key !== key) return null;
return cache.data || null;
},
_setConflictTimelineCache(routeId, waypoints, minMinutes, maxMinutes, data) {
if (!this._conflictTimelineCache) this._conflictTimelineCache = {};
const key = this._makeConflictTimelineCacheKey(waypoints, minMinutes, maxMinutes);
this._conflictTimelineCache[routeId] = { key, data };
}, },
/** 查看冲突:展开问题航线、显示右侧方案树、定位到冲突位置并跳转时间轴 */ /** 查看冲突:展开问题航线、显示右侧方案树、定位到冲突位置并跳转时间轴 */

Loading…
Cancel
Save