|
|
|
@ -39,6 +39,7 @@ |
|
|
|
@add-waypoint-at="handleAddWaypointAt" |
|
|
|
@add-waypoint-placed="handleAddWaypointPlaced" |
|
|
|
@toggle-waypoint-hold="handleToggleWaypointHold" |
|
|
|
@edit-hold-speed="handleEditHoldSpeed" |
|
|
|
@waypoint-position-changed="handleWaypointPositionChanged" |
|
|
|
@missile-deleted="handleMissileDeleted" |
|
|
|
@scale-click="handleScaleClick" |
|
|
|
@ -412,6 +413,7 @@ |
|
|
|
:current-room-id="currentRoomId" |
|
|
|
:routes="routes" |
|
|
|
:active-route-ids="activeRouteIds" |
|
|
|
@bar-time-change="handleGanttBarTimeChange" |
|
|
|
/> |
|
|
|
|
|
|
|
<!-- 4T悬浮窗(THREAT/TASK/TARGET/TACTIC)- 仅点击4T图标时打开 --> |
|
|
|
@ -518,7 +520,7 @@ import ConflictDrawer from './ConflictDrawer' |
|
|
|
import WhiteboardPanel from './WhiteboardPanel' |
|
|
|
import { createRoomWebSocket } from '@/utils/websocket'; |
|
|
|
import { listScenario, addScenario, delScenario } from "@/api/system/scenario"; |
|
|
|
import { listRoutes, getRoutes, addRoutes, updateRoutes, delRoutes, getPlatformStyle, getMissileParams, updateMissilePositions } from "@/api/system/routes"; |
|
|
|
import { listRoutes, getRoutes, addRoutes, updateRoutes, delRoutes, getPlatformStyle, getMissileParams, updateMissilePositions, saveMissileParams, deleteMissileParams } from "@/api/system/routes"; |
|
|
|
import { updateWaypoints, addWaypoints, delWaypoints } from "@/api/system/waypoints"; |
|
|
|
import { listLib,addLib,delLib} from "@/api/system/lib"; |
|
|
|
import { getRooms, updateRooms, listRooms } from "@/api/system/rooms"; |
|
|
|
@ -532,7 +534,6 @@ import GanttDrawer from './GanttDrawer.vue'; |
|
|
|
import { |
|
|
|
CONFLICT_TYPE, |
|
|
|
defaultConflictConfig, |
|
|
|
createSpectrumLedgerEntry, |
|
|
|
normalizeConflictList |
|
|
|
} from '@/utils/conflictDetection'; |
|
|
|
import ConflictCheckWorker from 'worker-loader!@/workers/conflictCheck.worker.js' |
|
|
|
@ -756,10 +757,8 @@ export default { |
|
|
|
// 冲突数据(由 runConflictCheck 根据当前航线与时间轴计算真实问题) |
|
|
|
conflictCount: 0, |
|
|
|
conflicts: [], |
|
|
|
/** 冲突检测配置(时间缓冲、航迹最小间隔、平台摆放最小距离、禁限区关键词、频谱邻频保护等) */ |
|
|
|
/** 冲突检测配置(时间缓冲、航迹最小间隔、平台摆放最小距离、禁限区关键词等) */ |
|
|
|
conflictConfig: { ...defaultConflictConfig }, |
|
|
|
/** 频谱资源台账(用于频谱冲突检测),可后续从接口或界面维护 */ |
|
|
|
spectrumLedger: [], |
|
|
|
/** 冲突检测 worker(非响应式对象,仅占位,实际实例挂在 this._conflictWorker) */ |
|
|
|
_conflictWorkerInited: false, |
|
|
|
|
|
|
|
@ -935,7 +934,8 @@ export default { |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route || !route.waypoints || !route.waypoints.length) return; |
|
|
|
try { |
|
|
|
const { segments } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, null); |
|
|
|
const pathData = this.buildPathDataForRouteTimeline(route.waypoints, routeId); |
|
|
|
const { segments } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData); |
|
|
|
if (!segments || segments.length === 0) return; |
|
|
|
const startMinutes = segments[0].startTime; |
|
|
|
const endMinutes = segments[segments.length - 1].endTime; |
|
|
|
@ -966,21 +966,11 @@ export default { |
|
|
|
ganttHoldBars() { |
|
|
|
const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); |
|
|
|
const bars = []; |
|
|
|
const cesiumMap = this.$refs.cesiumMap; |
|
|
|
this.activeRouteIds.forEach(routeId => { |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route || !route.waypoints || !route.waypoints.length) return; |
|
|
|
try { |
|
|
|
let pathData = null; |
|
|
|
if (cesiumMap && cesiumMap.getRoutePathWithSegmentIndices) { |
|
|
|
const cachedRadii = (cesiumMap._routeHoldRadiiByRoute && cesiumMap._routeHoldRadiiByRoute[routeId]) ? cesiumMap._routeHoldRadiiByRoute[routeId] : {}; |
|
|
|
const cachedEllipse = (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 ret = cesiumMap.getRoutePathWithSegmentIndices(route.waypoints, opts); |
|
|
|
if (ret.path && ret.path.length > 0 && ret.segmentEndIndices && ret.segmentEndIndices.length > 0 && ret.holdArcRanges) { |
|
|
|
pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges }; |
|
|
|
} |
|
|
|
} |
|
|
|
const pathData = this.buildPathDataForRouteTimeline(route.waypoints, routeId); |
|
|
|
const { segments } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData); |
|
|
|
if (!segments) return; |
|
|
|
const routeName = route.name || route.callSign || `航线${route.id}`; |
|
|
|
@ -1524,6 +1514,113 @@ export default { |
|
|
|
console.error(e); |
|
|
|
} |
|
|
|
}, |
|
|
|
/** 右键盘旋轨迹“编辑盘旋速度”:更新该盘旋航点 speed,推演按该值计算盘旋段速度(默认800)。 */ |
|
|
|
async handleEditHoldSpeed({ routeId, dbId, waypointIndex, speed }) { |
|
|
|
if (this.isRouteLockedByOther(routeId)) { |
|
|
|
this.$message.warning('该航线正被其他成员编辑,无法修改'); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (this.routeLocked[routeId]) { |
|
|
|
this.$message.info('该航线已上锁,请先解锁'); |
|
|
|
return; |
|
|
|
} |
|
|
|
const targetSpeed = Number(speed); |
|
|
|
if (!Number.isFinite(targetSpeed) || targetSpeed <= 0) { |
|
|
|
this.$message.warning('盘旋速度必须大于0'); |
|
|
|
return; |
|
|
|
} |
|
|
|
let route = this.routes.find(r => r.id === routeId); |
|
|
|
let waypoints = route && route.waypoints; |
|
|
|
if (!waypoints || waypoints.length === 0) { |
|
|
|
try { |
|
|
|
const res = await getRoutes(routeId); |
|
|
|
if (res.code === 200 && res.data && res.data.waypoints) { |
|
|
|
waypoints = res.data.waypoints; |
|
|
|
route = { ...route, waypoints }; |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
this.$message.error('获取航线失败'); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
if (!waypoints || waypoints.length === 0) { |
|
|
|
this.$message.warning('航线无航点'); |
|
|
|
return; |
|
|
|
} |
|
|
|
const wp = dbId != null ? waypoints.find(w => w.id === dbId) : waypoints[waypointIndex]; |
|
|
|
if (!wp) { |
|
|
|
this.$message.warning('未找到该盘旋航点'); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!this.isHoldWaypoint(wp)) { |
|
|
|
this.$message.warning('仅盘旋航点支持编辑盘旋速度'); |
|
|
|
return; |
|
|
|
} |
|
|
|
try { |
|
|
|
const holdParamsObj = this.parseHoldParams(wp) || { |
|
|
|
type: (wp.pointType || wp.point_type) === 'hold_ellipse' ? 'hold_ellipse' : 'hold_circle', |
|
|
|
clockwise: true |
|
|
|
}; |
|
|
|
const nextHoldParamsObj = { ...holdParamsObj, holdSpeed: targetSpeed }; |
|
|
|
const payload = { |
|
|
|
id: wp.id, |
|
|
|
routeId, |
|
|
|
name: wp.name, |
|
|
|
seq: wp.seq, |
|
|
|
lat: wp.lat, |
|
|
|
lng: wp.lng, |
|
|
|
alt: wp.alt, |
|
|
|
speed: wp.speed != null ? wp.speed : 800, |
|
|
|
startTime: wp.startTime != null && wp.startTime !== '' ? wp.startTime : 'K+00:00:00', |
|
|
|
turnAngle: wp.turnAngle != null && wp.turnAngle !== '' ? Number(wp.turnAngle) : 0, |
|
|
|
pointType: (wp.pointType || wp.point_type || 'hold_circle') |
|
|
|
}; |
|
|
|
payload.holdParams = JSON.stringify(nextHoldParamsObj); |
|
|
|
if (wp.segmentMode != null) payload.segmentMode = wp.segmentMode; |
|
|
|
if (wp.segmentTargetMinutes != null) payload.segmentTargetMinutes = wp.segmentTargetMinutes; |
|
|
|
if (wp.segmentTargetSpeed != null) payload.segmentTargetSpeed = wp.segmentTargetSpeed; |
|
|
|
if (wp.labelFontSize != null) payload.labelFontSize = wp.labelFontSize; |
|
|
|
if (wp.labelColor != null) payload.labelColor = wp.labelColor; |
|
|
|
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); |
|
|
|
if (routeInList && routeInList.waypoints) { |
|
|
|
const idx = routeInList.waypoints.findIndex(p => p.id === wp.id); |
|
|
|
if (idx !== -1) routeInList.waypoints.splice(idx, 1, merged); |
|
|
|
} |
|
|
|
if (this.selectedRouteId === routeId && this.selectedRouteDetails && this.selectedRouteDetails.waypoints) { |
|
|
|
const idxS = this.selectedRouteDetails.waypoints.findIndex(p => p.id === wp.id); |
|
|
|
if (idxS !== -1) this.selectedRouteDetails.waypoints.splice(idxS, 1, merged); |
|
|
|
} |
|
|
|
if (this.activeRouteIds.includes(routeId) && this.$refs.cesiumMap) { |
|
|
|
const r = this.routes.find(rr => rr.id === routeId); |
|
|
|
if (r && r.waypoints) { |
|
|
|
const roomId = this.currentRoomId; |
|
|
|
if (roomId && r.platformId) { |
|
|
|
try { |
|
|
|
const styleRes = await getPlatformStyle({ roomId, routeId, platformId: r.platformId }); |
|
|
|
if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data); |
|
|
|
} catch (_) {} |
|
|
|
} |
|
|
|
const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); |
|
|
|
if (r.waypoints.some(wp2 => this.isHoldWaypoint(wp2))) { |
|
|
|
this.getPositionAtMinutesFromK(r.waypoints, minMinutes, minMinutes, maxMinutes, routeId); |
|
|
|
} |
|
|
|
this.$refs.cesiumMap.removeRouteById(routeId); |
|
|
|
this.$refs.cesiumMap.renderRouteWaypoints(r.waypoints, routeId, r.platformId, r.platform, this.parseRouteStyle(r.attributes)); |
|
|
|
this.$nextTick(() => this.updateDeductionPositions()); |
|
|
|
} |
|
|
|
} |
|
|
|
this.$message.success(`盘旋速度已更新为 ${Math.round(targetSpeed * 10) / 10} km/h`); |
|
|
|
this.wsConnection?.sendSyncWaypoints?.(routeId); |
|
|
|
} catch (e) { |
|
|
|
this.$message.error(e.msg || e.message || '更新盘旋速度失败'); |
|
|
|
console.error(e); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
/** 右键「复制航线」:拉取航点后进入复制预览,左键放置后弹窗保存 */ |
|
|
|
async handleCopyRoute(routeId) { |
|
|
|
@ -4318,6 +4415,200 @@ export default { |
|
|
|
generateGanttChart() { |
|
|
|
this.showGanttDrawer = true |
|
|
|
}, |
|
|
|
parseRouteIdFromGanttBarId(barId) { |
|
|
|
if (!barId) return null; |
|
|
|
const s = String(barId); |
|
|
|
if (!s.startsWith('route-')) return null; |
|
|
|
const id = Number(s.slice(6)); |
|
|
|
return Number.isFinite(id) ? id : null; |
|
|
|
}, |
|
|
|
transformTimelineMinutes(value, oldStart, oldEnd, newStart, newEnd) { |
|
|
|
const v = Number(value); |
|
|
|
const os = Number(oldStart); |
|
|
|
const oe = Number(oldEnd); |
|
|
|
const ns = Number(newStart); |
|
|
|
const ne = Number(newEnd); |
|
|
|
if (![v, os, oe, ns, ne].every(Number.isFinite)) return v; |
|
|
|
const oldSpan = oe - os; |
|
|
|
const newSpan = ne - ns; |
|
|
|
if (oldSpan <= 0) return v + (ns - os); |
|
|
|
const ratio = (v - os) / oldSpan; |
|
|
|
return ns + ratio * newSpan; |
|
|
|
}, |
|
|
|
getRouteOperationRoomId(route) { |
|
|
|
const roomId = this.currentRoomId; |
|
|
|
const isParentRoom = this.roomDetail && this.roomDetail.parentId == null; |
|
|
|
const plan = route ? this.plans.find(p => p.id === route.scenarioId) : null; |
|
|
|
return (isParentRoom && plan && plan.roomId) ? plan.roomId : roomId; |
|
|
|
}, |
|
|
|
buildWaypointTimeUpdatePayload(wp, routeId, nextStartTime, nextSegmentTargetMinutes) { |
|
|
|
const payload = { |
|
|
|
id: wp.id, |
|
|
|
routeId: wp.routeId != null ? wp.routeId : routeId, |
|
|
|
name: wp.name, |
|
|
|
seq: wp.seq, |
|
|
|
lat: wp.lat, |
|
|
|
lng: wp.lng, |
|
|
|
alt: wp.alt, |
|
|
|
speed: wp.speed, |
|
|
|
startTime: nextStartTime != null ? nextStartTime : ((wp.startTime != null && wp.startTime !== '') ? wp.startTime : 'K+00:00:00'), |
|
|
|
turnAngle: wp.turnAngle |
|
|
|
}; |
|
|
|
if (wp.pointType != null) payload.pointType = wp.pointType; |
|
|
|
if (wp.holdParams != null) payload.holdParams = wp.holdParams; |
|
|
|
if (wp.labelFontSize != null) payload.labelFontSize = wp.labelFontSize; |
|
|
|
if (wp.labelColor != null) payload.labelColor = wp.labelColor; |
|
|
|
if (wp.segmentMode != null) payload.segmentMode = wp.segmentMode; |
|
|
|
if (wp.color != null) payload.color = wp.color; |
|
|
|
if (wp.pixelSize != null) payload.pixelSize = wp.pixelSize; |
|
|
|
if (wp.outlineColor != null) payload.outlineColor = wp.outlineColor; |
|
|
|
if (wp.segmentTargetSpeed != null) payload.segmentTargetSpeed = wp.segmentTargetSpeed; |
|
|
|
if (nextSegmentTargetMinutes != null && Number.isFinite(nextSegmentTargetMinutes)) { |
|
|
|
payload.segmentTargetMinutes = Number(nextSegmentTargetMinutes.toFixed(6)); |
|
|
|
} else if (wp.segmentTargetMinutes != null && wp.segmentTargetMinutes !== '') { |
|
|
|
payload.segmentTargetMinutes = wp.segmentTargetMinutes; |
|
|
|
} |
|
|
|
if (wp.displayStyle != null) { |
|
|
|
const ds = { ...wp.displayStyle }; |
|
|
|
if (nextSegmentTargetMinutes != null && Number.isFinite(nextSegmentTargetMinutes)) { |
|
|
|
ds.segmentTargetMinutes = Number(nextSegmentTargetMinutes.toFixed(6)); |
|
|
|
} |
|
|
|
payload.displayStyle = ds; |
|
|
|
} |
|
|
|
return payload; |
|
|
|
}, |
|
|
|
async shiftRouteMissilesByGantt(roomId, routeId, platformId, oldStart, oldEnd, newStart, newEnd, waypointsAfter) { |
|
|
|
if (roomId == null || routeId == null) return; |
|
|
|
let missilesRes; |
|
|
|
try { |
|
|
|
missilesRes = await getMissileParams({ roomId, routeId, platformId }); |
|
|
|
} catch (_) { |
|
|
|
return; |
|
|
|
} |
|
|
|
let missiles = missilesRes && missilesRes.data; |
|
|
|
if (!missiles) return; |
|
|
|
if (!Array.isArray(missiles)) missiles = [missiles]; |
|
|
|
if (missiles.length === 0) return; |
|
|
|
const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); |
|
|
|
const shifted = missiles.map(m => { |
|
|
|
const oldLaunch = m.launchTimeMinutesFromK != null ? Number(m.launchTimeMinutesFromK) : null; |
|
|
|
if (!Number.isFinite(oldLaunch)) return null; |
|
|
|
const nextLaunch = this.transformTimelineMinutes(oldLaunch, oldStart, oldEnd, newStart, newEnd); |
|
|
|
const { position, nextPosition } = this.getPositionAtMinutesFromK(waypointsAfter, nextLaunch, minMinutes, maxMinutes); |
|
|
|
const startLng = position && position.lng != null ? position.lng : m.startLng; |
|
|
|
const startLat = position && position.lat != null ? position.lat : m.startLat; |
|
|
|
let platformHeadingDeg = Number(m.platformHeadingDeg) || 0; |
|
|
|
if (position && nextPosition && nextPosition.lng != null && nextPosition.lat != null) { |
|
|
|
platformHeadingDeg = this.headingDegFromPositions(position, nextPosition); |
|
|
|
} |
|
|
|
return { |
|
|
|
roomId, |
|
|
|
routeId, |
|
|
|
platformId, |
|
|
|
angle: Number(m.angle) || 0, |
|
|
|
distance: Number(m.distance) || 1000, |
|
|
|
launchTimeMinutesFromK: Number(nextLaunch.toFixed(6)), |
|
|
|
startLng, |
|
|
|
startLat, |
|
|
|
platformHeadingDeg |
|
|
|
}; |
|
|
|
}).filter(Boolean); |
|
|
|
if (shifted.length === 0) return; |
|
|
|
for (let i = missiles.length - 1; i >= 0; i--) { |
|
|
|
try { |
|
|
|
await deleteMissileParams({ roomId, routeId, platformId, index: i }); |
|
|
|
} catch (_) {} |
|
|
|
} |
|
|
|
for (const item of shifted) { |
|
|
|
try { |
|
|
|
await saveMissileParams(item); |
|
|
|
} catch (_) {} |
|
|
|
} |
|
|
|
this.handleMissileDeleted(); |
|
|
|
}, |
|
|
|
async handleGanttBarTimeChange(change) { |
|
|
|
if (!change || change.type !== 'route') return; |
|
|
|
const routeId = this.parseRouteIdFromGanttBarId(change.id); |
|
|
|
if (routeId == null) return; |
|
|
|
if (this.isRouteLockedByOther(routeId)) { |
|
|
|
this.$message.warning('该航线正被其他成员编辑,无法通过甘特图调整时间'); |
|
|
|
return; |
|
|
|
} |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route || !Array.isArray(route.waypoints) || route.waypoints.length === 0) return; |
|
|
|
const oldBar = (this.ganttRouteBars || []).find(b => b.id === change.id); |
|
|
|
if (!oldBar) return; |
|
|
|
const oldStart = Number(oldBar.startMinutes); |
|
|
|
const oldEnd = Number(oldBar.endMinutes); |
|
|
|
const newStart = Number(change.startMinutes); |
|
|
|
const newEnd = Number(change.endMinutes); |
|
|
|
if (![oldStart, oldEnd, newStart, newEnd].every(Number.isFinite) || newEnd <= newStart) return; |
|
|
|
const roomId = this.getRouteOperationRoomId(route); |
|
|
|
const roomIdParam = roomId != null ? { roomId } : {}; |
|
|
|
const oldWpSorted = (route.waypoints || []).slice().sort((a, b) => (Number(a.seq) || 0) - (Number(b.seq) || 0)); |
|
|
|
const oldToNewById = {}; |
|
|
|
let prevNewMinutes = null; |
|
|
|
oldWpSorted.forEach(wp => { |
|
|
|
const oldMinutes = this.waypointStartTimeToMinutesDecimal(wp.startTime); |
|
|
|
let newMinutes = this.transformTimelineMinutes(oldMinutes, oldStart, oldEnd, newStart, newEnd); |
|
|
|
if (prevNewMinutes != null && newMinutes <= prevNewMinutes) { |
|
|
|
newMinutes = prevNewMinutes + 1 / 60; // 保证时间严格递增(1 秒级微调) |
|
|
|
} |
|
|
|
prevNewMinutes = newMinutes; |
|
|
|
let nextSegTarget = null; |
|
|
|
let oldSegTarget = null; |
|
|
|
if (wp.segmentTargetMinutes != null && wp.segmentTargetMinutes !== '') oldSegTarget = Number(wp.segmentTargetMinutes); |
|
|
|
else if (wp.displayStyle && wp.displayStyle.segmentTargetMinutes != null && wp.displayStyle.segmentTargetMinutes !== '') { |
|
|
|
oldSegTarget = Number(wp.displayStyle.segmentTargetMinutes); |
|
|
|
} |
|
|
|
if (Number.isFinite(oldSegTarget)) { |
|
|
|
nextSegTarget = this.transformTimelineMinutes(oldSegTarget, oldStart, oldEnd, newStart, newEnd); |
|
|
|
if (nextSegTarget < newMinutes) nextSegTarget = newMinutes; |
|
|
|
} |
|
|
|
oldToNewById[wp.id] = { |
|
|
|
nextStartTime: this.minutesToStartTimeWithSeconds(newMinutes), |
|
|
|
nextStartMinutes: newMinutes, |
|
|
|
nextSegTarget |
|
|
|
}; |
|
|
|
}); |
|
|
|
try { |
|
|
|
for (const wp of oldWpSorted) { |
|
|
|
const next = oldToNewById[wp.id]; |
|
|
|
const payload = this.buildWaypointTimeUpdatePayload(wp, routeId, next.nextStartTime, next.nextSegTarget); |
|
|
|
const res = await updateWaypoints(payload, roomIdParam); |
|
|
|
if (!res || res.code !== 200) throw new Error('update waypoint failed'); |
|
|
|
} |
|
|
|
const nextWaypoints = (route.waypoints || []).map(wp => { |
|
|
|
const next = oldToNewById[wp.id]; |
|
|
|
if (!next) return wp; |
|
|
|
const merged = { ...wp, startTime: next.nextStartTime }; |
|
|
|
if (next.nextSegTarget != null && Number.isFinite(next.nextSegTarget)) { |
|
|
|
merged.segmentTargetMinutes = Number(next.nextSegTarget.toFixed(6)); |
|
|
|
if (merged.displayStyle) merged.displayStyle = { ...merged.displayStyle, segmentTargetMinutes: merged.segmentTargetMinutes }; |
|
|
|
} |
|
|
|
return merged; |
|
|
|
}); |
|
|
|
this.$set(route, 'waypoints', nextWaypoints); |
|
|
|
if (this.selectedRouteDetails && this.selectedRouteDetails.id === routeId) { |
|
|
|
this.$set(this.selectedRouteDetails, 'waypoints', nextWaypoints.slice()); |
|
|
|
} |
|
|
|
await this.shiftRouteMissilesByGantt( |
|
|
|
roomId, |
|
|
|
routeId, |
|
|
|
route.platformId != null ? route.platformId : 0, |
|
|
|
oldStart, |
|
|
|
oldEnd, |
|
|
|
newStart, |
|
|
|
newEnd, |
|
|
|
nextWaypoints |
|
|
|
); |
|
|
|
this.updateTimeFromProgress(); |
|
|
|
this.$message.success('甘特图时间已联动到航线、盘旋与导弹发射时刻'); |
|
|
|
} catch (e) { |
|
|
|
console.warn('甘特图联动时间失败', e); |
|
|
|
this.$message.error('联动更新时间失败,请重试'); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
systemDescription() { |
|
|
|
this.$message.success('系统说明'); |
|
|
|
@ -4777,7 +5068,23 @@ export default { |
|
|
|
const toEntrySlice = path.slice(startIdx, range.start + 1); |
|
|
|
const holdPathSlice = path.slice(range.start, range.end + 1); |
|
|
|
const exitIdx = segmentEndIndices[i]; |
|
|
|
const toNextSlice = path.slice(exitIdx, (segmentEndIndices[i + 1] != null ? segmentEndIndices[i + 1] : path.length - 1) + 1); |
|
|
|
// 飞出盘旋后→下一航点:路径切片必须止于「下一航点」几何终点。 |
|
|
|
// 若下一航点也是盘旋,segmentEndIndices[i+1] 会指向下一段盘旋整条轨迹的末尾, |
|
|
|
// 误把下一段跑道全长算进本段直飞 → arrivalNext / effectiveTime 链错乱,甘特图出现「两段盘旋时间重叠」、 |
|
|
|
// 第二段盘旋开始时刻远早于地图实际进入下一跑道入口的时刻。应止于下一段盘旋弧的起点(入口)。 |
|
|
|
const nextWpIsHold = i + 2 < points.length && this.isHoldWaypoint(waypoints[i + 2]); |
|
|
|
let toNextSliceEndIdx; |
|
|
|
if (nextWpIsHold) { |
|
|
|
const nextHoldRange = holdArcRanges[i + 1]; |
|
|
|
if (nextHoldRange && Number.isFinite(nextHoldRange.start)) { |
|
|
|
toNextSliceEndIdx = nextHoldRange.start; |
|
|
|
} else { |
|
|
|
toNextSliceEndIdx = segmentEndIndices[i + 1] != null ? segmentEndIndices[i + 1] : path.length - 1; |
|
|
|
} |
|
|
|
} else { |
|
|
|
toNextSliceEndIdx = segmentEndIndices[i + 1] != null ? segmentEndIndices[i + 1] : path.length - 1; |
|
|
|
} |
|
|
|
const toNextSlice = path.slice(exitIdx, toNextSliceEndIdx + 1); |
|
|
|
const distToEntry = this.pathSliceDistance(toEntrySlice); |
|
|
|
const holdWpForSegment = waypoints[i + 1]; |
|
|
|
const segTarget = holdWpForSegment && (holdWpForSegment.segmentTargetMinutes ?? holdWpForSegment.displayStyle?.segmentTargetMinutes); |
|
|
|
@ -4798,6 +5105,8 @@ export default { |
|
|
|
const travelToEntryMin = (distToEntry / 1000) * (60 / speedKmhForLeg); |
|
|
|
arrivalEntry = effectiveTime[i] + travelToEntryMin; |
|
|
|
} |
|
|
|
// 定时盘旋等若配置了早于本段起飞的 target,会与上一航段(尤其上一段盘旋)结束时刻冲突,导致两段 hold 在时间上重叠 |
|
|
|
arrivalEntry = Math.max(arrivalEntry, effectiveTime[i]); |
|
|
|
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; |
|
|
|
@ -4823,18 +5132,29 @@ export default { |
|
|
|
? [{ ...holdClosedLoopPath[0] }, ...holdEntryToExitRaw.slice(1)] |
|
|
|
: holdEntryToExitRaw; |
|
|
|
const holdExitDistanceOnLoop = this.pathSliceDistance(holdEntryToExitSlice); |
|
|
|
const holdSpeedKmh = points[i + 1].speed || 800; |
|
|
|
const HOLD_SPEED_KMH = 800; |
|
|
|
const holdParamsForSpeed = this.parseHoldParams(holdWpForSegment); |
|
|
|
const holdSpeedFromParams = Number(holdParamsForSpeed && holdParamsForSpeed.holdSpeed); |
|
|
|
const HOLD_SPEED_KMH = (Number.isFinite(holdSpeedFromParams) && holdSpeedFromParams > 0) |
|
|
|
? holdSpeedFromParams |
|
|
|
: (points[i + 1].speed || 800); |
|
|
|
const holdSpeedKmh = HOLD_SPEED_KMH; |
|
|
|
const speedMpMin = (HOLD_SPEED_KMH * 1000) / 60; |
|
|
|
const requiredDistAtK10 = (holdEndTime - arrivalEntry) * speedMpMin; |
|
|
|
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) { |
|
|
|
const geometricDist = this.pathSliceDistance(holdPathSlice) || 0; |
|
|
|
const baseExitDistance = Math.max(0.1, holdExitDistanceOnLoop || geometricDist || 0); |
|
|
|
const loopLen = Math.max(0.1, holdLoopLength || geometricDist || 0); |
|
|
|
const targetHoldMinutes = Math.max(0, holdEndTime - arrivalEntry); |
|
|
|
const targetHoldDist = targetHoldMinutes * speedMpMin; |
|
|
|
// 盘旋段不得早于航点相对 K 时切出;若基础轨迹不足则继续整圈盘旋后再切出。 |
|
|
|
let holdN = 0; |
|
|
|
if (targetHoldDist > baseExitDistance + 1e-6) { |
|
|
|
holdN = Math.ceil((targetHoldDist - baseExitDistance) / loopLen); |
|
|
|
} |
|
|
|
const holdTotalDist = baseExitDistance + holdN * loopLen; |
|
|
|
const segmentEndTime = arrivalEntry + holdTotalDist / speedMpMin; |
|
|
|
if (segmentEndTime > holdEndTime + timeTolMin) { |
|
|
|
const delaySec = Math.round((segmentEndTime - holdEndTime) * 60); |
|
|
|
const holdWp = waypoints[i + 1]; |
|
|
|
warnings.push(`盘旋「${holdWp.name || 'WP' + (i + 2)}」:到设定时间时未在切出点,继续盘旋至切出点,实际切出将延迟 ${delaySec} 秒。`); |
|
|
|
warnings.push(`盘旋「${holdWp.name || 'WP' + (i + 2)}」:按几何轨迹飞完需晚于该航点相对K时,实际切出将延迟约 ${delaySec} 秒。`); |
|
|
|
holdDelayConflicts.push({ |
|
|
|
legIndex: i, |
|
|
|
holdCenter: holdWp ? { lng: parseFloat(holdWp.lng), lat: parseFloat(holdWp.lat), alt: Number(holdWp.alt) || 0 } : null, |
|
|
|
@ -4843,14 +5163,55 @@ export default { |
|
|
|
delayMinutes: segmentEndTime - holdEndTime, |
|
|
|
delaySeconds: delaySec, |
|
|
|
fromName: waypoints[i].name, |
|
|
|
toName: (waypoints[i + 1] && waypoints[i + 1].name) ? waypoints[i + 1].name : `盘旋${i + 2}` |
|
|
|
toName: (waypoints[i + 1] && waypoints[i + 1].name) ? waypoints[i + 1].name : `盘旋${i + 2}`, |
|
|
|
arrivalEntryTime: arrivalEntry, |
|
|
|
holdSpeedKmh: HOLD_SPEED_KMH, |
|
|
|
holdTotalDistM: holdTotalDist, |
|
|
|
holdBaseExitDistM: baseExitDistance, |
|
|
|
holdLoopLengthM: loopLen, |
|
|
|
holdN, |
|
|
|
prevLegStartTime: effectiveTime[i], |
|
|
|
prevLegDistanceM: distToEntry, |
|
|
|
prevLegSpeedKmh: speedKmhForLeg, |
|
|
|
holdSegmentMode: holdWpForSegment && holdWpForSegment.segmentMode ? holdWpForSegment.segmentMode : null, |
|
|
|
turnAngleDeg: holdWpForSegment && holdWpForSegment.turnAngle != null ? Number(holdWpForSegment.turnAngle) : null |
|
|
|
}); |
|
|
|
} |
|
|
|
const distExitToNext = this.pathSliceDistance(toNextSlice); |
|
|
|
const travelExitMin = (distExitToNext / 1000) * (60 / holdSpeedKmh); |
|
|
|
const arrivalNext = segmentEndTime + travelExitMin; |
|
|
|
if (i + 2 < points.length) { |
|
|
|
const scheduledNext = points[i + 2].minutes; |
|
|
|
if (arrivalNext > scheduledNext + timeTolMin) { |
|
|
|
const availableMin = scheduledNext - segmentEndTime; |
|
|
|
let requiredSpeedKmh = null; |
|
|
|
if (availableMin > 0.001 && distExitToNext > 0) { |
|
|
|
requiredSpeedKmh = (distExitToNext / 1000) / (availableMin / 60); |
|
|
|
} |
|
|
|
lateArrivalLegs.push({ |
|
|
|
legIndex: i + 1, |
|
|
|
fromName: (waypoints[i + 1] && waypoints[i + 1].name) ? waypoints[i + 1].name : `WP${i + 2}`, |
|
|
|
toName: (waypoints[i + 2] && waypoints[i + 2].name) ? waypoints[i + 2].name : `WP${i + 3}`, |
|
|
|
requiredSpeedKmh: (requiredSpeedKmh != null && Number.isFinite(requiredSpeedKmh)) ? Math.ceil(requiredSpeedKmh) : null, |
|
|
|
speedKmh: holdSpeedKmh, |
|
|
|
actualArrival: arrivalNext, |
|
|
|
scheduled: scheduledNext |
|
|
|
}); |
|
|
|
} else if (arrivalNext < scheduledNext - timeTolMin) { |
|
|
|
// 与普通航段一致:提前到达时在航点等待至计划时间。 |
|
|
|
const nextPos = toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos; |
|
|
|
segments.push({ |
|
|
|
startTime: arrivalNext, |
|
|
|
endTime: scheduledNext, |
|
|
|
startPos: nextPos, |
|
|
|
endPos: nextPos, |
|
|
|
type: 'wait', |
|
|
|
legIndex: i + 1 |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
effectiveTime[i + 1] = segmentEndTime; |
|
|
|
if (i + 2 < points.length) effectiveTime[i + 2] = arrivalNext; |
|
|
|
if (i + 2 < points.length) effectiveTime[i + 2] = Math.max(arrivalNext, points[i + 2].minutes); |
|
|
|
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]; |
|
|
|
@ -4878,7 +5239,8 @@ export default { |
|
|
|
holdLoopLength, |
|
|
|
holdExitDistanceOnLoop, |
|
|
|
holdEntryToExitPath, |
|
|
|
holdN: n, |
|
|
|
holdN, |
|
|
|
holdGeometricDist: holdTotalDist, |
|
|
|
speedKmh: HOLD_SPEED_KMH, |
|
|
|
holdEndTime, |
|
|
|
holdCenter, |
|
|
|
@ -4887,10 +5249,8 @@ export default { |
|
|
|
holdClockwise, |
|
|
|
holdEntryAngle |
|
|
|
}); |
|
|
|
// 出口→下一航点的 fly 段 |
|
|
|
// 出口→下一航点的 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 { |
|
|
|
@ -5038,6 +5398,14 @@ export default { |
|
|
|
} |
|
|
|
return this.getPositionAlongPathSlice(s.holdClosedLoopPath, distToExit / s.holdLoopLength); |
|
|
|
} |
|
|
|
// 兜底:缺少闭环参数时按整条 holdPath 弧长插值 |
|
|
|
if (s.holdGeometricDist != null && s.holdGeometricDist > 0 && s.speedKmh != null) { |
|
|
|
const speedMpMin = (s.speedKmh * 1000) / 60; |
|
|
|
const distM = (minutesFromK - s.startTime) * speedMpMin; |
|
|
|
if (distM >= s.holdGeometricDist) return s.endPos; |
|
|
|
const tPath = Math.max(0, Math.min(1, distM / s.holdGeometricDist)); |
|
|
|
return this.getPositionAlongPathSlice(s.holdPath, tPath); |
|
|
|
} |
|
|
|
return this.getPositionAlongPathSlice(s.holdPath, t); |
|
|
|
} |
|
|
|
if (s.type === 'fly' && s.pathSlice && s.pathSlice.length) { |
|
|
|
@ -5059,19 +5427,30 @@ export default { |
|
|
|
return last.endPos; |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* 与 getPositionAtMinutesFromK 使用相同规则构建 pathData,保证甘特图/冲突检测与地图推演几何一致。 |
|
|
|
* 必须与推演首次取位一致:不传 holdRadiusByLegIndex(避免缓存半径与默认公式半径混用导致盘旋弧长、甘特条与飞机位置错位)。 |
|
|
|
* 仅当地图上已有椭圆拟合缓存时传入 holdEllipseParamsByLegIndex(与推演相同)。 |
|
|
|
*/ |
|
|
|
buildPathDataForRouteTimeline(waypoints, routeId) { |
|
|
|
const cesiumMap = this.$refs.cesiumMap; |
|
|
|
if (!waypoints || waypoints.length === 0 || !cesiumMap || !cesiumMap.getRoutePathWithSegmentIndices) return null; |
|
|
|
const cachedEllipse = (routeId != null && cesiumMap._routeHoldEllipseParamsByRoute && cesiumMap._routeHoldEllipseParamsByRoute[routeId]) |
|
|
|
? cesiumMap._routeHoldEllipseParamsByRoute[routeId] |
|
|
|
: {}; |
|
|
|
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) { |
|
|
|
return { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} }; |
|
|
|
} |
|
|
|
return null; |
|
|
|
}, |
|
|
|
|
|
|
|
/** 根据当前推演时间(相对 K 的分钟)得到平台位置,使用速度与等待逻辑;若有地图 ref 则沿转弯弧/盘旋弧路径运动;盘旋半径由系统根据 k+10 落点反算,使平滑落在切点。routeId 可选,传入时会把计算半径同步给地图以实时渲染盘旋轨迹与切点进入。返回 { position, nextPosition, previousPosition, warnings, earlyArrivalLegs, currentSegment } */ |
|
|
|
getPositionAtMinutesFromK(waypoints, minutesFromK, globalMin, globalMax, routeId) { |
|
|
|
if (!waypoints || waypoints.length === 0) return { position: null, nextPosition: null, previousPosition: null, warnings: [], earlyArrivalLegs: [], currentSegment: null }; |
|
|
|
const cesiumMap = this.$refs.cesiumMap; |
|
|
|
let pathData = null; |
|
|
|
if (cesiumMap && cesiumMap.getRoutePathWithSegmentIndices) { |
|
|
|
const cachedEllipse = (routeId != null && cesiumMap._routeHoldEllipseParamsByRoute && cesiumMap._routeHoldEllipseParamsByRoute[routeId]) ? cesiumMap._routeHoldEllipseParamsByRoute[routeId] : {}; |
|
|
|
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 pathData = this.buildPathDataForRouteTimeline(waypoints, routeId); |
|
|
|
let { segments, warnings, earlyArrivalLegs } = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData); |
|
|
|
// 圆形盘旋:半径固定由速度+坡度公式计算,盘旋时间靠多转圈数解决,不反算半径。 |
|
|
|
// 椭圆/跑道形盘旋:通过反算椭圆参数(semiMajor/semiMinor)来匹配盘旋时间。 |
|
|
|
@ -5677,7 +6056,62 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 冲突操作:时间冲突(提前到达、无法按时到达、盘旋时间不足、航线时间窗重叠)、空间冲突(航迹间隔、平台摆放、禁限区入侵)、频谱冲突 |
|
|
|
buildHoldDelaySuggestion(conf) { |
|
|
|
const delaySec = Number(conf && conf.delaySeconds); |
|
|
|
const delayMin = Number(conf && conf.delayMinutes); |
|
|
|
const setExit = Number(conf && conf.setExitTime); |
|
|
|
const actualExit = Number(conf && conf.actualExitTime); |
|
|
|
const entryTime = Number(conf && conf.arrivalEntryTime); |
|
|
|
const holdBaseDist = Number(conf && conf.holdBaseExitDistM); |
|
|
|
const holdTotalDist = Number(conf && conf.holdTotalDistM); |
|
|
|
const holdSpeed = Number(conf && conf.holdSpeedKmh) || 800; |
|
|
|
const delaySecNum = Number.isFinite(delaySec) |
|
|
|
? Math.max(1, Math.round(delaySec)) |
|
|
|
: Math.max(1, Math.round((Number.isFinite(delayMin) ? delayMin : 0) * 60)); |
|
|
|
const delaySecText = `${delaySecNum} 秒`; |
|
|
|
|
|
|
|
const setK = Number.isFinite(setExit) ? this.minutesToStartTimeWithSeconds(setExit) : '当前K时'; |
|
|
|
const targetK = Number.isFinite(actualExit) |
|
|
|
? this.minutesToStartTimeWithSeconds(actualExit) |
|
|
|
: (Number.isFinite(setExit) && Number.isFinite(delayMin) |
|
|
|
? this.minutesToStartTimeWithSeconds(setExit + delayMin) |
|
|
|
: '更晚时刻'); |
|
|
|
|
|
|
|
let bankLine = '② 调整转弯坡度:将该盘旋点转弯坡度提高 5°~15°(每次 5°),直到实际切出时间不晚于计划相对K时。'; |
|
|
|
if (Number.isFinite(holdBaseDist) && holdBaseDist > 0 && Number.isFinite(setExit) && Number.isFinite(entryTime)) { |
|
|
|
const allocatedDist = Math.max(0, (setExit - entryTime) * (holdSpeed * 1000 / 60)); |
|
|
|
const ratio = allocatedDist > 0 ? (allocatedDist / holdBaseDist) : 0; |
|
|
|
const currentTurnDeg = Number(conf && conf.turnAngleDeg); |
|
|
|
if (ratio > 0 && ratio < 1 && Number.isFinite(currentTurnDeg) && currentTurnDeg > 0 && currentTurnDeg < 85) { |
|
|
|
const tanCur = Math.tan((currentTurnDeg * Math.PI) / 180); |
|
|
|
const tanNeed = tanCur / ratio; |
|
|
|
const targetDeg = Math.atan(tanNeed) * 180 / Math.PI; |
|
|
|
const targetDegClamped = Math.min(85, Math.max(currentTurnDeg + 1, targetDeg)); |
|
|
|
bankLine = `② 调整转弯坡度:建议由 ${Math.round(currentTurnDeg)}° 提高到约 ${Math.round(targetDegClamped)}°(可先调到 ${Math.round(Math.min(targetDegClamped, currentTurnDeg + 10))}° 复测)。`; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
let holdSpeedLine = '③ 调整该点盘旋时间(盘旋速度):提高该盘旋点速度,使实际切出时间不晚于计划相对K时。'; |
|
|
|
if (Number.isFinite(holdTotalDist) && holdTotalDist > 0 && Number.isFinite(setExit) && Number.isFinite(entryTime)) { |
|
|
|
const availableMin = setExit - entryTime; |
|
|
|
if (availableMin > 0.001) { |
|
|
|
const reqHoldSpeed = (holdTotalDist / 1000) / (availableMin / 60); |
|
|
|
const reqHoldSpeedRound = Math.ceil(reqHoldSpeed); |
|
|
|
const delta = Math.max(0, Math.ceil(reqHoldSpeedRound - holdSpeed)); |
|
|
|
holdSpeedLine = `③ 调整该点盘旋时间(盘旋速度):将该盘旋点速度调到 ≥${reqHoldSpeedRound} km/h(当前约 ${Math.round(holdSpeed)} km/h,约 +${delta}),使该盘旋段在计划相对K时前切出。`; |
|
|
|
} else { |
|
|
|
holdSpeedLine = '③ 调整该点盘旋时间:当前可用盘旋时间不足,请优先顺延该点相对K时。'; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return [ |
|
|
|
`① 调整该点相对K时:至少顺延 ${delaySecText}(${setK} → ${targetK})。`, |
|
|
|
bankLine, |
|
|
|
holdSpeedLine |
|
|
|
].join(' '); |
|
|
|
}, |
|
|
|
|
|
|
|
// 冲突操作:时间冲突(提前到达、无法按时到达、盘旋时间不足、航线时间窗重叠)、空间冲突(航迹间隔、平台摆放、禁限区入侵、Safety Column) |
|
|
|
async runConflictCheck() { |
|
|
|
// 防止重复点击导致并发:上一轮还在跑就直接再发起一次(旧结果会被 requestId 丢弃) |
|
|
|
if (!this._conflictWorkerInited) { |
|
|
|
@ -5732,6 +6166,9 @@ export default { |
|
|
|
(lateArrivalLegs || []).forEach(leg => { |
|
|
|
const kTimeStr = leg.actualArrival != null && Number.isFinite(leg.actualArrival) ? this.minutesToStartTime(leg.actualArrival) : ''; |
|
|
|
const part2 = kTimeStr ? ` ② 或将下一航点相对K时调至 ${kTimeStr} 或更晚` : ' ② 或将下一航点相对K时调晚'; |
|
|
|
const speedPart = (leg.requiredSpeedKmh != null && Number.isFinite(leg.requiredSpeedKmh)) |
|
|
|
? `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h` |
|
|
|
: '① 当前可用时间已不足,单纯提速无法满足到达时刻'; |
|
|
|
allRaw.push({ |
|
|
|
type: CONFLICT_TYPE.TIME, |
|
|
|
subType: 'late_arrival', |
|
|
|
@ -5740,11 +6177,12 @@ export default { |
|
|
|
routeIds: [routeId], |
|
|
|
fromWaypoint: leg.fromName, |
|
|
|
toWaypoint: leg.toName, |
|
|
|
suggestion: `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h${part2} ③ 调整上游航段速度或时间`, |
|
|
|
suggestion: `${speedPart}${part2} ③ 调整上游航段速度或时间`, |
|
|
|
severity: 'high' |
|
|
|
}); |
|
|
|
}); |
|
|
|
(holdDelayConflicts || []).forEach(conf => { |
|
|
|
const holdSuggestion = this.buildHoldDelaySuggestion(conf); |
|
|
|
allRaw.push({ |
|
|
|
type: CONFLICT_TYPE.TIME, |
|
|
|
subType: 'hold_delay', |
|
|
|
@ -5755,7 +6193,7 @@ export default { |
|
|
|
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时`, |
|
|
|
suggestion: holdSuggestion, |
|
|
|
severity: 'high', |
|
|
|
holdCenter: conf.holdCenter, |
|
|
|
positionLng: conf.holdCenter && conf.holdCenter.lng, |
|
|
|
@ -5764,13 +6202,7 @@ export default { |
|
|
|
}); |
|
|
|
}); |
|
|
|
} 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 pathData = this.buildPathDataForRouteTimeline(route.waypoints, routeId); |
|
|
|
const timeline = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData); |
|
|
|
routeIdToTimeline[routeId] = { |
|
|
|
segments: timeline.segments, |
|
|
|
@ -5807,6 +6239,9 @@ export default { |
|
|
|
(lateArrivalLegs || []).forEach(leg => { |
|
|
|
const kTimeStr = leg.actualArrival != null && Number.isFinite(leg.actualArrival) ? this.minutesToStartTime(leg.actualArrival) : ''; |
|
|
|
const part2 = kTimeStr ? ` ② 或将下一航点相对K时调至 ${kTimeStr} 或更晚` : ' ② 或将下一航点相对K时调晚'; |
|
|
|
const speedPart = (leg.requiredSpeedKmh != null && Number.isFinite(leg.requiredSpeedKmh)) |
|
|
|
? `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h` |
|
|
|
: '① 当前可用时间已不足,单纯提速无法满足到达时刻'; |
|
|
|
allRaw.push({ |
|
|
|
type: CONFLICT_TYPE.TIME, |
|
|
|
subType: 'late_arrival', |
|
|
|
@ -5815,11 +6250,12 @@ export default { |
|
|
|
routeIds: [routeId], |
|
|
|
fromWaypoint: leg.fromName, |
|
|
|
toWaypoint: leg.toName, |
|
|
|
suggestion: `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h${part2} ③ 调整上游航段速度或时间`, |
|
|
|
suggestion: `${speedPart}${part2} ③ 调整上游航段速度或时间`, |
|
|
|
severity: 'high' |
|
|
|
}); |
|
|
|
}); |
|
|
|
(holdDelayConflicts || []).forEach(conf => { |
|
|
|
const holdSuggestion = this.buildHoldDelaySuggestion(conf); |
|
|
|
allRaw.push({ |
|
|
|
type: CONFLICT_TYPE.TIME, |
|
|
|
subType: 'hold_delay', |
|
|
|
@ -5830,7 +6266,7 @@ export default { |
|
|
|
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时`, |
|
|
|
suggestion: holdSuggestion, |
|
|
|
severity: 'high', |
|
|
|
holdCenter: conf.holdCenter, |
|
|
|
positionLng: conf.holdCenter && conf.holdCenter.lng, |
|
|
|
@ -5844,7 +6280,7 @@ export default { |
|
|
|
if (idx % 8 === 7) await new Promise(r => setTimeout(r, 0)); |
|
|
|
} |
|
|
|
|
|
|
|
// ---------- 空间/禁限区/频谱冲突:交给 worker ---------- |
|
|
|
// ---------- 空间/禁限区冲突:交给 worker ---------- |
|
|
|
const platformIcons = (this.$refs.cesiumMap && this.$refs.cesiumMap.getPlatformIconPositions) |
|
|
|
? this.$refs.cesiumMap.getPlatformIconPositions() |
|
|
|
: []; |
|
|
|
@ -5852,6 +6288,12 @@ export default { |
|
|
|
? (((this.$refs.cesiumMap.getFrontendDrawingsData() || {}).entities) || []) |
|
|
|
: []; |
|
|
|
|
|
|
|
const routeNamesById = {} |
|
|
|
routeIdsWithTimeline.forEach((rid) => { |
|
|
|
const rr = this.routes.find(r => r.id === rid) |
|
|
|
routeNamesById[rid] = (rr && rr.name) ? rr.name : `航线${rid}` |
|
|
|
}) |
|
|
|
|
|
|
|
const workerRaw = await this._runConflictWorkerOnce({ |
|
|
|
requestId, |
|
|
|
routeIds: routeIdsWithTimeline, |
|
|
|
@ -5859,9 +6301,9 @@ export default { |
|
|
|
maxMinutes, |
|
|
|
config, |
|
|
|
routeTimelines: routeIdToTimeline, |
|
|
|
routeNamesById, |
|
|
|
platformIcons, |
|
|
|
drawingsEntities, |
|
|
|
spectrumLedger: this.spectrumLedger || [] |
|
|
|
drawingsEntities |
|
|
|
}); |
|
|
|
|
|
|
|
if (workerRaw && Array.isArray(workerRaw)) { |
|
|
|
@ -5943,7 +6385,9 @@ export default { |
|
|
|
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))}` |
|
|
|
const holdRaw = w.holdParams != null ? w.holdParams : w.hold_params |
|
|
|
const holdStr = typeof holdRaw === 'string' ? holdRaw : (holdRaw != null ? JSON.stringify(holdRaw) : '') |
|
|
|
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))},${take(holdStr)}` |
|
|
|
for (let k = 0; k < s.length; k++) { |
|
|
|
h = ((h << 5) + h) ^ s.charCodeAt(k); |
|
|
|
} |
|
|
|
|