From 6bdf9f253046b0e2a3e544691b246881a2e33174 Mon Sep 17 00:00:00 2001 From: menghao <1584479611@qq.com> Date: Wed, 18 Mar 2026 16:00:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=B2=E7=AA=811.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-ui/package.json | 3 +- ruoyi-ui/src/utils/conflictDetection.js | 4 + ruoyi-ui/src/views/childRoom/index.vue | 425 +++++++++++++++++++++----------- 3 files changed, 283 insertions(+), 149 deletions(-) diff --git a/ruoyi-ui/package.json b/ruoyi-ui/package.json index 2613168..30967d5 100644 --- a/ruoyi-ui/package.json +++ b/ruoyi-ui/package.json @@ -71,7 +71,8 @@ "sass-loader": "10.1.1", "script-ext-html-webpack-plugin": "2.1.5", "svg-sprite-loader": "5.1.1", - "vue-template-compiler": "2.6.12" + "vue-template-compiler": "2.6.12", + "worker-loader": "^3.0.8" }, "engines": { "node": ">=8.9", diff --git a/ruoyi-ui/src/utils/conflictDetection.js b/ruoyi-ui/src/utils/conflictDetection.js index b5244cb..289e3fd 100644 --- a/ruoyi-ui/src/utils/conflictDetection.js +++ b/ruoyi-ui/src/utils/conflictDetection.js @@ -17,6 +17,10 @@ export const defaultConflictConfig = { // 时间 timeWindowOverlapMinutes: 0, // 时间窗重叠判定:两航线时间窗重叠即报(0=任意重叠) resourceBufferMinutes: 0, // 资源占用前后缓冲(分钟) + /** 到达时间容差(秒):早于或晚于计划在此秒数内不报冲突,超出才报 */ + timeToleranceSeconds: 5, + /** 速度容差(km/h):与所需/建议速度差在此范围内不报冲突,超出才报 */ + speedToleranceKmh: 20, // 空间 minTrackSeparationMeters: 5000, // 推演中两平台航迹最小间隔(米) minPlatformPlacementMeters: 3000, // 摆放平台图标最小间距(米) diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index 04185c8..9830adc 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -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 }; }, /** 查看冲突:展开问题航线、显示右侧方案树、定位到冲突位置并跳转时间轴 */