|
|
|
@ -14,6 +14,7 @@ |
|
|
|
:scaleConfig="scaleConfig" |
|
|
|
:coordinateFormat="coordinateFormat" |
|
|
|
:bottomPanelVisible="bottomPanelVisible" |
|
|
|
:map-drag-enabled="mapDragEnabled" |
|
|
|
:hide-map-info="sixStepsOverlayVisible" |
|
|
|
:route-locked="routeLocked" |
|
|
|
:route-locked-by-other-ids="routeLockedByOtherRouteIds" |
|
|
|
@ -99,7 +100,9 @@ |
|
|
|
:user-avatar="userAvatar" |
|
|
|
:is-icon-edit-mode="isIconEditMode" |
|
|
|
:current-scale-config="scaleConfig" |
|
|
|
:map-drag-enabled="mapDragEnabled" |
|
|
|
@select-nav="selectTopNav" |
|
|
|
@toggle-map-drag="mapDragEnabled = !mapDragEnabled" |
|
|
|
@set-k-time="openKTimeSetDialog" |
|
|
|
@save-plan="savePlan" |
|
|
|
@import-plan-file="importPlanFile" |
|
|
|
@ -471,6 +474,8 @@ export default { |
|
|
|
return { |
|
|
|
drawDom:false, |
|
|
|
airspaceDrawDom:false, |
|
|
|
/** 是否允许地图拖动(由顶部小手图标切换,默认关闭) */ |
|
|
|
mapDragEnabled: false, |
|
|
|
// 在线成员弹窗 |
|
|
|
showOnlineMembers: false, |
|
|
|
// 编辑弹窗控制 |
|
|
|
@ -1507,6 +1512,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 { |
|
|
|
@ -1681,11 +1687,11 @@ export default { |
|
|
|
// 关闭弹窗 |
|
|
|
this.showPlatformDialog = false; |
|
|
|
}, |
|
|
|
/** 新建航线时写入数据库的默认样式(与地图默认显示一致:墨绿色实线线宽3) */ |
|
|
|
/** 新建航线时写入数据库的默认样式(简约风格:灰蓝配色、细描边) */ |
|
|
|
getDefaultRouteAttributes() { |
|
|
|
const defaultAttrs = { |
|
|
|
waypointStyle: { pixelSize: 7, color: '#ffffff', outlineColor: '#0078FF', outlineWidth: 2 }, |
|
|
|
lineStyle: { style: 'solid', width: 3, color: '#2E5C3E', gapColor: '#000000', dashLength: 20 } |
|
|
|
waypointStyle: { pixelSize: 10, color: '#f1f5f9', outlineColor: '#64748b', outlineWidth: 1.5 }, |
|
|
|
lineStyle: { style: 'solid', width: 3, color: '#64748b', gapColor: '#cbd5e1', dashLength: 20 } |
|
|
|
}; |
|
|
|
return JSON.stringify(defaultAttrs); |
|
|
|
}, |
|
|
|
@ -1769,6 +1775,9 @@ export default { |
|
|
|
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.pixelSize != null) payload.pixelSize = wp.pixelSize; |
|
|
|
if (wp.color != null) payload.color = wp.color; |
|
|
|
if (wp.outlineColor != null) payload.outlineColor = wp.outlineColor; |
|
|
|
if (payload.turnAngle > 0 && this.$refs.cesiumMap) { |
|
|
|
payload.turnRadius = this.$refs.cesiumMap.getWaypointRadius(payload); |
|
|
|
} else { |
|
|
|
@ -2143,9 +2152,14 @@ export default { |
|
|
|
if (updatedWaypoint.holdParams != null) payload.holdParams = updatedWaypoint.holdParams; |
|
|
|
if (updatedWaypoint.labelFontSize != null) payload.labelFontSize = updatedWaypoint.labelFontSize; |
|
|
|
if (updatedWaypoint.labelColor != null) payload.labelColor = updatedWaypoint.labelColor; |
|
|
|
if (updatedWaypoint.pixelSize != null) payload.pixelSize = updatedWaypoint.pixelSize; |
|
|
|
if (updatedWaypoint.color != null) payload.color = updatedWaypoint.color; |
|
|
|
if (updatedWaypoint.outlineColor != null) payload.outlineColor = updatedWaypoint.outlineColor; |
|
|
|
const response = await updateWaypoints(payload); |
|
|
|
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 }); |
|
|
|
@ -2156,8 +2170,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 }); |
|
|
|
@ -3217,8 +3229,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) => ({ |
|
|
|
@ -3252,6 +3265,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 || {}; |
|
|
|
@ -3267,20 +3281,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 |
|
|
|
@ -3289,13 +3336,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, |
|
|
|
@ -3303,7 +3352,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; |
|
|
|
} |
|
|
|
@ -3349,7 +3398,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)取点:按弧长比例插值 */ |
|
|
|
@ -3401,24 +3450,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); |
|
|
|
@ -3439,17 +3478,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); |
|
|
|
@ -3498,7 +3645,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; |
|
|
|
@ -3687,6 +3834,7 @@ export default { |
|
|
|
} |
|
|
|
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes)); |
|
|
|
} |
|
|
|
this.$nextTick(() => this.updateDeductionPositions()); |
|
|
|
} catch (_) {} |
|
|
|
} |
|
|
|
const firstId = planRouteIds[0]; |
|
|
|
@ -3789,6 +3937,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('该航线暂无坐标数据,无法在地图展示'); |
|
|
|
@ -3967,7 +4116,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 => { |
|
|
|
@ -3993,6 +4142,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; |
|
|
|
|