|
|
|
@ -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) { |
|
|
|
@ -5025,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; |
|
|
|
@ -5043,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时/速度)'); |
|
|
|
}, |
|
|
|
|
|
|
|
// 系统功能 |
|
|
|
|