|
|
|
@ -194,8 +194,7 @@ |
|
|
|
:route-locked-by="routeLockedBy" |
|
|
|
:current-user-id="currentUserId" |
|
|
|
:selected-route-details="selectedRouteDetails" |
|
|
|
:conflicts="conflicts" |
|
|
|
:conflict-count="conflictCount" |
|
|
|
:expand-route-ids="expandRouteIdsForPanel" |
|
|
|
:air-platforms="airPlatforms" |
|
|
|
:sea-platforms="seaPlatforms" |
|
|
|
:ground-platforms="groundPlatforms" |
|
|
|
@ -213,9 +212,6 @@ |
|
|
|
@cancel-route="cancelRoute" |
|
|
|
@toggle-route-visibility="(route, opts) => toggleRouteVisibility(route, opts)" |
|
|
|
@toggle-route-lock="handleToggleRouteLockFromPanel" |
|
|
|
@view-conflict="viewConflict" |
|
|
|
@resolve-conflict="resolveConflict" |
|
|
|
@run-conflict-check="runConflictCheck" |
|
|
|
@open-platform-dialog="openPlatformDialog" |
|
|
|
@delete-platform="handleDeletePlatform" |
|
|
|
@open-import-dialog="showImportDialog = true" |
|
|
|
@ -338,6 +334,7 @@ |
|
|
|
v-model="showRouteDialog" |
|
|
|
:route="selectedRoute" |
|
|
|
:room-id="currentRoomId" |
|
|
|
:initial-tab="routeEditInitialTab" |
|
|
|
@save="updateRoute" |
|
|
|
/> |
|
|
|
|
|
|
|
@ -415,6 +412,15 @@ |
|
|
|
:room-id="currentRoomId" |
|
|
|
/> |
|
|
|
|
|
|
|
<!-- 冲突列表弹窗:可拖动、可调整大小、分页,点击左侧冲突按钮即打开并自动检测 --> |
|
|
|
<conflict-drawer |
|
|
|
v-if="!screenshotMode" |
|
|
|
:visible.sync="showConflictDrawer" |
|
|
|
:conflicts="conflicts" |
|
|
|
@view-conflict="viewConflict" |
|
|
|
@resolve-conflict="resolveConflict" |
|
|
|
/> |
|
|
|
|
|
|
|
<!-- 白板面板(底部) --> |
|
|
|
<whiteboard-panel |
|
|
|
v-show="showWhiteboardPanel && !screenshotMode" |
|
|
|
@ -496,6 +502,7 @@ import RightPanel from './RightPanel' |
|
|
|
import BottomLeftPanel from './BottomLeftPanel' |
|
|
|
import TopHeader from './TopHeader' |
|
|
|
import FourTPanel from './FourTPanel' |
|
|
|
import ConflictDrawer from './ConflictDrawer' |
|
|
|
import WhiteboardPanel from './WhiteboardPanel' |
|
|
|
import { createRoomWebSocket } from '@/utils/websocket'; |
|
|
|
import { listScenario, addScenario, delScenario } from "@/api/system/scenario"; |
|
|
|
@ -510,6 +517,18 @@ import PlatformImportDialog from "@/views/dialogs/PlatformImportDialog.vue"; |
|
|
|
import ExportRoutesDialog from "@/views/dialogs/ExportRoutesDialog.vue"; |
|
|
|
import ImportRoutesDialog from "@/views/dialogs/ImportRoutesDialog.vue"; |
|
|
|
import GanttDrawer from './GanttDrawer.vue'; |
|
|
|
import { |
|
|
|
CONFLICT_TYPE, |
|
|
|
defaultConflictConfig, |
|
|
|
detectTimeWindowOverlap, |
|
|
|
detectTrackSeparation, |
|
|
|
detectPlatformPlacementTooClose, |
|
|
|
detectRestrictedZoneIntrusion, |
|
|
|
parseRestrictedZonesFromDrawings, |
|
|
|
detectSpectrumConflicts, |
|
|
|
createSpectrumLedgerEntry, |
|
|
|
normalizeConflictList |
|
|
|
} from '@/utils/conflictDetection'; |
|
|
|
export default { |
|
|
|
name: 'MissionPlanningView', |
|
|
|
components: { |
|
|
|
@ -531,6 +550,7 @@ export default { |
|
|
|
BottomLeftPanel, |
|
|
|
TopHeader, |
|
|
|
FourTPanel, |
|
|
|
ConflictDrawer, |
|
|
|
WhiteboardPanel |
|
|
|
}, |
|
|
|
data() { |
|
|
|
@ -685,6 +705,12 @@ export default { |
|
|
|
showKTimePopup: false, |
|
|
|
// 4T悬浮窗显示控制(仅点击4T图标时打开/关闭) |
|
|
|
show4TPanel: false, |
|
|
|
/** 冲突列表弹窗(点击左侧冲突按钮即打开并自动执行检测) */ |
|
|
|
showConflictDrawer: false, |
|
|
|
/** 定位冲突时让右侧面板展开的航线 ID 列表 */ |
|
|
|
expandRouteIdsForPanel: [], |
|
|
|
/** 打开航线编辑弹窗时默认选中的 tab(解决冲突时传 'waypoints' 直接打开航点列表) */ |
|
|
|
routeEditInitialTab: null, |
|
|
|
|
|
|
|
// 白板模式 |
|
|
|
showWhiteboardPanel: false, |
|
|
|
@ -715,6 +741,10 @@ export default { |
|
|
|
// 冲突数据(由 runConflictCheck 根据当前航线与时间轴计算真实问题) |
|
|
|
conflictCount: 0, |
|
|
|
conflicts: [], |
|
|
|
/** 冲突检测配置(时间缓冲、航迹最小间隔、平台摆放最小距离、禁限区关键词、频谱邻频保护等) */ |
|
|
|
conflictConfig: { ...defaultConflictConfig }, |
|
|
|
/** 频谱资源台账(用于频谱冲突检测),可后续从接口或界面维护 */ |
|
|
|
spectrumLedger: [], |
|
|
|
|
|
|
|
// 平台数据 |
|
|
|
activePlatformTab: 'air', |
|
|
|
@ -2075,6 +2105,7 @@ export default { |
|
|
|
// 航线编辑弹窗相关方法 |
|
|
|
openRouteDialog(route) { |
|
|
|
this.selectedRoute = route; |
|
|
|
this.routeEditInitialTab = null; |
|
|
|
this.showRouteDialog = true; |
|
|
|
// 进入编辑即锁定该航线,广播给其他成员 |
|
|
|
if (this.wsConnection && this.wsConnection.sendObjectEditLock && route && route.id != null) { |
|
|
|
@ -3777,13 +3808,9 @@ export default { |
|
|
|
// 白板:进入/退出白板模式 |
|
|
|
this.toggleWhiteboardMode(); |
|
|
|
} else if (item.id === 'start') { |
|
|
|
// 如果当前已经是冲突标签页,则关闭右侧面板 |
|
|
|
if (this.activeRightTab === 'conflict' && !this.isRightPanelHidden) { |
|
|
|
this.isRightPanelHidden = true; |
|
|
|
} else { |
|
|
|
this.activeRightTab = 'conflict'; |
|
|
|
this.isRightPanelHidden = false; |
|
|
|
} |
|
|
|
// 冲突:打开可拖动冲突列表弹窗,并立即执行检测(无需再点“重新检测”) |
|
|
|
this.showConflictDrawer = true; |
|
|
|
this.$nextTick(() => { this.runConflictCheck(); }); |
|
|
|
} else if (item.id === 'insert') { |
|
|
|
// 如果当前已经是平台标签页,则关闭右侧面板 |
|
|
|
if (this.activeRightTab === 'platform' && !this.isRightPanelHidden) { |
|
|
|
@ -4113,9 +4140,9 @@ export default { |
|
|
|
/** |
|
|
|
* 按速度与计划时间构建航线时间轴:含飞行段、盘旋段与“提前到达则等待”的等待段。 |
|
|
|
* pathData 可选:{ path, segmentEndIndices, holdArcRanges },由 getRoutePathWithSegmentIndices 提供,用于输出 hold 段。 |
|
|
|
* holdRadiusByLegIndex 可选:{ [legIndex]: number },为盘旋段指定半径(用于推演时落点精准在切点)。 |
|
|
|
* 圆形盘旋半径由速度+坡度公式固定计算,盘旋时间靠多转圈数解决,不反算半径。 |
|
|
|
*/ |
|
|
|
buildRouteTimeline(waypoints, globalMin, globalMax, pathData, holdRadiusByLegIndex) { |
|
|
|
buildRouteTimeline(waypoints, globalMin, globalMax, pathData) { |
|
|
|
const warnings = []; |
|
|
|
if (!waypoints || waypoints.length === 0) return { segments: [], warnings }; |
|
|
|
const points = waypoints.map((wp, idx) => ({ |
|
|
|
@ -4153,7 +4180,12 @@ export default { |
|
|
|
const path = pathData && pathData.path; |
|
|
|
const segmentEndIndices = pathData && pathData.segmentEndIndices; |
|
|
|
const holdArcRanges = pathData && pathData.holdArcRanges || {}; |
|
|
|
let skipNextLeg = false; |
|
|
|
for (let i = 0; i < points.length - 1; i++) { |
|
|
|
if (skipNextLeg) { |
|
|
|
skipNextLeg = false; |
|
|
|
continue; |
|
|
|
} |
|
|
|
if (this.isHoldWaypoint(waypoints[i + 1]) && path && segmentEndIndices && holdArcRanges[i]) { |
|
|
|
const range = holdArcRanges[i]; |
|
|
|
const startIdx = i === 0 ? 0 : segmentEndIndices[i - 1] + 1; |
|
|
|
@ -4183,25 +4215,35 @@ export default { |
|
|
|
} |
|
|
|
const holdEndTime = points[i + 1].minutes; // 用户设定的切出时间(如 K+10) |
|
|
|
const exitPos = holdPathSlice.length ? holdPathSlice[holdPathSlice.length - 1] : (toEntrySlice.length ? toEntrySlice[toEntrySlice.length - 1] : { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt }); |
|
|
|
let loopEndIdx = 1; |
|
|
|
for (let k = 1; k < Math.min(holdPathSlice.length, 200); k++) { |
|
|
|
if (this.segmentDistance(holdPathSlice[0], holdPathSlice[k]) < 80) { loopEndIdx = k; break; } |
|
|
|
let loopEndIdx; |
|
|
|
if (range.loopEndIndex != null) { |
|
|
|
loopEndIdx = range.loopEndIndex - range.start; |
|
|
|
} else { |
|
|
|
const minSearchIdx = Math.max(2, Math.floor(holdPathSlice.length * 0.33)); |
|
|
|
loopEndIdx = holdPathSlice.length - 1; |
|
|
|
for (let k = minSearchIdx; k < holdPathSlice.length; k++) { |
|
|
|
if (this.segmentDistance(holdPathSlice[0], holdPathSlice[k]) < 80) { loopEndIdx = k; break; } |
|
|
|
} |
|
|
|
} |
|
|
|
const holdClosedLoopPath = holdPathSlice.slice(0, loopEndIdx + 1); |
|
|
|
if (loopEndIdx < 1) loopEndIdx = 1; |
|
|
|
if (loopEndIdx >= holdPathSlice.length) loopEndIdx = holdPathSlice.length - 1; |
|
|
|
const holdClosedLoopRaw = holdPathSlice.slice(0, loopEndIdx + 1); |
|
|
|
const holdClosedLoopPath = holdClosedLoopRaw.length >= 2 |
|
|
|
? [...holdClosedLoopRaw.slice(0, -1), { ...holdClosedLoopRaw[0] }] |
|
|
|
: holdClosedLoopRaw; |
|
|
|
const holdLoopLength = this.pathSliceDistance(holdClosedLoopPath) || 1; |
|
|
|
// 出口必须在整条盘旋路径上找,不能只在“整圈”段内找,否则会误把圈上某点当出口导致飞半圈就停或折回 |
|
|
|
let exitIdxOnLoop = holdPathSlice.length - 1; |
|
|
|
let minD = 1e9; |
|
|
|
for (let k = 0; k < holdPathSlice.length; k++) { |
|
|
|
const d = this.segmentDistance(holdPathSlice[k], exitPos); |
|
|
|
if (d < minD) { minD = d; exitIdxOnLoop = k; } |
|
|
|
} |
|
|
|
const holdExitDistanceOnLoop = this.pathSliceDistance(holdPathSlice.slice(0, exitIdxOnLoop + 1)); |
|
|
|
const holdEntryToExitRaw = holdPathSlice.slice(loopEndIdx); |
|
|
|
const holdEntryToExitSlice = holdEntryToExitRaw.length >= 2 |
|
|
|
? [{ ...holdClosedLoopPath[0] }, ...holdEntryToExitRaw.slice(1)] |
|
|
|
: holdEntryToExitRaw; |
|
|
|
const holdExitDistanceOnLoop = this.pathSliceDistance(holdEntryToExitSlice); |
|
|
|
const holdSpeedKmh = points[i + 1].speed || 800; |
|
|
|
const HOLD_SPEED_KMH = 800; |
|
|
|
const speedMpMin = (HOLD_SPEED_KMH * 1000) / 60; |
|
|
|
const requiredDistAtK10 = (holdEndTime - arrivalEntry) * speedMpMin; |
|
|
|
let n = Math.ceil((requiredDistAtK10 - holdExitDistanceOnLoop) / holdLoopLength); |
|
|
|
const rawLoops = (requiredDistAtK10 - holdExitDistanceOnLoop) / holdLoopLength; |
|
|
|
let n = Math.ceil(rawLoops - 1e-9); |
|
|
|
if (n < 0 || !Number.isFinite(n)) n = 0; |
|
|
|
const segmentEndTime = arrivalEntry + (holdExitDistanceOnLoop + n * holdLoopLength) / speedMpMin; |
|
|
|
if (segmentEndTime > holdEndTime) { |
|
|
|
@ -4222,22 +4264,22 @@ export default { |
|
|
|
const distExitToNext = this.pathSliceDistance(toNextSlice); |
|
|
|
const travelExitMin = (distExitToNext / 1000) * (60 / holdSpeedKmh); |
|
|
|
const arrivalNext = segmentEndTime + travelExitMin; |
|
|
|
effectiveTime[i + 1] = holdEndTime; |
|
|
|
effectiveTime[i + 1] = segmentEndTime; |
|
|
|
if (i + 2 < points.length) effectiveTime[i + 2] = arrivalNext; |
|
|
|
const posCur = { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt }; |
|
|
|
const entryPos = toEntrySlice.length ? toEntrySlice[toEntrySlice.length - 1] : posCur; |
|
|
|
const holdWp = waypoints[i + 1]; |
|
|
|
const holdParams = this.parseHoldParams(holdWp); |
|
|
|
const holdCenter = holdWp ? { lng: parseFloat(holdWp.lng), lat: parseFloat(holdWp.lat), alt: Number(holdWp.alt) || 0 } : null; |
|
|
|
const overrideR = holdRadiusByLegIndex && holdRadiusByLegIndex[i] != null ? holdRadiusByLegIndex[i] : null; |
|
|
|
const holdRadius = (overrideR != null && Number.isFinite(overrideR)) ? overrideR : (holdParams && holdParams.radius != null ? holdParams.radius : null); |
|
|
|
const computedR = this.$refs.cesiumMap ? this.$refs.cesiumMap.getWaypointRadius(holdWp) : null; |
|
|
|
const holdRadius = (computedR != null && computedR > 0) ? computedR : 500; |
|
|
|
const holdClockwise = holdParams && holdParams.clockwise !== false; |
|
|
|
const holdCircumference = holdRadius != null ? 2 * Math.PI * holdRadius : null; |
|
|
|
const holdEntryAngle = holdCenter && entryPos && holdRadius != null |
|
|
|
? this.angleFromCenterToPoint(holdCenter.lng, holdCenter.lat, entryPos.lng, entryPos.lat) |
|
|
|
: null; |
|
|
|
segments.push({ startTime: effectiveTime[i], endTime: arrivalEntry, startPos: posCur, endPos: entryPos, type: 'fly', legIndex: i, pathSlice: toEntrySlice, speedKmh: speedKmhForLeg }); |
|
|
|
const holdEntryToExitPath = holdClosedLoopPath.slice(0, exitIdxOnLoop + 1); |
|
|
|
const holdEntryToExitPath = holdEntryToExitSlice; |
|
|
|
segments.push({ |
|
|
|
startTime: arrivalEntry, |
|
|
|
endTime: segmentEndTime, |
|
|
|
@ -4259,8 +4301,18 @@ export default { |
|
|
|
holdClockwise, |
|
|
|
holdEntryAngle |
|
|
|
}); |
|
|
|
segments.push({ startTime: segmentEndTime, endTime: arrivalNext, startPos: exitPos, endPos: toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos, type: 'fly', legIndex: i, pathSlice: toNextSlice, speedKmh: holdSpeedKmh }); |
|
|
|
continue; // 不 i++,让下次迭代处理下一航段(含连续盘旋点如 WP2→WP3 均为盘旋) |
|
|
|
// 出口→下一航点的 fly 段 |
|
|
|
const exitEndPos = toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos; |
|
|
|
// 如果下一个航点(WP_{i+2})也是盘旋,不创建 fly 段(让下一次循环处理), |
|
|
|
// 只更新 effectiveTime 使下一次循环的起始时间正确 |
|
|
|
if (i + 2 < points.length && this.isHoldWaypoint(waypoints[i + 2])) { |
|
|
|
segments.push({ startTime: segmentEndTime, endTime: arrivalNext, startPos: exitPos, endPos: exitEndPos, type: 'fly', legIndex: i + 1, pathSlice: toNextSlice, speedKmh: holdSpeedKmh }); |
|
|
|
} else { |
|
|
|
segments.push({ startTime: segmentEndTime, endTime: arrivalNext, startPos: exitPos, endPos: exitEndPos, type: 'fly', legIndex: i + 1, pathSlice: toNextSlice, speedKmh: holdSpeedKmh }); |
|
|
|
// 下一航点不是盘旋,fly 段已覆盖 leg i+1,跳过 |
|
|
|
skipNextLeg = true; |
|
|
|
} |
|
|
|
continue; |
|
|
|
} |
|
|
|
const dist = this.segmentDistance(points[i], points[i + 1]); |
|
|
|
const speedKmh = points[i].speed || 800; |
|
|
|
@ -4287,7 +4339,15 @@ export default { |
|
|
|
effectiveTime[i + 1] = Math.max(actualArrival, scheduled); |
|
|
|
const posCur = { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt }; |
|
|
|
const posNext = { lng: points[i + 1].lng, lat: points[i + 1].lat, alt: points[i + 1].alt }; |
|
|
|
segments.push({ startTime: effectiveTime[i], endTime: actualArrival, startPos: posCur, endPos: posNext, type: 'fly', legIndex: i, speedKmh: speedKmh }); |
|
|
|
let flyPathSlice = null; |
|
|
|
if (path && segmentEndIndices) { |
|
|
|
const startIdx = i === 0 ? 0 : (segmentEndIndices[i - 1] != null ? segmentEndIndices[i - 1] : 0); |
|
|
|
const endIdx = segmentEndIndices[i]; |
|
|
|
if (endIdx != null && endIdx >= startIdx) { |
|
|
|
flyPathSlice = path.slice(startIdx, endIdx + 1); |
|
|
|
} |
|
|
|
} |
|
|
|
segments.push({ startTime: effectiveTime[i], endTime: actualArrival, startPos: posCur, endPos: posNext, type: 'fly', legIndex: i, speedKmh: speedKmh, pathSlice: flyPathSlice }); |
|
|
|
if (actualArrival < effectiveTime[i + 1]) { |
|
|
|
segments.push({ startTime: actualArrival, endTime: effectiveTime[i + 1], startPos: posNext, endPos: posNext, type: 'wait', legIndex: i }); |
|
|
|
} |
|
|
|
@ -4402,24 +4462,27 @@ export default { |
|
|
|
const cesiumMap = this.$refs.cesiumMap; |
|
|
|
let pathData = null; |
|
|
|
if (cesiumMap && cesiumMap.getRoutePathWithSegmentIndices) { |
|
|
|
const cachedRadii = (routeId != null && cesiumMap._routeHoldRadiiByRoute && cesiumMap._routeHoldRadiiByRoute[routeId]) ? cesiumMap._routeHoldRadiiByRoute[routeId] : {}; |
|
|
|
const cachedEllipse = (routeId != null && cesiumMap._routeHoldEllipseParamsByRoute && cesiumMap._routeHoldEllipseParamsByRoute[routeId]) ? cesiumMap._routeHoldEllipseParamsByRoute[routeId] : {}; |
|
|
|
const opts = (Object.keys(cachedRadii).length > 0 || Object.keys(cachedEllipse).length > 0) ? { holdRadiusByLegIndex: cachedRadii, holdEllipseParamsByLegIndex: cachedEllipse } : {}; |
|
|
|
const opts = Object.keys(cachedEllipse).length > 0 ? { holdEllipseParamsByLegIndex: cachedEllipse } : {}; |
|
|
|
const ret = cesiumMap.getRoutePathWithSegmentIndices(waypoints, opts); |
|
|
|
if (ret.path && ret.path.length > 0 && ret.segmentEndIndices && ret.segmentEndIndices.length > 0) { |
|
|
|
pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} }; |
|
|
|
} |
|
|
|
} |
|
|
|
let { segments, warnings, earlyArrivalLegs } = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData); |
|
|
|
const holdRadiusByLegIndex = {}; |
|
|
|
// 圆形盘旋:半径固定由速度+坡度公式计算,盘旋时间靠多转圈数解决,不反算半径。 |
|
|
|
// 椭圆/跑道形盘旋:通过反算椭圆参数(semiMajor/semiMinor)来匹配盘旋时间。 |
|
|
|
const holdEllipseParamsByLegIndex = {}; |
|
|
|
if (cesiumMap && segments && pathData) { |
|
|
|
for (let idx = 0; idx < segments.length; idx++) { |
|
|
|
const s = segments[idx]; |
|
|
|
if (s.type !== 'hold' || s.holdCenter == null) continue; |
|
|
|
const i = s.legIndex; |
|
|
|
const holdEndTime = s.holdEndTime != null ? s.holdEndTime : this.waypointStartTimeToMinutesDecimal(waypoints[i + 1]?.startTime); |
|
|
|
const holdWp = waypoints[i + 1]; |
|
|
|
if (!holdWp) continue; |
|
|
|
const isHoldEllipse = (holdWp.pointType || holdWp.point_type) === 'hold_ellipse'; |
|
|
|
if (!isHoldEllipse || !cesiumMap.computeEllipseParamsForDuration) continue; |
|
|
|
const holdEndTime = s.holdEndTime != null ? s.holdEndTime : this.waypointStartTimeToMinutesDecimal(holdWp?.startTime); |
|
|
|
const segTarget = holdWp && (holdWp.segmentTargetMinutes ?? holdWp.displayStyle?.segmentTargetMinutes); |
|
|
|
const arrivalAtHold = (holdWp && holdWp.segmentMode === 'fixed_time' && segTarget != null && segTarget !== '') |
|
|
|
? Number(segTarget) : s.startTime; |
|
|
|
@ -4428,123 +4491,36 @@ export default { |
|
|
|
const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000; |
|
|
|
const prevWp = waypoints[i]; |
|
|
|
const nextWp = (i + 2) < waypoints.length ? waypoints[i + 2] : holdWp; |
|
|
|
if (!prevWp || !holdWp) continue; |
|
|
|
if (!prevWp) continue; |
|
|
|
const centerCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(holdWp.lng), parseFloat(holdWp.lat), Number(holdWp.alt) || 0); |
|
|
|
const prevCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(prevWp.lng), parseFloat(prevWp.lat), Number(prevWp.alt) || 0); |
|
|
|
const nextCartesian = nextWp ? Cesium.Cartesian3.fromDegrees(parseFloat(nextWp.lng), parseFloat(nextWp.lat), Number(nextWp.alt) || 0) : centerCartesian; |
|
|
|
const clockwise = s.holdClockwise !== false; |
|
|
|
const isHoldEllipse = waypoints[i + 1] && (waypoints[i + 1].pointType || waypoints[i + 1].point_type) === 'hold_ellipse'; |
|
|
|
const isEllipse = isHoldEllipse || s.holdRadius == null; |
|
|
|
if (isEllipse && !isHoldEllipse && cesiumMap.computeEllipseParamsForDuration) { |
|
|
|
const holdParams = this.parseHoldParams(holdWp); |
|
|
|
const headingDeg = holdParams && holdParams.headingDeg != null ? holdParams.headingDeg : 0; |
|
|
|
const a0 = holdParams && (holdParams.semiMajor != null || holdParams.semiMajorAxis != null) ? (holdParams.semiMajor ?? holdParams.semiMajorAxis) : 500; |
|
|
|
const b0 = holdParams && (holdParams.semiMinor != null || holdParams.semiMinorAxis != null) ? (holdParams.semiMinor ?? holdParams.semiMinorAxis) : 300; |
|
|
|
const out = cesiumMap.computeEllipseParamsForDuration(centerCartesian, prevCartesian, nextCartesian, clockwise, totalHoldDistM, headingDeg, a0, b0); |
|
|
|
if (out && out.semiMajor != null && out.semiMinor != null) { |
|
|
|
holdEllipseParamsByLegIndex[i] = { |
|
|
|
semiMajor: out.semiMajor, |
|
|
|
semiMinor: out.semiMinor, |
|
|
|
headingDeg |
|
|
|
}; |
|
|
|
} |
|
|
|
} else if (!isEllipse && cesiumMap.computeHoldRadiusForDuration) { |
|
|
|
let R = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, clockwise, totalHoldDistM); |
|
|
|
if (R == null || !Number.isFinite(R)) { |
|
|
|
R = totalHoldDistM / (2 * Math.PI); |
|
|
|
} |
|
|
|
if (R != null && Number.isFinite(R) && R > 0) { |
|
|
|
holdRadiusByLegIndex[i] = R; |
|
|
|
} |
|
|
|
const holdParams = this.parseHoldParams(holdWp); |
|
|
|
const headingDeg = holdParams && holdParams.headingDeg != null ? holdParams.headingDeg : 0; |
|
|
|
const a0 = holdParams && (holdParams.semiMajor != null || holdParams.semiMajorAxis != null) ? (holdParams.semiMajor ?? holdParams.semiMajorAxis) : 500; |
|
|
|
const b0 = holdParams && (holdParams.semiMinor != null || holdParams.semiMinorAxis != null) ? (holdParams.semiMinor ?? holdParams.semiMinorAxis) : 300; |
|
|
|
const out = cesiumMap.computeEllipseParamsForDuration(centerCartesian, prevCartesian, nextCartesian, s.holdClockwise !== false, totalHoldDistM, headingDeg, a0, b0); |
|
|
|
if (out && out.semiMajor != null && out.semiMinor != null) { |
|
|
|
holdEllipseParamsByLegIndex[i] = { semiMajor: out.semiMajor, semiMinor: out.semiMinor, headingDeg }; |
|
|
|
} |
|
|
|
} |
|
|
|
const hasCircle = Object.keys(holdRadiusByLegIndex).length > 0; |
|
|
|
const hasEllipse = Object.keys(holdEllipseParamsByLegIndex).length > 0; |
|
|
|
if (hasCircle || hasEllipse) { |
|
|
|
let pathData2 = null; |
|
|
|
let segments2 = null; |
|
|
|
for (let iter = 0; iter < 4; iter++) { |
|
|
|
const ret2 = cesiumMap.getRoutePathWithSegmentIndices(waypoints, { holdRadiusByLegIndex, holdEllipseParamsByLegIndex }); |
|
|
|
if (!ret2.path || ret2.path.length === 0 || !ret2.segmentEndIndices || ret2.segmentEndIndices.length === 0) break; |
|
|
|
pathData2 = { path: ret2.path, segmentEndIndices: ret2.segmentEndIndices, holdArcRanges: ret2.holdArcRanges || {} }; |
|
|
|
const out = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData2, holdRadiusByLegIndex); |
|
|
|
segments2 = out.segments; |
|
|
|
let changed = false; |
|
|
|
if (hasCircle) { |
|
|
|
const nextRadii = {}; |
|
|
|
for (let idx = 0; idx < segments2.length; idx++) { |
|
|
|
const s = segments2[idx]; |
|
|
|
if (s.type !== 'hold' || s.holdRadius == null || s.holdCenter == null) continue; |
|
|
|
const i = s.legIndex; |
|
|
|
const holdWpCircle = waypoints[i + 1]; |
|
|
|
const holdEndTimeCircle = s.holdEndTime != null ? s.holdEndTime : this.waypointStartTimeToMinutesDecimal(holdWpCircle?.startTime); |
|
|
|
const segTargetCircle = holdWpCircle && (holdWpCircle.segmentTargetMinutes ?? holdWpCircle.displayStyle?.segmentTargetMinutes); |
|
|
|
const arrivalAtHoldCircle = (holdWpCircle && holdWpCircle.segmentMode === 'fixed_time' && segTargetCircle != null && segTargetCircle !== '') |
|
|
|
? Number(segTargetCircle) : s.startTime; |
|
|
|
const holdDurationMin = Math.max(0, holdEndTimeCircle - arrivalAtHoldCircle); |
|
|
|
const speedKmh = s.speedKmh != null ? s.speedKmh : (Number(holdWpCircle?.speed) || 800); |
|
|
|
const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000; |
|
|
|
const prevWp = waypoints[i]; |
|
|
|
const holdWp = holdWpCircle; |
|
|
|
const nextWp = (i + 2) < waypoints.length ? waypoints[i + 2] : holdWp; |
|
|
|
if (!prevWp || !holdWp) continue; |
|
|
|
const centerCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(holdWp.lng), parseFloat(holdWp.lat), Number(holdWp.alt) || 0); |
|
|
|
const prevCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(prevWp.lng), parseFloat(prevWp.lat), Number(prevWp.alt) || 0); |
|
|
|
const nextCartesian = nextWp ? Cesium.Cartesian3.fromDegrees(parseFloat(nextWp.lng), parseFloat(nextWp.lat), Number(nextWp.alt) || 0) : centerCartesian; |
|
|
|
let Rnew = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, s.holdClockwise !== false, totalHoldDistM); |
|
|
|
if (Rnew == null || !Number.isFinite(Rnew)) Rnew = totalHoldDistM / (2 * Math.PI); |
|
|
|
if (Rnew != null && Number.isFinite(Rnew) && Rnew > 0) { |
|
|
|
nextRadii[i] = Rnew; |
|
|
|
if (holdRadiusByLegIndex[i] == null || Math.abs(nextRadii[i] - holdRadiusByLegIndex[i]) > 1) changed = true; |
|
|
|
} |
|
|
|
} |
|
|
|
Object.assign(holdRadiusByLegIndex, nextRadii); |
|
|
|
} |
|
|
|
if (hasEllipse) { |
|
|
|
for (let idx = 0; idx < segments2.length; idx++) { |
|
|
|
const s = segments2[idx]; |
|
|
|
if (s.type !== 'hold' || s.holdRadius != null || s.holdCenter == null) continue; |
|
|
|
const i = s.legIndex; |
|
|
|
const holdWp = waypoints[i + 1]; |
|
|
|
if ((holdWp && (holdWp.pointType || holdWp.point_type) === 'hold_ellipse')) continue; |
|
|
|
const holdParams = this.parseHoldParams(holdWp); |
|
|
|
const holdEndTime = s.holdEndTime != null ? s.holdEndTime : this.waypointStartTimeToMinutesDecimal(holdWp?.startTime); |
|
|
|
const segTargetEllipse = holdWp && (holdWp.segmentTargetMinutes ?? holdWp.displayStyle?.segmentTargetMinutes); |
|
|
|
const arrivalAtHold = (holdWp && holdWp.segmentMode === 'fixed_time' && segTargetEllipse != null && segTargetEllipse !== '') |
|
|
|
? Number(segTargetEllipse) : s.startTime; |
|
|
|
const holdDurationMin = Math.max(0, holdEndTime - arrivalAtHold); |
|
|
|
const speedKmh = s.speedKmh != null ? s.speedKmh : (Number(holdWp?.speed) || 800); |
|
|
|
const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000; |
|
|
|
const prevWp = waypoints[i]; |
|
|
|
const nextWp = (i + 2) < waypoints.length ? waypoints[i + 2] : holdWp; |
|
|
|
if (!prevWp || !holdWp || !cesiumMap.computeEllipseParamsForDuration) continue; |
|
|
|
const centerCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(holdWp.lng), parseFloat(holdWp.lat), Number(holdWp.alt) || 0); |
|
|
|
const prevCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(prevWp.lng), parseFloat(prevWp.lat), Number(prevWp.alt) || 0); |
|
|
|
const nextCartesian = nextWp ? Cesium.Cartesian3.fromDegrees(parseFloat(nextWp.lng), parseFloat(nextWp.lat), Number(nextWp.alt) || 0) : centerCartesian; |
|
|
|
const headingDeg = holdParams && holdParams.headingDeg != null ? holdParams.headingDeg : 0; |
|
|
|
const a0 = holdParams && (holdParams.semiMajor != null || holdParams.semiMajorAxis != null) ? (holdParams.semiMajor ?? holdParams.semiMajorAxis) : 500; |
|
|
|
const b0 = holdParams && (holdParams.semiMinor != null || holdParams.semiMinorAxis != null) ? (holdParams.semiMinor ?? holdParams.semiMinorAxis) : 300; |
|
|
|
const out = cesiumMap.computeEllipseParamsForDuration(centerCartesian, prevCartesian, nextCartesian, s.holdClockwise !== false, totalHoldDistM, headingDeg, a0, b0); |
|
|
|
if (out && out.semiMajor != null) { |
|
|
|
const smj = out.semiMajor; |
|
|
|
const smn = out.semiMinor; |
|
|
|
const old = holdEllipseParamsByLegIndex[i]; |
|
|
|
if (!old || Math.abs(smj - old.semiMajor) > 1) changed = true; |
|
|
|
holdEllipseParamsByLegIndex[i] = { semiMajor: smj, semiMinor: smn, headingDeg }; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (!changed || iter === 3) break; |
|
|
|
if (hasEllipse) { |
|
|
|
const ret2 = cesiumMap.getRoutePathWithSegmentIndices(waypoints, { holdEllipseParamsByLegIndex }); |
|
|
|
if (ret2.path && ret2.path.length > 0 && ret2.segmentEndIndices && ret2.segmentEndIndices.length > 0) { |
|
|
|
pathData = { path: ret2.path, segmentEndIndices: ret2.segmentEndIndices, holdArcRanges: ret2.holdArcRanges || {} }; |
|
|
|
const out2 = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData); |
|
|
|
segments = out2.segments; |
|
|
|
} |
|
|
|
if (pathData2) pathData = pathData2; |
|
|
|
if (segments2) segments = segments2; |
|
|
|
if (routeId != null) { |
|
|
|
if (cesiumMap.setRouteHoldRadii) cesiumMap.setRouteHoldRadii(routeId, holdRadiusByLegIndex); |
|
|
|
if (cesiumMap.setRouteHoldEllipseParams) cesiumMap.setRouteHoldEllipseParams(routeId, holdEllipseParamsByLegIndex); |
|
|
|
if (routeId != null && cesiumMap.setRouteHoldEllipseParams) { |
|
|
|
cesiumMap.setRouteHoldEllipseParams(routeId, holdEllipseParamsByLegIndex); |
|
|
|
} |
|
|
|
} else if (routeId != null) { |
|
|
|
if (cesiumMap.setRouteHoldRadii) cesiumMap.setRouteHoldRadii(routeId, {}); |
|
|
|
if (cesiumMap.setRouteHoldEllipseParams) cesiumMap.setRouteHoldEllipseParams(routeId, {}); |
|
|
|
} else if (routeId != null && cesiumMap.setRouteHoldEllipseParams) { |
|
|
|
cesiumMap.setRouteHoldEllipseParams(routeId, {}); |
|
|
|
} |
|
|
|
// 圆形盘旋不再使用反算半径,清空旧缓存 |
|
|
|
if (routeId != null && cesiumMap.setRouteHoldRadii) { |
|
|
|
cesiumMap.setRouteHoldRadii(routeId, {}); |
|
|
|
} |
|
|
|
} |
|
|
|
const path = pathData ? pathData.path : null; |
|
|
|
@ -5076,13 +5052,16 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 冲突操作:根据当前展示的航线与时间轴计算真实问题(提前到达、无法按时到达) |
|
|
|
// 冲突操作:时间冲突(提前到达、无法按时到达、盘旋时间不足、航线时间窗重叠)、空间冲突(航迹间隔、平台摆放、禁限区入侵)、频谱冲突 |
|
|
|
runConflictCheck() { |
|
|
|
const list = []; |
|
|
|
let id = 1; |
|
|
|
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); |
|
|
|
|
|
|
|
const allRaw = []; |
|
|
|
|
|
|
|
// ---------- 时间冲突:单航线内(提前到达、无法按时到达、盘旋时间不足)---------- |
|
|
|
routeIds.forEach(routeId => { |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route || !route.waypoints || route.waypoints.length < 2) return; |
|
|
|
@ -5094,65 +5073,208 @@ export default { |
|
|
|
} |
|
|
|
} |
|
|
|
const { earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData); |
|
|
|
|
|
|
|
const routeName = route.name || `航线${route.id}`; |
|
|
|
(earlyArrivalLegs || []).forEach(leg => { |
|
|
|
list.push({ |
|
|
|
id: id++, |
|
|
|
// 提前到达:给出多种可选思路,统一在提示里说明“视定速/定时约束优先调整未受约束量” |
|
|
|
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: '该航段将提前到达下一航点,建议在此段加入盘旋或延后下一航点计划时间。', |
|
|
|
suggestion: '该航段将提前到达下一航点。可选措施:① 适当降低本段巡航速度;② 在本段或下一航点前增加盘旋等待;③ 视任务需要调整下一航点相对K时/计划时间。若存在定速点或定时点,请优先调整未受约束的速度或时间。', |
|
|
|
severity: 'high' |
|
|
|
}); |
|
|
|
}); |
|
|
|
(lateArrivalLegs || []).forEach(leg => { |
|
|
|
list.push({ |
|
|
|
id: id++, |
|
|
|
// 无法按时到达:同时提示“提速”和“调整时间/路径”等多种方案 |
|
|
|
allRaw.push({ |
|
|
|
type: CONFLICT_TYPE.TIME, |
|
|
|
subType: 'late_arrival', |
|
|
|
title: '无法按时到达', |
|
|
|
routeName, |
|
|
|
routeIds: [routeId], |
|
|
|
fromWaypoint: leg.fromName, |
|
|
|
toWaypoint: leg.toName, |
|
|
|
suggestion: `当前速度不足,建议将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h,或延后下一航点计划时间。`, |
|
|
|
suggestion: `当前速度不足,理论上需将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h 才能按时到达。可选措施:① 在安全范围内提高本段或前一段速度;② 适当提前下一航点相对K时/计划时间;③ 结合任务需要调整上游航段或加入盘旋缓冲。若存在定速点或定时点,请优先从未锁定的速度或时间入手。`, |
|
|
|
severity: 'high' |
|
|
|
}); |
|
|
|
}); |
|
|
|
(holdDelayConflicts || []).forEach(conf => { |
|
|
|
list.push({ |
|
|
|
id: id++, |
|
|
|
// 盘旋时间不足:提示可修改盘旋圈数/时间、速度或切出时刻 |
|
|
|
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} 秒。`, |
|
|
|
suggestion: `警告:设定的盘旋时间不足以支撑战斗机完成最后一圈,实际切出将延迟 ${conf.delaySeconds} 秒。可选措施:① 增加盘旋圈数或调整盘旋结束时间;② 在允许范围内调整盘旋段速度;③ 结合上下游航段,微调相关航点的相对K时/计划时间。若存在定速点或定时点,请优先调整未受约束的参数。`, |
|
|
|
severity: 'high', |
|
|
|
holdCenter: conf.holdCenter |
|
|
|
holdCenter: conf.holdCenter, |
|
|
|
positionLng: conf.holdCenter && conf.holdCenter.lng, |
|
|
|
positionLat: conf.holdCenter && conf.holdCenter.lat, |
|
|
|
positionAlt: conf.holdCenter && conf.holdCenter.alt |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
this.conflicts = list; |
|
|
|
this.conflictCount = list.length; |
|
|
|
if (list.length > 0) { |
|
|
|
this.$message.warning(`检测到 ${list.length} 处航线时间问题`); |
|
|
|
// 时间窗重叠:仅“时间段有交集”不算冲突(不同地点飞机可同时起飞)。后续若有跑道/频段等资源绑定,再按“同一资源占用时间重叠”报冲突。 |
|
|
|
// 故此处不再调用 detectTimeWindowOverlap。 |
|
|
|
|
|
|
|
// ---------- 空间冲突:航迹最小间隔 ---------- |
|
|
|
const getPositionAtMinutesForConflict = (routeId, minutesFromK) => { |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route || !route.waypoints || route.waypoints.length === 0) return null; |
|
|
|
const { position } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes, routeId); |
|
|
|
return position; |
|
|
|
}; |
|
|
|
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.$message.success('未发现冲突'); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
/** 查看冲突:展开问题航线、显示右侧方案树、定位到冲突位置并跳转时间轴 */ |
|
|
|
viewConflict(conflict) { |
|
|
|
this.$message.info(`查看冲突:${conflict.title}`); |
|
|
|
const routeIds = conflict.routeIds || []; |
|
|
|
if (routeIds.length > 0) { |
|
|
|
// 保留原有已展示航线,同时并入本次冲突涉及的航线 |
|
|
|
const prevActive = this.activeRouteIds || []; |
|
|
|
this.activeRouteIds = [...new Set([...prevActive, ...routeIds])]; |
|
|
|
this.isRightPanelHidden = false; |
|
|
|
this.activeRightTab = 'plan'; |
|
|
|
this.expandRouteIdsForPanel = [...routeIds]; |
|
|
|
|
|
|
|
// 确保相关航线在地图上真实渲染出来(不仅仅是相机飞过去) |
|
|
|
if (this.$refs.cesiumMap) { |
|
|
|
routeIds.forEach(async (routeId) => { |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route) return; |
|
|
|
|
|
|
|
// 若列表中尚未加载航点,则从后端拉取一次 |
|
|
|
let waypoints = Array.isArray(route.waypoints) ? route.waypoints : []; |
|
|
|
if (!waypoints.length) { |
|
|
|
try { |
|
|
|
const res = await getRoutes(route.id); |
|
|
|
if (res && res.code === 200 && res.data && Array.isArray(res.data.waypoints)) { |
|
|
|
waypoints = res.data.waypoints; |
|
|
|
// 回写到 routes,避免后续重复请求 |
|
|
|
const idx = this.routes.findIndex(r => r.id === route.id); |
|
|
|
if (idx > -1) { |
|
|
|
this.$set(this.routes, idx, { |
|
|
|
...this.routes[idx], |
|
|
|
waypoints |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
console.warn('viewConflict: 获取航线航点失败', e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (waypoints.length > 0) { |
|
|
|
// 应用平台样式(若有),保持与正常选中航线一致的效果 |
|
|
|
const roomId = this.currentRoomId; |
|
|
|
if (roomId && route.platformId) { |
|
|
|
try { |
|
|
|
const styleRes = await getPlatformStyle({ roomId, routeId, platformId: route.platformId }); |
|
|
|
if (styleRes && styleRes.data) { |
|
|
|
this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data); |
|
|
|
} |
|
|
|
} catch (_) {} |
|
|
|
} |
|
|
|
this.$refs.cesiumMap.renderRouteWaypoints( |
|
|
|
waypoints, |
|
|
|
routeId, |
|
|
|
route.platformId, |
|
|
|
route.platform, |
|
|
|
this.parseRouteStyle(route.attributes) |
|
|
|
); |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
if (conflict.positionLng != null && conflict.positionLat != null && this.$refs.cesiumMap && this.$refs.cesiumMap.flyToPosition) { |
|
|
|
this.$refs.cesiumMap.flyToPosition(conflict.positionLng, conflict.positionLat, conflict.positionAlt, 1.5); |
|
|
|
} |
|
|
|
if (conflict.minutesFromK != null && Number.isFinite(conflict.minutesFromK)) { |
|
|
|
const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); |
|
|
|
const span = Math.max(0, maxMinutes - minMinutes) || 120; |
|
|
|
const progress = Math.max(0, Math.min(100, ((conflict.minutesFromK - minMinutes) / span) * 100)); |
|
|
|
this.timeProgress = progress; |
|
|
|
} |
|
|
|
this.$message({ message: `已定位:${conflict.title}`, type: 'info', duration: 2000 }); |
|
|
|
}, |
|
|
|
|
|
|
|
resolveConflict(conflict) { |
|
|
|
this.$message.success(`解决冲突:${conflict.title}`); |
|
|
|
// 移除已解决的冲突 |
|
|
|
this.conflicts = this.conflicts.filter(c => c.id !== conflict.id); |
|
|
|
this.conflictCount = this.conflicts.length; |
|
|
|
/** 解决冲突:根据建议打开该航线的航点列表(编辑航线弹窗-航点 tab),由用户修改盘旋/速度/相对K时等,不直接删除冲突 */ |
|
|
|
async resolveConflict(conflict) { |
|
|
|
const routeId = conflict.routeIds && conflict.routeIds[0]; |
|
|
|
if (!routeId) { |
|
|
|
this.$message.warning('无法确定关联航线'); |
|
|
|
return; |
|
|
|
} |
|
|
|
let route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route) { |
|
|
|
this.$message.warning('未找到该航线'); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!route.waypoints || route.waypoints.length === 0) { |
|
|
|
try { |
|
|
|
const res = await getRoutes(routeId); |
|
|
|
if (res.data && res.data.waypoints) { |
|
|
|
route = { ...route, waypoints: res.data.waypoints }; |
|
|
|
const idx = this.routes.findIndex(r => r.id === routeId); |
|
|
|
if (idx !== -1) this.routes.splice(idx, 1, route); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
console.warn('获取航线航点失败', e); |
|
|
|
} |
|
|
|
} |
|
|
|
this.selectedRouteId = routeId; |
|
|
|
this.selectedRouteDetails = route.waypoints ? { ...route } : null; |
|
|
|
this.selectedRoute = route.waypoints ? { ...route } : route; |
|
|
|
this.routeEditInitialTab = 'waypoints'; |
|
|
|
this.showRouteDialog = true; |
|
|
|
if (this.wsConnection && this.wsConnection.sendObjectEditLock && route && route.id != null) { |
|
|
|
this.wsConnection.sendObjectEditLock('route', route.id); |
|
|
|
this.routeEditLockedId = route.id; |
|
|
|
} |
|
|
|
this.$message.info('请根据建议在航点列表中修改(如加入盘旋或调整相对K时/速度)'); |
|
|
|
}, |
|
|
|
|
|
|
|
// 系统功能 |
|
|
|
|