|
|
|
@ -520,15 +520,10 @@ import GanttDrawer from './GanttDrawer.vue'; |
|
|
|
import { |
|
|
|
CONFLICT_TYPE, |
|
|
|
defaultConflictConfig, |
|
|
|
detectTimeWindowOverlap, |
|
|
|
detectTrackSeparation, |
|
|
|
detectPlatformPlacementTooClose, |
|
|
|
detectRestrictedZoneIntrusion, |
|
|
|
parseRestrictedZonesFromDrawings, |
|
|
|
detectSpectrumConflicts, |
|
|
|
createSpectrumLedgerEntry, |
|
|
|
normalizeConflictList |
|
|
|
} from '@/utils/conflictDetection'; |
|
|
|
import ConflictCheckWorker from 'worker-loader!@/workers/conflictCheck.worker.js' |
|
|
|
export default { |
|
|
|
name: 'MissionPlanningView', |
|
|
|
components: { |
|
|
|
@ -747,6 +742,8 @@ export default { |
|
|
|
conflictConfig: { ...defaultConflictConfig }, |
|
|
|
/** 频谱资源台账(用于频谱冲突检测),可后续从接口或界面维护 */ |
|
|
|
spectrumLedger: [], |
|
|
|
/** 冲突检测 worker(非响应式对象,仅占位,实际实例挂在 this._conflictWorker) */ |
|
|
|
_conflictWorkerInited: false, |
|
|
|
|
|
|
|
// 平台数据 |
|
|
|
activePlatformTab: 'air', |
|
|
|
@ -1036,6 +1033,11 @@ export default { |
|
|
|
clearInterval(this.playbackInterval); |
|
|
|
this.playbackInterval = null; |
|
|
|
} |
|
|
|
// 释放冲突检测 worker,避免页面切换后仍占用资源 |
|
|
|
if (this._conflictWorker && this._conflictWorker.terminate) { |
|
|
|
try { this._conflictWorker.terminate(); } catch (_) {} |
|
|
|
this._conflictWorker = null; |
|
|
|
} |
|
|
|
}, |
|
|
|
created() { |
|
|
|
this.currentRoomId = this.$route.query.roomId; |
|
|
|
@ -1292,7 +1294,8 @@ export default { |
|
|
|
if (segmentMode != null) addPayload.segmentMode = segmentMode; |
|
|
|
if (segmentTargetMinutes != null) addPayload.segmentTargetMinutes = segmentTargetMinutes; |
|
|
|
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(); |
|
|
|
let updated = this.routes.find(r => r.id === routeId); |
|
|
|
if (!updated || !updated.waypoints || updated.waypoints.length !== count) { |
|
|
|
@ -1351,7 +1354,7 @@ export default { |
|
|
|
if (isNewlyInserted && segmentTargetMinutes != null) updatePayload.segmentTargetMinutes = segmentTargetMinutes; |
|
|
|
if (isNewlyInserted && segmentTargetSpeed != null) updatePayload.segmentTargetSpeed = segmentTargetSpeed; |
|
|
|
if (isNewlyInserted && segmentMode === 'fixed_speed') updatePayload.startTime = startTime; |
|
|
|
await updateWaypoints(updatePayload); |
|
|
|
await updateWaypoints(updatePayload, roomIdParam); |
|
|
|
} |
|
|
|
await this.getList(); |
|
|
|
updated = this.routes.find(r => r.id === routeId); |
|
|
|
@ -1464,7 +1467,8 @@ export default { |
|
|
|
} else { |
|
|
|
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 || '更新失败'); |
|
|
|
const merged = { ...wp, ...payload }; |
|
|
|
const routeInList = this.routes.find(r => r.id === routeId); |
|
|
|
@ -1571,7 +1575,8 @@ export default { |
|
|
|
seq: wp.seq, |
|
|
|
lat: Number(lat), |
|
|
|
lng: Number(lng), |
|
|
|
alt: Number(alt), |
|
|
|
// 拖拽只允许修改经纬度,高度保持原值(避免 5000 -> 4999.999... 的数值抖动) |
|
|
|
alt: wp.alt, |
|
|
|
speed: wp.speed, |
|
|
|
startTime: (wp.startTime != null && wp.startTime !== '') ? wp.startTime : 'K+00:00:00', |
|
|
|
turnAngle: wp.turnAngle |
|
|
|
@ -1589,8 +1594,9 @@ export default { |
|
|
|
const prevWp = idx > 0 ? waypoints[idx - 1] : null; |
|
|
|
payload.turnRadius = this.$refs.cesiumMap.getTurnRadiusFromPrevSpeed(prevWp, wp.turnAngle); |
|
|
|
} |
|
|
|
const roomIdParam = this.currentRoomId != null ? { roomId: this.currentRoomId } : {}; |
|
|
|
try { |
|
|
|
const response = await updateWaypoints(payload); |
|
|
|
const response = await updateWaypoints(payload, roomIdParam); |
|
|
|
if (response.code === 200) { |
|
|
|
const merged = { ...wp, ...payload }; |
|
|
|
if (idx !== -1) waypoints.splice(idx, 1, merged); |
|
|
|
@ -1614,7 +1620,7 @@ export default { |
|
|
|
const startPayload = { ...merged, startTime: newStartTime }; |
|
|
|
if (merged.segmentMode != null) startPayload.segmentMode = merged.segmentMode; |
|
|
|
try { |
|
|
|
const r2 = await updateWaypoints(startPayload); |
|
|
|
const r2 = await updateWaypoints(startPayload, roomIdParam); |
|
|
|
if (r2.code === 200) { |
|
|
|
Object.assign(merged, { startTime: newStartTime }); |
|
|
|
if (idx !== -1) waypoints.splice(idx, 1, merged); |
|
|
|
@ -1635,7 +1641,7 @@ export default { |
|
|
|
const speedPayload = { ...prev, speed: speedVal }; |
|
|
|
if (prev.segmentMode != null) speedPayload.segmentMode = prev.segmentMode; |
|
|
|
try { |
|
|
|
const r2 = await updateWaypoints(speedPayload); |
|
|
|
const r2 = await updateWaypoints(speedPayload, roomIdParam); |
|
|
|
if (r2.code === 200) { |
|
|
|
Object.assign(prev, { speed: speedVal }); |
|
|
|
const prevIdx = idx - 1; |
|
|
|
@ -1673,7 +1679,7 @@ export default { |
|
|
|
if (merged.pixelSize != null) currPayload.pixelSize = merged.pixelSize; |
|
|
|
if (merged.outlineColor != null) currPayload.outlineColor = merged.outlineColor; |
|
|
|
try { |
|
|
|
const r2 = await updateWaypoints(currPayload); |
|
|
|
const r2 = await updateWaypoints(currPayload, roomIdParam); |
|
|
|
if (r2.code === 200) { |
|
|
|
Object.assign(merged, { speed: speedVal }); |
|
|
|
waypoints.splice(idx, 1, merged); |
|
|
|
@ -2260,7 +2266,7 @@ export default { |
|
|
|
} else { |
|
|
|
payload.turnRadius = 0; |
|
|
|
} |
|
|
|
await updateWaypoints(payload); |
|
|
|
await updateWaypoints(payload, this.currentRoomId != null ? { roomId: this.currentRoomId } : {}); |
|
|
|
} |
|
|
|
const mergedWaypoints = (newRouteData.waypoints || []).map((oldWp) => { |
|
|
|
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.color != null) payload.color = updatedWaypoint.color; |
|
|
|
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) { |
|
|
|
const roomId = this.currentRoomId; |
|
|
|
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.outlineColor != null) prevPayload.outlineColor = prev.outlineColor; |
|
|
|
try { |
|
|
|
const r2 = await updateWaypoints(prevPayload); |
|
|
|
const r2 = await updateWaypoints(prevPayload, roomIdParam); |
|
|
|
if (r2.code === 200) { |
|
|
|
Object.assign(prev, { speed: speedVal }); |
|
|
|
sd.waypoints.splice(index - 1, 1, prev); |
|
|
|
@ -2755,7 +2762,7 @@ export default { |
|
|
|
if (merged.color != null) currPayload.color = merged.color; |
|
|
|
if (merged.outlineColor != null) currPayload.outlineColor = merged.outlineColor; |
|
|
|
try { |
|
|
|
const r2 = await updateWaypoints(currPayload); |
|
|
|
const r2 = await updateWaypoints(currPayload, roomIdParam); |
|
|
|
if (r2.code === 200) { |
|
|
|
Object.assign(merged, { speed: speedVal }); |
|
|
|
sd.waypoints.splice(index, 1, merged); |
|
|
|
@ -2804,7 +2811,7 @@ export default { |
|
|
|
if (wp.pixelSize != null) cascadePayload.pixelSize = wp.pixelSize; |
|
|
|
if (wp.outlineColor != null) cascadePayload.outlineColor = wp.outlineColor; |
|
|
|
try { |
|
|
|
const rCascade = await updateWaypoints(cascadePayload); |
|
|
|
const rCascade = await updateWaypoints(cascadePayload, roomIdParam); |
|
|
|
if (rCascade.code === 200) { |
|
|
|
Object.assign(wp, { startTime: newStartTime, ...(wp.segmentMode === 'fixed_time' && { segmentTargetMinutes: newArrival }) }); |
|
|
|
sd.waypoints.splice(i, 1, wp); |
|
|
|
@ -3969,15 +3976,9 @@ export default { |
|
|
|
// 白板:进入/退出白板模式 |
|
|
|
this.toggleWhiteboardMode(); |
|
|
|
} else if (item.id === 'start') { |
|
|
|
// 冲突:打开弹窗并异步执行检测,避免航线多时主线程阻塞导致页面卡死 |
|
|
|
// 冲突:打开弹窗并执行检测(重计算下沉到 Web Worker,避免航线多时主线程阻塞导致页面卡死) |
|
|
|
this.showConflictDrawer = true; |
|
|
|
this.conflictCheckRunning = true; |
|
|
|
this.$nextTick(() => { |
|
|
|
setTimeout(() => { |
|
|
|
this.runConflictCheck(); |
|
|
|
this.conflictCheckRunning = false; |
|
|
|
}, 0); |
|
|
|
}); |
|
|
|
this.runConflictCheck(); |
|
|
|
} else if (item.id === 'insert') { |
|
|
|
// 如果当前已经是平台标签页,则关闭右侧面板 |
|
|
|
if (this.activeRightTab === 'platform' && !this.isRightPanelHidden) { |
|
|
|
@ -4347,6 +4348,9 @@ export default { |
|
|
|
const path = pathData && pathData.path; |
|
|
|
const segmentEndIndices = pathData && pathData.segmentEndIndices; |
|
|
|
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; |
|
|
|
for (let i = 0; i < points.length - 1; i++) { |
|
|
|
if (skipNextLeg) { |
|
|
|
@ -4489,7 +4493,8 @@ export default { |
|
|
|
const scheduled = points[i + 1].minutes; |
|
|
|
if (travelMin > 0 && scheduled - points[i].minutes > 0) { |
|
|
|
const requiredSpeedKmh = (dist / 1000) / ((scheduled - points[i].minutes) / 60); |
|
|
|
if (actualArrival > scheduled) { |
|
|
|
const needFasterBy = requiredSpeedKmh - speedKmh; |
|
|
|
if (actualArrival > scheduled + timeTolMin && needFasterBy > speedTol) { |
|
|
|
warnings.push( |
|
|
|
`某航段:距离约 ${(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, |
|
|
|
toName: waypoints[i + 1].name, |
|
|
|
requiredSpeedKmh: Math.ceil(requiredSpeedKmh), |
|
|
|
speedKmh |
|
|
|
speedKmh, |
|
|
|
actualArrival, |
|
|
|
scheduled |
|
|
|
}); |
|
|
|
} else if (actualArrival < scheduled - 0.5) { |
|
|
|
} else if (actualArrival < scheduled - timeTolMin) { |
|
|
|
warnings.push('存在航段将提前到达下一航点,平台将在该点等待至计划时间再飞往下一段。'); |
|
|
|
} |
|
|
|
} |
|
|
|
@ -4528,8 +4535,22 @@ export default { |
|
|
|
const travelMin = (dist / 1000) * (60 / speedKmh); |
|
|
|
const actualArrival = effectiveTime[i] + travelMin; |
|
|
|
const scheduled = points[i + 1].minutes; |
|
|
|
if (travelMin > 0 && scheduled - points[i].minutes > 0 && actualArrival < scheduled - 0.5) { |
|
|
|
earlyArrivalLegs.push({ legIndex: i, scheduled, actualArrival, fromName: waypoints[i].name, toName: waypoints[i + 1].name }); |
|
|
|
const earlyMinutes = scheduled - actualArrival; |
|
|
|
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 }; |
|
|
|
@ -4819,6 +4840,7 @@ export default { |
|
|
|
const startTime = this.addHoldForm.startTimeMinutes !== '' && this.addHoldForm.startTimeMinutes != null && !Number.isNaN(Number(this.addHoldForm.startTimeMinutes)) |
|
|
|
? this.minutesToStartTime(Number(this.addHoldForm.startTimeMinutes)) |
|
|
|
: (nextWp.startTime || 'K+01:00'); |
|
|
|
const roomIdParam = this.currentRoomId != null ? { roomId: this.currentRoomId } : {}; |
|
|
|
try { |
|
|
|
await addWaypoints({ |
|
|
|
routeId, |
|
|
|
@ -4831,12 +4853,12 @@ export default { |
|
|
|
startTime, |
|
|
|
pointType: this.addHoldForm.holdType, |
|
|
|
holdParams: JSON.stringify(holdParams) |
|
|
|
}); |
|
|
|
await delWaypoints(nextWp.id); |
|
|
|
}, roomIdParam); |
|
|
|
await delWaypoints(nextWp.id, roomIdParam); |
|
|
|
for (let i = legIndex + 2; i < waypoints.length; i++) { |
|
|
|
const w = waypoints[i]; |
|
|
|
if (w.id) { |
|
|
|
await updateWaypoints({ ...w, seq: baseSeq + (i - legIndex) }); |
|
|
|
await updateWaypoints({ ...w, seq: baseSeq + (i - legIndex) }, roomIdParam); |
|
|
|
} |
|
|
|
} |
|
|
|
this.showAddHoldDialog = false; |
|
|
|
@ -5221,128 +5243,235 @@ export default { |
|
|
|
}, |
|
|
|
|
|
|
|
// 冲突操作:时间冲突(提前到达、无法按时到达、盘旋时间不足、航线时间窗重叠)、空间冲突(航迹间隔、平台摆放、禁限区入侵)、频谱冲突 |
|
|
|
runConflictCheck() { |
|
|
|
const routeIds = this.activeRouteIds && this.activeRouteIds.length > 0 ? this.activeRouteIds : this.routes.map(r => r.id); |
|
|
|
const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); |
|
|
|
const config = this.conflictConfig || defaultConflictConfig; |
|
|
|
const waypointStartTimeToMinutes = (s) => this.waypointStartTimeToMinutes(s); |
|
|
|
async runConflictCheck() { |
|
|
|
// 防止重复点击导致并发:上一轮还在跑就直接再发起一次(旧结果会被 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 config = this.conflictConfig || defaultConflictConfig; |
|
|
|
|
|
|
|
const allRaw = []; |
|
|
|
/** 按航线缓存 timeline(segments+path),供航迹间隔检测复用,避免每 (routeId,t) 重复 buildRouteTimeline 导致航线多时卡死 */ |
|
|
|
const routeIdToTimeline = {}; |
|
|
|
const allRaw = []; |
|
|
|
const routeIdToTimeline = {}; |
|
|
|
const routeIdsWithTimeline = []; |
|
|
|
|
|
|
|
// ---------- 时间冲突:单航线内(提前到达、无法按时到达、盘旋时间不足)---------- |
|
|
|
routeIds.forEach(routeId => { |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route || !route.waypoints || route.waypoints.length < 2) return; |
|
|
|
let pathData = null; |
|
|
|
if (this.$refs.cesiumMap && this.$refs.cesiumMap.getRoutePathWithSegmentIndices) { |
|
|
|
const ret = this.$refs.cesiumMap.getRoutePathWithSegmentIndices(route.waypoints); |
|
|
|
if (ret.path && ret.path.length > 0 && ret.segmentEndIndices) { |
|
|
|
pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} }; |
|
|
|
// ---------- 时间冲突:单航线内(提前到达、无法按时到达、盘旋时间不足)---------- |
|
|
|
// 同时构建 timeline(segments+path),供 worker 空间冲突检测复用。 |
|
|
|
for (let idx = 0; idx < routeIdsAll.length; idx++) { |
|
|
|
const routeId = routeIdsAll[idx]; |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
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; |
|
|
|
if (this.$refs.cesiumMap && this.$refs.cesiumMap.getRoutePathWithSegmentIndices) { |
|
|
|
const ret = this.$refs.cesiumMap.getRoutePathWithSegmentIndices(route.waypoints); |
|
|
|
if (ret.path && ret.path.length > 0 && ret.segmentEndIndices) { |
|
|
|
pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} }; |
|
|
|
} |
|
|
|
} |
|
|
|
const timeline = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData); |
|
|
|
routeIdToTimeline[routeId] = { |
|
|
|
segments: timeline.segments, |
|
|
|
path: pathData && pathData.path ? pathData.path : 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}`; |
|
|
|
(earlyArrivalLegs || []).forEach(leg => { |
|
|
|
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({ |
|
|
|
type: CONFLICT_TYPE.TIME, |
|
|
|
subType: 'early_arrival', |
|
|
|
title: '提前到达', |
|
|
|
routeName, |
|
|
|
routeIds: [routeId], |
|
|
|
fromWaypoint: leg.fromName, |
|
|
|
toWaypoint: leg.toName, |
|
|
|
time: this.minutesToStartTime(leg.actualArrival), |
|
|
|
suggestion: `① 将本段速度降至 ${speedStr} ② 若下一航点为盘旋点,可盘旋等待 ${earlyStr} ③ 将下一航点相对K时调至 ${kTimeStr} 或更晚`, |
|
|
|
severity: 'high' |
|
|
|
}); |
|
|
|
}); |
|
|
|
(lateArrivalLegs || []).forEach(leg => { |
|
|
|
const kTimeStr = leg.actualArrival != null && Number.isFinite(leg.actualArrival) ? this.minutesToStartTime(leg.actualArrival) : ''; |
|
|
|
const part2 = kTimeStr ? ` ② 或将下一航点相对K时调至 ${kTimeStr} 或更晚` : ' ② 或将下一航点相对K时调晚'; |
|
|
|
allRaw.push({ |
|
|
|
type: CONFLICT_TYPE.TIME, |
|
|
|
subType: 'late_arrival', |
|
|
|
title: '无法按时到达', |
|
|
|
routeName, |
|
|
|
routeIds: [routeId], |
|
|
|
fromWaypoint: leg.fromName, |
|
|
|
toWaypoint: leg.toName, |
|
|
|
suggestion: `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h${part2} ③ 调整上游航段速度或时间`, |
|
|
|
severity: 'high' |
|
|
|
}); |
|
|
|
}); |
|
|
|
(holdDelayConflicts || []).forEach(conf => { |
|
|
|
allRaw.push({ |
|
|
|
type: CONFLICT_TYPE.TIME, |
|
|
|
subType: 'hold_delay', |
|
|
|
title: '盘旋时间不足', |
|
|
|
routeName, |
|
|
|
routeIds: [routeId], |
|
|
|
fromWaypoint: conf.fromName, |
|
|
|
toWaypoint: conf.toName, |
|
|
|
time: this.minutesToStartTime(conf.setExitTime), |
|
|
|
position: conf.holdCenter ? `经度 ${conf.holdCenter.lng.toFixed(5)}°, 纬度 ${conf.holdCenter.lat.toFixed(5)}°` : undefined, |
|
|
|
suggestion: `实际切出将延迟 ${conf.delaySeconds} 秒。① 延长该盘旋点相对K时 ② 定时盘旋调转弯半径,非定时调上一航点速度或本点相对K时 ③ 微调上下游航点相对K时`, |
|
|
|
severity: 'high', |
|
|
|
holdCenter: conf.holdCenter, |
|
|
|
positionLng: conf.holdCenter && conf.holdCenter.lng, |
|
|
|
positionLat: conf.holdCenter && conf.holdCenter.lat, |
|
|
|
positionAlt: conf.holdCenter && conf.holdCenter.alt |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 航线很多时让出一次主线程,避免长任务堆积(worker 会承担主要重循环) |
|
|
|
if (idx % 8 === 7) await new Promise(r => setTimeout(r, 0)); |
|
|
|
} |
|
|
|
const timeline = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData); |
|
|
|
const { earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts } = timeline; |
|
|
|
routeIdToTimeline[routeId] = { |
|
|
|
segments: timeline.segments, |
|
|
|
path: pathData && pathData.path ? pathData.path : null, |
|
|
|
segmentEndIndices: pathData && pathData.segmentEndIndices ? pathData.segmentEndIndices : null |
|
|
|
}; |
|
|
|
const routeName = route.name || `航线${route.id}`; |
|
|
|
(earlyArrivalLegs || []).forEach(leg => { |
|
|
|
// 提前到达:相对K时=离开该点时间;到达早于该时间时可降速/盘旋等待/调晚下一航点相对K时;受定速/定时约束时只调未锁定参数 |
|
|
|
allRaw.push({ |
|
|
|
type: CONFLICT_TYPE.TIME, |
|
|
|
subType: 'early_arrival', |
|
|
|
title: '提前到达', |
|
|
|
routeName, |
|
|
|
routeIds: [routeId], |
|
|
|
fromWaypoint: leg.fromName, |
|
|
|
toWaypoint: leg.toName, |
|
|
|
time: this.minutesToStartTime(leg.actualArrival), |
|
|
|
suggestion: '① 降低本段速度 ② 下一航点为盘旋点时依靠盘旋等待 ③ 将下一航点相对K时调晚', |
|
|
|
severity: 'high' |
|
|
|
}); |
|
|
|
}); |
|
|
|
(lateArrivalLegs || []).forEach(leg => { |
|
|
|
// 无法按时到达:需在下一航点相对K时前到达;可提速或调早下一航点相对K时;受定速/定时约束时只调未锁定参数 |
|
|
|
allRaw.push({ |
|
|
|
type: CONFLICT_TYPE.TIME, |
|
|
|
subType: 'late_arrival', |
|
|
|
title: '无法按时到达', |
|
|
|
routeName, |
|
|
|
routeIds: [routeId], |
|
|
|
fromWaypoint: leg.fromName, |
|
|
|
toWaypoint: leg.toName, |
|
|
|
suggestion: `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h ② 将下一航点相对K时调早 ③ 调整上游航段速度或时间`, |
|
|
|
severity: 'high' |
|
|
|
}); |
|
|
|
}); |
|
|
|
(holdDelayConflicts || []).forEach(conf => { |
|
|
|
// 盘旋时间不足:项目不控制盘旋圈数。定时盘旋时盘旋时间=该点相对K时-到达时间、速度由半径与时间反算;非定时时速度继承上一航点。只能通过延长相对K时/调半径或上一航点速度等改善 |
|
|
|
allRaw.push({ |
|
|
|
type: CONFLICT_TYPE.TIME, |
|
|
|
subType: 'hold_delay', |
|
|
|
title: '盘旋时间不足', |
|
|
|
routeName, |
|
|
|
routeIds: [routeId], |
|
|
|
fromWaypoint: conf.fromName, |
|
|
|
toWaypoint: conf.toName, |
|
|
|
time: this.minutesToStartTime(conf.setExitTime), |
|
|
|
position: conf.holdCenter ? `经度 ${conf.holdCenter.lng.toFixed(5)}°, 纬度 ${conf.holdCenter.lat.toFixed(5)}°` : undefined, |
|
|
|
suggestion: `实际切出将延迟 ${conf.delaySeconds} 秒。① 延长该盘旋点相对K时 ② 定时盘旋调转弯半径,非定时调上一航点速度或本点相对K时 ③ 微调上下游航点相对K时`, |
|
|
|
severity: 'high', |
|
|
|
holdCenter: conf.holdCenter, |
|
|
|
positionLng: conf.holdCenter && conf.holdCenter.lng, |
|
|
|
positionLat: conf.holdCenter && conf.holdCenter.lat, |
|
|
|
positionAlt: conf.holdCenter && conf.holdCenter.alt |
|
|
|
}); |
|
|
|
|
|
|
|
// ---------- 空间/禁限区/频谱冲突:交给 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 || [] |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
// 时间窗重叠:仅“时间段有交集”不算冲突(不同地点飞机可同时起飞)。后续若有跑道/频段等资源绑定,再按“同一资源占用时间重叠”报冲突。 |
|
|
|
// 故此处不再调用 detectTimeWindowOverlap。 |
|
|
|
if (workerRaw && Array.isArray(workerRaw)) { |
|
|
|
workerRaw.forEach(c => allRaw.push(c)); |
|
|
|
} |
|
|
|
|
|
|
|
// 统一规范化,供 UI 展示 |
|
|
|
this.conflicts = normalizeConflictList(allRaw, 1); |
|
|
|
this.conflictCount = this.conflicts.length; |
|
|
|
if (this.conflicts.length > 0) { |
|
|
|
this.$message.warning(`检测到 ${this.conflicts.length} 处冲突`); |
|
|
|
} else { |
|
|
|
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 } |
|
|
|
|
|
|
|
// ---------- 空间冲突:航迹最小间隔(使用缓存的 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; |
|
|
|
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); |
|
|
|
}; |
|
|
|
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)); |
|
|
|
|
|
|
|
// ---------- 频谱冲突(台账内两两判定)---------- |
|
|
|
if (this.spectrumLedger && this.spectrumLedger.length >= 2) { |
|
|
|
const spectrumConflicts = detectSpectrumConflicts(this.spectrumLedger, config); |
|
|
|
spectrumConflicts.forEach(c => allRaw.push(c)); |
|
|
|
} |
|
|
|
|
|
|
|
this.conflicts = normalizeConflictList(allRaw, 1); |
|
|
|
this.conflictCount = this.conflicts.length; |
|
|
|
if (this.conflicts.length > 0) { |
|
|
|
this.$message.warning(`检测到 ${this.conflicts.length} 处冲突`); |
|
|
|
} else { |
|
|
|
this.$message.success('未发现冲突'); |
|
|
|
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(大数据会很慢) |
|
|
|
// 只取影响时间轴的关键字段:经纬高、startTime、speed、盘旋属性 |
|
|
|
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 }; |
|
|
|
}, |
|
|
|
|
|
|
|
/** 查看冲突:展开问题航线、显示右侧方案树、定位到冲突位置并跳转时间轴 */ |
|
|
|
|