|
|
|
@ -623,6 +623,8 @@ export default { |
|
|
|
plans: [], |
|
|
|
activeRightTab: 'plan', |
|
|
|
activeRouteIds: [], // 存储当前所有选中的航线ID |
|
|
|
/** 新加入时收到的房间可见航线 ID,若 routes 未加载则暂存,getList 完成后应用 */ |
|
|
|
roomStatePendingVisibleRouteIds: [], |
|
|
|
/** 航线上锁状态:routeId -> true 上锁,与地图右键及右侧列表锁图标同步 */ |
|
|
|
routeLocked: {}, |
|
|
|
// 冲突数据(由 runConflictCheck 根据当前航线与时间轴计算真实问题) |
|
|
|
@ -1417,6 +1419,9 @@ export default { |
|
|
|
if (this.isMySyncSession(senderSessionId)) return; |
|
|
|
this.applySyncPlatformStyles(); |
|
|
|
}, |
|
|
|
onRoomState: (visibleRouteIds) => { |
|
|
|
this.applyRoomStateVisibleRoutes(visibleRouteIds); |
|
|
|
}, |
|
|
|
onConnected: () => {}, |
|
|
|
onDisconnected: () => { |
|
|
|
this.onlineCount = 0; |
|
|
|
@ -1436,12 +1441,31 @@ export default { |
|
|
|
this.wsConnection.disconnect(); |
|
|
|
this.wsConnection = null; |
|
|
|
} |
|
|
|
this.roomStatePendingVisibleRouteIds = []; |
|
|
|
this.wsOnlineMembers = []; |
|
|
|
this.mySyncSessionIds = []; |
|
|
|
this.onlineCount = 0; |
|
|
|
this.chatMessages = []; |
|
|
|
this.privateChatMessages = {}; |
|
|
|
}, |
|
|
|
/** 应用房间状态中的可见航线(新加入用户同步;若 routes 未加载则暂存) */ |
|
|
|
async applyRoomStateVisibleRoutes(visibleRouteIds) { |
|
|
|
if (!visibleRouteIds || !Array.isArray(visibleRouteIds) || visibleRouteIds.length === 0) return; |
|
|
|
if (!this.routes || this.routes.length === 0) { |
|
|
|
this.roomStatePendingVisibleRouteIds = visibleRouteIds; |
|
|
|
return; |
|
|
|
} |
|
|
|
for (const routeId of visibleRouteIds) { |
|
|
|
await this.applySyncRouteVisibility(routeId, true); |
|
|
|
} |
|
|
|
}, |
|
|
|
/** 应用暂存的房间可见航线(getList 完成后调用) */ |
|
|
|
async applyRoomStatePending() { |
|
|
|
if (this.roomStatePendingVisibleRouteIds.length === 0) return; |
|
|
|
const pending = [...this.roomStatePendingVisibleRouteIds]; |
|
|
|
this.roomStatePendingVisibleRouteIds = []; |
|
|
|
await this.applyRoomStateVisibleRoutes(pending); |
|
|
|
}, |
|
|
|
/** 判断是否为当前连接发出的同步消息(避免自己发的消息再应用一次) */ |
|
|
|
isMySyncSession(senderSessionId) { |
|
|
|
if (!senderSessionId) return false; |
|
|
|
@ -1480,6 +1504,7 @@ export default { |
|
|
|
} catch (_) {} |
|
|
|
} |
|
|
|
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes)); |
|
|
|
this.$nextTick(() => this.updateDeductionPositions()); |
|
|
|
} |
|
|
|
} catch (_) {} |
|
|
|
} else { |
|
|
|
@ -1920,6 +1945,7 @@ export default { |
|
|
|
}).catch(() => {}); |
|
|
|
} |
|
|
|
} |
|
|
|
this.$nextTick(() => this.applyRoomStatePending()); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.error("数据加载失败:", error); |
|
|
|
@ -2117,7 +2143,9 @@ export default { |
|
|
|
if (updatedWaypoint.labelColor != null) payload.labelColor = updatedWaypoint.labelColor; |
|
|
|
const response = await updateWaypoints(payload); |
|
|
|
if (response.code === 200) { |
|
|
|
const index = this.selectedRouteDetails.waypoints.findIndex(p => p.id === updatedWaypoint.id); |
|
|
|
const roomId = this.currentRoomId; |
|
|
|
const sd = this.selectedRouteDetails; |
|
|
|
const index = sd.waypoints.findIndex(p => p.id === updatedWaypoint.id); |
|
|
|
if (index !== -1) { |
|
|
|
// 更新本地数据(用已提交的 payload 保证 startTime 等与数据库一致) |
|
|
|
this.selectedRouteDetails.waypoints.splice(index, 1, { ...updatedWaypoint, ...payload }); |
|
|
|
@ -2128,8 +2156,6 @@ export default { |
|
|
|
if (idxInList !== -1) routeInList.waypoints.splice(idxInList, 1, merged); |
|
|
|
} |
|
|
|
if (this.$refs.cesiumMap) { |
|
|
|
const roomId = this.currentRoomId; |
|
|
|
const sd = this.selectedRouteDetails; |
|
|
|
if (roomId && sd.platformId) { |
|
|
|
try { |
|
|
|
const styleRes = await getPlatformStyle({ roomId, routeId: sd.id, platformId: sd.platformId }); |
|
|
|
@ -3189,8 +3215,9 @@ export default { |
|
|
|
/** |
|
|
|
* 按速度与计划时间构建航线时间轴:含飞行段、盘旋段与“提前到达则等待”的等待段。 |
|
|
|
* pathData 可选:{ path, segmentEndIndices, holdArcRanges },由 getRoutePathWithSegmentIndices 提供,用于输出 hold 段。 |
|
|
|
* holdRadiusByLegIndex 可选:{ [legIndex]: number },为盘旋段指定半径(用于推演时落点精准在切点)。 |
|
|
|
*/ |
|
|
|
buildRouteTimeline(waypoints, globalMin, globalMax, pathData) { |
|
|
|
buildRouteTimeline(waypoints, globalMin, globalMax, pathData, holdRadiusByLegIndex) { |
|
|
|
const warnings = []; |
|
|
|
if (!waypoints || waypoints.length === 0) return { segments: [], warnings }; |
|
|
|
const points = waypoints.map((wp, idx) => ({ |
|
|
|
@ -3224,6 +3251,7 @@ export default { |
|
|
|
const effectiveTime = [points[0].minutes]; |
|
|
|
const segments = []; |
|
|
|
const lateArrivalLegs = []; // 无法按时到达的航段,供冲突检测用 |
|
|
|
const holdDelayConflicts = []; // 盘旋时间不足:设定时间到但位置未到切出点,实际切出顺延 |
|
|
|
const path = pathData && pathData.path; |
|
|
|
const segmentEndIndices = pathData && pathData.segmentEndIndices; |
|
|
|
const holdArcRanges = pathData && pathData.holdArcRanges || {}; |
|
|
|
@ -3239,20 +3267,53 @@ export default { |
|
|
|
const speedKmh = points[i].speed || 800; |
|
|
|
const travelToEntryMin = (distToEntry / 1000) * (60 / speedKmh); |
|
|
|
const arrivalEntry = effectiveTime[i] + travelToEntryMin; |
|
|
|
const holdEndTime = points[i + 1].minutes; |
|
|
|
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, 120); k++) { |
|
|
|
if (this.segmentDistance(holdPathSlice[0], holdPathSlice[k]) < 80) { loopEndIdx = k; break; } |
|
|
|
} |
|
|
|
const holdClosedLoopPath = holdPathSlice.slice(0, loopEndIdx + 1); |
|
|
|
const holdLoopLength = this.pathSliceDistance(holdClosedLoopPath) || 1; |
|
|
|
let exitIdxOnLoop = 0; |
|
|
|
let minD = 1e9; |
|
|
|
for (let k = 0; k <= loopEndIdx; 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 speedMpMin = (speedKmh * 1000) / 60; |
|
|
|
const requiredDistAtK10 = (holdEndTime - arrivalEntry) * speedMpMin; |
|
|
|
let n = Math.ceil((requiredDistAtK10 - holdExitDistanceOnLoop) / holdLoopLength); |
|
|
|
if (n < 0 || !Number.isFinite(n)) n = 0; |
|
|
|
const segmentEndTime = arrivalEntry + (holdExitDistanceOnLoop + n * holdLoopLength) / speedMpMin; |
|
|
|
if (segmentEndTime > holdEndTime) { |
|
|
|
const delaySec = Math.round((segmentEndTime - holdEndTime) * 60); |
|
|
|
const holdWp = waypoints[i + 1]; |
|
|
|
warnings.push(`盘旋「${holdWp.name || 'WP' + (i + 2)}」:到设定时间时未在切出点,继续盘旋至切出点,实际切出将延迟 ${delaySec} 秒。`); |
|
|
|
holdDelayConflicts.push({ |
|
|
|
legIndex: i, |
|
|
|
holdCenter: holdWp ? { lng: parseFloat(holdWp.lng), lat: parseFloat(holdWp.lat), alt: Number(holdWp.alt) || 0 } : null, |
|
|
|
setExitTime: holdEndTime, |
|
|
|
actualExitTime: segmentEndTime, |
|
|
|
delayMinutes: segmentEndTime - holdEndTime, |
|
|
|
delaySeconds: delaySec, |
|
|
|
fromName: waypoints[i].name, |
|
|
|
toName: (waypoints[i + 1] && waypoints[i + 1].name) ? waypoints[i + 1].name : `盘旋${i + 2}` |
|
|
|
}); |
|
|
|
} |
|
|
|
const distExitToNext = this.pathSliceDistance(toNextSlice); |
|
|
|
const travelExitMin = (distExitToNext / 1000) * (60 / speedKmh); |
|
|
|
const arrivalNext = holdEndTime + travelExitMin; |
|
|
|
const arrivalNext = segmentEndTime + travelExitMin; |
|
|
|
effectiveTime[i + 1] = holdEndTime; |
|
|
|
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 exitPos = holdPathSlice.length ? holdPathSlice[holdPathSlice.length - 1] : entryPos; |
|
|
|
const holdDurationMin = holdEndTime - arrivalEntry; |
|
|
|
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 holdRadius = holdParams && holdParams.radius != null ? holdParams.radius : 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 holdClockwise = holdParams && holdParams.clockwise !== false; |
|
|
|
const holdCircumference = holdRadius != null ? 2 * Math.PI * holdRadius : null; |
|
|
|
const holdEntryAngle = holdCenter && entryPos && holdRadius != null |
|
|
|
@ -3261,13 +3322,15 @@ export default { |
|
|
|
segments.push({ startTime: effectiveTime[i], endTime: arrivalEntry, startPos: posCur, endPos: entryPos, type: 'fly', legIndex: i, pathSlice: toEntrySlice }); |
|
|
|
segments.push({ |
|
|
|
startTime: arrivalEntry, |
|
|
|
endTime: holdEndTime, |
|
|
|
endTime: segmentEndTime, |
|
|
|
startPos: entryPos, |
|
|
|
endPos: exitPos, |
|
|
|
type: 'hold', |
|
|
|
legIndex: i, |
|
|
|
holdPath: holdPathSlice, |
|
|
|
holdDurationMin, |
|
|
|
holdClosedLoopPath, |
|
|
|
holdLoopLength, |
|
|
|
holdExitDistanceOnLoop, |
|
|
|
speedKmh: points[i].speed || 800, |
|
|
|
holdCenter, |
|
|
|
holdRadius, |
|
|
|
@ -3275,7 +3338,7 @@ export default { |
|
|
|
holdClockwise, |
|
|
|
holdEntryAngle |
|
|
|
}); |
|
|
|
segments.push({ startTime: holdEndTime, endTime: arrivalNext, startPos: exitPos, endPos: toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos, type: 'fly', legIndex: i, pathSlice: toNextSlice }); |
|
|
|
segments.push({ startTime: segmentEndTime, endTime: arrivalNext, startPos: exitPos, endPos: toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos, type: 'fly', legIndex: i, pathSlice: toNextSlice }); |
|
|
|
i++; |
|
|
|
continue; |
|
|
|
} |
|
|
|
@ -3321,7 +3384,7 @@ export default { |
|
|
|
earlyArrivalLegs.push({ legIndex: i, scheduled, actualArrival, fromName: waypoints[i].name, toName: waypoints[i + 1].name }); |
|
|
|
} |
|
|
|
} |
|
|
|
return { segments, warnings, earlyArrivalLegs, lateArrivalLegs }; |
|
|
|
return { segments, warnings, earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts }; |
|
|
|
}, |
|
|
|
|
|
|
|
/** 从路径片段中按比例 t(0~1)取点:按弧长比例插值 */ |
|
|
|
@ -3373,24 +3436,14 @@ export default { |
|
|
|
return s.startPos; |
|
|
|
} |
|
|
|
if (s.type === 'hold' && s.holdPath && s.holdPath.length) { |
|
|
|
const durationMin = s.holdDurationMin != null ? s.holdDurationMin : (s.endTime - s.startTime); |
|
|
|
const speedKmh = s.speedKmh != null ? s.speedKmh : 800; |
|
|
|
const totalHoldDistM = (speedKmh * (durationMin / 60)) * 1000; |
|
|
|
if (s.holdCircumference != null && s.holdCircumference > 0 && s.holdCenter && s.holdRadius != null) { |
|
|
|
const currentDistM = t * totalHoldDistM; |
|
|
|
const distOnLap = currentDistM % s.holdCircumference; |
|
|
|
const angleRad = (distOnLap / s.holdCircumference) * (2 * Math.PI); |
|
|
|
const signedAngle = s.holdClockwise ? -angleRad : angleRad; |
|
|
|
const entryAngle = s.holdEntryAngle != null ? s.holdEntryAngle : 0; |
|
|
|
const angle = entryAngle + signedAngle; |
|
|
|
return this.positionOnCircle(s.holdCenter.lng, s.holdCenter.lat, s.holdCenter.alt, s.holdRadius, angle); |
|
|
|
// 飞机一直绕闭合环盘旋,不静止;到设定K时若在切出点则切出,否则继续飞到切出点再切出并报冲突 |
|
|
|
if (s.holdClosedLoopPath && s.holdClosedLoopPath.length >= 2 && s.holdLoopLength > 0 && s.speedKmh != null) { |
|
|
|
const distM = (minutesFromK - s.startTime) * (s.speedKmh * 1000 / 60); |
|
|
|
const distOnLoop = ((distM % s.holdLoopLength) + s.holdLoopLength) % s.holdLoopLength; |
|
|
|
const tPath = distOnLoop / s.holdLoopLength; |
|
|
|
return this.getPositionAlongPathSlice(s.holdClosedLoopPath, tPath); |
|
|
|
} |
|
|
|
const holdPathLen = this.pathSliceDistance(s.holdPath); |
|
|
|
if (holdPathLen <= 0) return this.getPositionAlongPathSlice(s.holdPath, t); |
|
|
|
const currentDistM = t * totalHoldDistM; |
|
|
|
const positionOnLap = currentDistM % holdPathLen; |
|
|
|
const tLap = holdPathLen > 0 ? positionOnLap / holdPathLen : 0; |
|
|
|
return this.getPositionAlongPathSlice(s.holdPath, tLap); |
|
|
|
return this.getPositionAlongPathSlice(s.holdPath, t); |
|
|
|
} |
|
|
|
if (s.type === 'fly' && s.pathSlice && s.pathSlice.length) { |
|
|
|
return this.getPositionAlongPathSlice(s.pathSlice, t); |
|
|
|
@ -3411,17 +3464,125 @@ export default { |
|
|
|
return last.endPos; |
|
|
|
}, |
|
|
|
|
|
|
|
/** 根据当前推演时间(相对 K 的分钟)得到平台位置,使用速度与等待逻辑;若有地图 ref 则沿转弯弧/盘旋弧路径运动;返回 { position, nextPosition, previousPosition, warnings, earlyArrivalLegs, currentSegment },currentSegment 含 speedKmh 用于标牌 */ |
|
|
|
getPositionAtMinutesFromK(waypoints, minutesFromK, globalMin, globalMax) { |
|
|
|
/** 根据当前推演时间(相对 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 (this.$refs.cesiumMap && this.$refs.cesiumMap.getRoutePathWithSegmentIndices) { |
|
|
|
const ret = this.$refs.cesiumMap.getRoutePathWithSegmentIndices(waypoints); |
|
|
|
if (cesiumMap && cesiumMap.getRoutePathWithSegmentIndices) { |
|
|
|
const ret = cesiumMap.getRoutePathWithSegmentIndices(waypoints); |
|
|
|
if (ret.path && ret.path.length > 0 && ret.segmentEndIndices && ret.segmentEndIndices.length > 0) { |
|
|
|
pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} }; |
|
|
|
} |
|
|
|
} |
|
|
|
const { segments, warnings, earlyArrivalLegs } = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData); |
|
|
|
let { segments, warnings, earlyArrivalLegs } = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData); |
|
|
|
const holdRadiusByLegIndex = {}; |
|
|
|
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 holdDurationMin = s.holdDurationMin != null ? s.holdDurationMin : (s.endTime - s.startTime); |
|
|
|
const speedKmh = s.speedKmh != null ? s.speedKmh : 800; |
|
|
|
const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000; |
|
|
|
const prevWp = waypoints[i]; |
|
|
|
const holdWp = waypoints[i + 1]; |
|
|
|
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; |
|
|
|
const clockwise = s.holdClockwise !== false; |
|
|
|
const isEllipse = (waypoints[i + 1] && (waypoints[i + 1].pointType || waypoints[i + 1].point_type) === 'hold_ellipse') || s.holdRadius == null; |
|
|
|
if (isEllipse && 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: headingDeg }; |
|
|
|
} else if (!isEllipse && cesiumMap.computeHoldRadiusForDuration) { |
|
|
|
const R = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, clockwise, totalHoldDistM); |
|
|
|
if (R != null && Number.isFinite(R)) holdRadiusByLegIndex[i] = R; |
|
|
|
} |
|
|
|
} |
|
|
|
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 < 2; 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 holdDurationMin = s.holdDurationMin != null ? s.holdDurationMin : (s.endTime - s.startTime); |
|
|
|
const speedKmh = s.speedKmh != null ? s.speedKmh : 800; |
|
|
|
const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000; |
|
|
|
const prevWp = waypoints[i]; |
|
|
|
const holdWp = waypoints[i + 1]; |
|
|
|
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; |
|
|
|
const Rnew = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, s.holdClockwise !== false, totalHoldDistM); |
|
|
|
if (Rnew != null && Number.isFinite(Rnew)) { |
|
|
|
if (holdRadiusByLegIndex[i] == null || Math.abs(Rnew - holdRadiusByLegIndex[i]) > 1) changed = true; |
|
|
|
nextRadii[i] = Rnew; |
|
|
|
} |
|
|
|
} |
|
|
|
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]; |
|
|
|
const holdParams = this.parseHoldParams(holdWp); |
|
|
|
const holdDurationMin = s.holdDurationMin != null ? s.holdDurationMin : (s.endTime - s.startTime); |
|
|
|
const speedKmh = s.speedKmh != null ? s.speedKmh : 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 old = holdEllipseParamsByLegIndex[i]; |
|
|
|
if (!old || Math.abs(out.semiMajor - old.semiMajor) > 1) changed = true; |
|
|
|
holdEllipseParamsByLegIndex[i] = { semiMajor: out.semiMajor, semiMinor: out.semiMinor, headingDeg: headingDeg }; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (!changed || iter === 1) break; |
|
|
|
} |
|
|
|
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); |
|
|
|
} |
|
|
|
} else if (routeId != null) { |
|
|
|
if (cesiumMap.setRouteHoldRadii) cesiumMap.setRouteHoldRadii(routeId, {}); |
|
|
|
if (cesiumMap.setRouteHoldEllipseParams) cesiumMap.setRouteHoldEllipseParams(routeId, {}); |
|
|
|
} |
|
|
|
} |
|
|
|
const path = pathData ? pathData.path : null; |
|
|
|
const segmentEndIndices = pathData ? pathData.segmentEndIndices : null; |
|
|
|
const position = this.getPositionFromTimeline(segments, minutesFromK, path, segmentEndIndices); |
|
|
|
@ -3470,7 +3631,7 @@ export default { |
|
|
|
this.activeRouteIds.forEach(routeId => { |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route || !route.waypoints || route.waypoints.length === 0) return; |
|
|
|
const { position, nextPosition, previousPosition, warnings, earlyArrivalLegs, currentSegment } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes); |
|
|
|
const { position, nextPosition, previousPosition, warnings, earlyArrivalLegs, currentSegment } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes, routeId); |
|
|
|
if (warnings && warnings.length) allWarnings.push(...warnings); |
|
|
|
if (position) { |
|
|
|
const directionPoint = nextPosition || previousPosition; |
|
|
|
@ -3613,25 +3774,73 @@ export default { |
|
|
|
const minutes = absMin % 60; |
|
|
|
return `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`; |
|
|
|
}, |
|
|
|
selectPlan(plan) { |
|
|
|
async selectPlan(plan) { |
|
|
|
if (plan && plan.id) { |
|
|
|
this.selectedPlanId = plan.id; |
|
|
|
this.selectedPlanDetails = plan; |
|
|
|
} else { |
|
|
|
this.selectedPlanId = null; |
|
|
|
this.selectedPlanDetails = null; |
|
|
|
this.selectedRouteId = null; |
|
|
|
this.selectedRouteDetails = null; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 重置状态 |
|
|
|
this.selectedRouteId = null; |
|
|
|
this.selectedRouteDetails = null; |
|
|
|
this.activeRouteIds = []; |
|
|
|
// 清空地图上的航点/航线及探测区、威力区(按 routeId 逐个移除,避免误删其他实体) |
|
|
|
if (this.$refs.cesiumMap) { |
|
|
|
[...this.activeRouteIds].forEach(routeId => { |
|
|
|
this.$refs.cesiumMap.removeRouteById(routeId); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 物理清场(仅航点/航线;空域图形与房间绑定,不随方案切换) |
|
|
|
if (this.$refs.cesiumMap && this.$refs.cesiumMap.clearAllWaypoints) { |
|
|
|
this.$refs.cesiumMap.clearAllWaypoints(); |
|
|
|
// 保留 activeRouteIds,恢复显示该方案下已打开的航线(含联网同步的) |
|
|
|
const planRouteIds = this.activeRouteIds.filter(id => { |
|
|
|
const r = this.routes.find(x => x.id === id); |
|
|
|
return r && r.scenarioId === plan.id; |
|
|
|
}); |
|
|
|
if (planRouteIds.length > 0) { |
|
|
|
for (const routeId of planRouteIds) { |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route) continue; |
|
|
|
try { |
|
|
|
const res = await getRoutes(route.id); |
|
|
|
if (res.code !== 200 || !res.data) continue; |
|
|
|
const waypoints = res.data.waypoints || []; |
|
|
|
const routeIndex = this.routes.findIndex(r => r.id === route.id); |
|
|
|
if (routeIndex > -1) { |
|
|
|
this.$set(this.routes, routeIndex, { ...this.routes[routeIndex], waypoints }); |
|
|
|
} |
|
|
|
if (waypoints.length > 0 && this.$refs.cesiumMap) { |
|
|
|
const roomId = this.currentRoomId; |
|
|
|
if (roomId && route.platformId) { |
|
|
|
try { |
|
|
|
const styleRes = await getPlatformStyle({ roomId, routeId: route.id, platformId: route.platformId }); |
|
|
|
if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(route.id, styleRes.data); |
|
|
|
} catch (_) {} |
|
|
|
} |
|
|
|
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes)); |
|
|
|
} |
|
|
|
this.$nextTick(() => this.updateDeductionPositions()); |
|
|
|
} catch (_) {} |
|
|
|
} |
|
|
|
const firstId = planRouteIds[0]; |
|
|
|
const firstRoute = this.routes.find(r => r.id === firstId); |
|
|
|
if (firstRoute && firstRoute.waypoints) { |
|
|
|
this.selectedRouteId = firstRoute.id; |
|
|
|
this.selectedRouteDetails = { |
|
|
|
id: firstRoute.id, |
|
|
|
name: firstRoute.callSign || firstRoute.name, |
|
|
|
waypoints: firstRoute.waypoints, |
|
|
|
platformId: firstRoute.platformId, |
|
|
|
platform: firstRoute.platform, |
|
|
|
attributes: firstRoute.attributes |
|
|
|
}; |
|
|
|
} |
|
|
|
} else { |
|
|
|
this.selectedRouteId = null; |
|
|
|
this.selectedRouteDetails = null; |
|
|
|
} |
|
|
|
console.log(`>>> [切换成功] 已进入方案: ${plan && plan.name},地图已清空,列表已展开。`); |
|
|
|
console.log(`>>> [切换成功] 已进入方案: ${plan.name},已恢复显示 ${planRouteIds.length} 条航线。`); |
|
|
|
}, |
|
|
|
/** 切换航线:实现多选/开关逻辑 */ |
|
|
|
async selectRoute(route) { |
|
|
|
@ -3714,6 +3923,7 @@ export default { |
|
|
|
} catch (_) {} |
|
|
|
} |
|
|
|
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes)); |
|
|
|
this.$nextTick(() => this.updateDeductionPositions()); |
|
|
|
} |
|
|
|
} else { |
|
|
|
this.$message.warning('该航线暂无坐标数据,无法在地图展示'); |
|
|
|
@ -3892,7 +4102,7 @@ export default { |
|
|
|
pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} }; |
|
|
|
} |
|
|
|
} |
|
|
|
const { earlyArrivalLegs, lateArrivalLegs } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData); |
|
|
|
const { earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData); |
|
|
|
|
|
|
|
const routeName = route.name || `航线${route.id}`; |
|
|
|
(earlyArrivalLegs || []).forEach(leg => { |
|
|
|
@ -3918,6 +4128,20 @@ export default { |
|
|
|
severity: 'high' |
|
|
|
}); |
|
|
|
}); |
|
|
|
(holdDelayConflicts || []).forEach(conf => { |
|
|
|
list.push({ |
|
|
|
id: id++, |
|
|
|
title: '盘旋时间不足', |
|
|
|
routeName, |
|
|
|
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} 秒。`, |
|
|
|
severity: 'high', |
|
|
|
holdCenter: conf.holdCenter |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
this.conflicts = list; |
|
|
|
|