From c1c6712bb9f0a2b3fbc6142288420e8b34e38074 Mon Sep 17 00:00:00 2001 From: menghao <1584479611@qq.com> Date: Tue, 24 Mar 2026 13:19:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=B2=E7=AA=81=E6=A3=80=E6=B5=8B=E5=9C=86?= =?UTF-8?q?=E6=9F=B1=E4=BD=93=E3=80=81=E7=94=98=E7=89=B9=E5=9B=BE=E6=8B=96?= =?UTF-8?q?=E6=8B=BD=E6=94=B9=E6=97=B6=E9=97=B4=E3=80=81=E7=9B=98=E6=97=8B?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-ui/src/lang/en.js | 1 - ruoyi-ui/src/lang/zh.js | 1 - ruoyi-ui/src/utils/conflictDetection.js | 338 ++++++++++---- ruoyi-ui/src/utils/timelinePosition.js | 51 +- ruoyi-ui/src/views/cesiumMap/ContextMenu.vue | 60 +++ ruoyi-ui/src/views/cesiumMap/index.vue | 106 ++++- ruoyi-ui/src/views/childRoom/ConflictDrawer.vue | 3 - ruoyi-ui/src/views/childRoom/GanttDrawer.vue | 595 ++++++++++++++---------- ruoyi-ui/src/views/childRoom/index.vue | 560 +++++++++++++++++++--- ruoyi-ui/src/workers/conflictCheck.worker.js | 69 ++- 10 files changed, 1316 insertions(+), 468 deletions(-) diff --git a/ruoyi-ui/src/lang/en.js b/ruoyi-ui/src/lang/en.js index bc7fd5b..30275b0 100644 --- a/ruoyi-ui/src/lang/en.js +++ b/ruoyi-ui/src/lang/en.js @@ -127,7 +127,6 @@ export default { conflictTypeAll: 'All', conflictTypeTime: 'Time', conflictTypeSpace: 'Space', - conflictTypeSpectrum: 'Spectrum', air: 'Air', sea: 'Sea', ground: 'Ground' diff --git a/ruoyi-ui/src/lang/zh.js b/ruoyi-ui/src/lang/zh.js index eb5e276..b948664 100644 --- a/ruoyi-ui/src/lang/zh.js +++ b/ruoyi-ui/src/lang/zh.js @@ -127,7 +127,6 @@ export default { conflictTypeAll: '全部', conflictTypeTime: '时间', conflictTypeSpace: '空间', - conflictTypeSpectrum: '频谱', air: '空中', sea: '海上', ground: '地面' diff --git a/ruoyi-ui/src/utils/conflictDetection.js b/ruoyi-ui/src/utils/conflictDetection.js index 289e3fd..fb54358 100644 --- a/ruoyi-ui/src/utils/conflictDetection.js +++ b/ruoyi-ui/src/utils/conflictDetection.js @@ -1,15 +1,13 @@ /** - * 冲突检测工具:时间冲突、空间冲突、频谱冲突 + * 冲突检测工具:时间冲突、空间冲突 * - 时间:航线时间窗重叠、无法按时到达、提前到达、盘旋时间不足、资源占用缓冲 - * - 空间:航迹最小间隔、摆放平台距离过小、禁限区入侵(考虑高度带) - * - 频谱:同频/邻频 + 时间 + 带宽 + 地理范围叠加判定,支持频谱资源台账 + * - 空间:航迹最小间隔、摆放平台距离过小、禁限区入侵(考虑高度带)、Safety Column 间距 */ /** 冲突类型 */ export const CONFLICT_TYPE = { TIME: 'time', - SPACE: 'space', - SPECTRUM: 'spectrum' + SPACE: 'space' } /** 默认冲突配置(可由界面配置覆盖) */ @@ -27,9 +25,18 @@ export const defaultConflictConfig = { trackSampleStepMinutes: 1, // 航迹采样步长(分钟) restrictedZoneNameKeywords: ['禁限', '禁区', '限制区'], // 空域名称含这些词视为禁限区 useRestrictedAltitudeBand: true, // 禁限区是否考虑高度带(若空域有 altMin/altMax) - // 频谱 - adjacentFreqGuardMHz: 5, // 邻频保护间隔(MHz),两频段中心差小于此视为邻频冲突 - spectrumConflictGeoOverlap: true // 频谱冲突是否要求地理范围叠加 + /** 隐藏圆柱体(Safety Column):水平半径 R、垂直高度差阈值 H、同点先后时间阈值 ΔT(秒) */ + safetyColumnRadiusMeters: 10000, // R:单柱水平半径(米) + safetyColumnHeightMeters: 300, // H:两机高度差小于此值且水平重叠时视为垂直方向不安全(米) + safetyColumnTimeThresholdSeconds: 60, // ΔT:先后经过同一位置的时间间隔小于此值报时间类冲突(秒) + /** 空间重叠采样步长(分钟),默认 0.5 即 30 秒 */ + safetyColumnSpatialStepMinutes: 0.5, + /** 同点先后判定:时间维采样步长(分钟),默认 5 秒 */ + safetyColumnTemporalStepMinutes: 5 / 60, + /** 判定「同一物理位置」的网格:经纬度量化(度),约百米量级 */ + safetyColumnTemporalGridLngDeg: 0.001, + safetyColumnTemporalGridLatDeg: 0.001, + safetyColumnTemporalGridAltMeters: 100 } /** @@ -87,7 +94,7 @@ export function detectTimeWindowOverlap(routes, waypointStartTimeToMinutes, conf title: '航线时间窗重叠', routeIds: [a.routeId, b.routeId], routeNames: [a.routeName, b.routeName], - time: `K+${formatMinutes(a.min)} ~ K+${formatMinutes(a.max)} 与 K+${formatMinutes(b.min)} ~ K+${formatMinutes(b.max)} 重叠`, + time: `${formatKLabel(a.min)} ~ ${formatKLabel(a.max)} 与 ${formatKLabel(b.min)} ~ ${formatKLabel(b.max)} 重叠`, suggestion: '建议错开两条航线的计划时间窗,或为资源设置缓冲时间后再检测。' }) } @@ -96,13 +103,230 @@ export function detectTimeWindowOverlap(routes, waypointStartTimeToMinutes, conf return list } -function formatMinutes(m) { +export function formatMinutes(m) { const h = Math.floor(Math.abs(m) / 60) const min = Math.floor(Math.abs(m) % 60) const sign = m >= 0 ? '+' : '-' return `${sign}${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}` } +/** 展示用完整 K 时标签:K+00:10(formatMinutes 已含 ±,勿再拼接「K+」前缀以免出现 K++) */ +export function formatKLabel(m) { + return `K${formatMinutes(m)}` +} + +/** + * 地表水平距离(米),不含高度 + */ +export function horizontalDistanceMeters(lng1, lat1, lng2, lat2) { + const R = 6371000 + const toRad = x => (x * Math.PI) / 180 + const φ1 = toRad(lat1) + const φ2 = toRad(lat2) + const Δφ = toRad(lat2 - lat1) + const Δλ = toRad(lng2 - lng1) + const a = Math.sin(Δφ / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2 + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) + return R * c +} + +/** + * Safety Column 空间冲突:同一时刻 t,水平距离 < 2R 且 |Δ高度| < H + * getSpeedKmhAtMinutes:可选,(routeId, minutesFromK) => number,用于给出可执行的航速建议 + */ +export function detectSafetyColumnSpatial( + routeIds, + minMinutes, + maxMinutes, + getPositionAtMinutesFromK, + config = {}, + routeNamesById = {}, + canPairFn = null, + getSpeedKmhAtMinutes = null +) { + const R = config.safetyColumnRadiusMeters != null ? Number(config.safetyColumnRadiusMeters) : defaultConflictConfig.safetyColumnRadiusMeters + const H = config.safetyColumnHeightMeters != null ? Number(config.safetyColumnHeightMeters) : defaultConflictConfig.safetyColumnHeightMeters + const step = config.safetyColumnSpatialStepMinutes != null ? Number(config.safetyColumnSpatialStepMinutes) : defaultConflictConfig.safetyColumnSpatialStepMinutes + const list = [] + const twoR = 2 * R + const sampleTimes = [] + for (let t = minMinutes; t <= maxMinutes; t += step) sampleTimes.push(t) + for (let i = 0; i < routeIds.length; i++) { + for (let j = i + 1; j < routeIds.length; j++) { + const ridA = routeIds[i] + const ridB = routeIds[j] + if (canPairFn && !canPairFn(ridA, ridB)) continue + for (const t of sampleTimes) { + const posA = getPositionAtMinutesFromK(ridA, t) + const posB = getPositionAtMinutesFromK(ridB, t) + if (!posA || !posB || posA.lng == null || posB.lng == null) continue + const horiz = horizontalDistanceMeters(posA.lng, posA.lat, posB.lng, posB.lat) + const altA = posA.alt != null ? Number(posA.alt) : 0 + const altB = posB.alt != null ? Number(posB.alt) : 0 + const dAlt = Math.abs(altA - altB) + if (horiz < twoR && dAlt < H) { + const needVertSep = Math.max(0, H - dAlt) + const nameA = routeNamesById[ridA] || `航线${ridA}` + const nameB = routeNamesById[ridB] || `航线${ridB}` + const vA = getSpeedKmhAtMinutes ? getSpeedKmhAtMinutes(ridA, t) : null + const vB = getSpeedKmhAtMinutes ? getSpeedKmhAtMinutes(ridB, t) : null + const vNumA = vA != null && Number.isFinite(vA) && vA > 0 ? vA : null + const vNumB = vB != null && Number.isFinite(vB) && vB > 0 ? vB : null + let vRef = 800 + if (vNumA != null && vNumB != null) vRef = (vNumA + vNumB) / 2 + else if (vNumA != null) vRef = vNumA + else if (vNumB != null) vRef = vNumB + const horizDeficitM = Math.max(0, twoR - horiz) + const reductionKmh = Math.min(150, Math.max(35, Math.round((horizDeficitM / 1000) * 10))) + const vSuggest = Math.max(120, Math.round(vRef - reductionKmh)) + const kShiftMin = 1 + const kLater = formatKLabel(t + kShiftMin) + const kEarlier = formatKLabel(t - kShiftMin) + const speedLine = + vNumA != null && vNumB != null + ? `② 按当前推演航段速度(「${nameA}」约 ${Math.round(vNumA)} km/h、「${nameB}」约 ${Math.round(vNumB)} km/h),建议将其中一机在本段调至约 ${vSuggest} km/h(约比两机均值低 ${Math.round(vRef - vSuggest)} km/h),使通过该位置的时刻与另一机至少错开约 1 分钟。` + : `② 按参考航段速度约 ${Math.round(vRef)} km/h 计,建议将其中一机在本段调至约 ${vSuggest} km/h(约降低 ${Math.round(vRef - vSuggest)} km/h),使通过该冲突区域的先后间隔至少约 1 分钟。` + const kTimeLine = + `③ 将其中一条航线在冲突时刻附近航点的相对 K 时整体推迟或提前至少 ${kShiftMin} 分钟:例如由 ${formatKLabel(t)} 调至 ${kLater} 或更晚,或调至 ${kEarlier} 或更早。` + list.push({ + type: CONFLICT_TYPE.SPACE, + subType: 'safety_column_spatial', + title: '飞机间距不足', + routeIds: [ridA, ridB], + routeNames: [nameA, nameB], + time: `约 ${formatKLabel(t)}`, + position: `经度 ${((posA.lng + posB.lng) / 2).toFixed(5)}°, 纬度 ${((posA.lat + posB.lat) / 2).toFixed(5)}°, 高度约 ${((altA + altB) / 2).toFixed(0)} m`, + suggestion: + `两机在相同时刻占位过近:水平间距约 ${(horiz / 1000).toFixed(2)} km,高度差约 ${dAlt.toFixed(0)} m。` + + `① 将其中一条航线在冲突航段附近的高度上调或下调,使两机垂直间隔至少再拉开约 ${Math.ceil(needVertSep)} m(择一执行即可)。` + + speedLine + + kTimeLine, + severity: 'high', + positionLng: (posA.lng + posB.lng) / 2, + positionLat: (posA.lat + posB.lat) / 2, + positionAlt: (altA + altB) / 2, + minutesFromK: t, + safetyColumnHorizM: horiz, + safetyColumnDAltM: dAlt + }) + break + } + } + } + } + return list +} + +/** + * 隐藏圆柱体 — 时间冲突:不同航线先后经过同一物理位置,时间间隔 < ΔT(秒) + * 使用网格量化「同一位置」,采样步长应小于 ΔT 以避免漏检。 + */ +export function detectSafetyColumnTemporal( + routeIds, + minMinutes, + maxMinutes, + getPositionAtMinutesFromK, + config = {}, + routeNamesById = {} +) { + const deltaSec = config.safetyColumnTimeThresholdSeconds != null + ? Number(config.safetyColumnTimeThresholdSeconds) + : defaultConflictConfig.safetyColumnTimeThresholdSeconds + const step = config.safetyColumnTemporalStepMinutes != null + ? Number(config.safetyColumnTemporalStepMinutes) + : defaultConflictConfig.safetyColumnTemporalStepMinutes + const gLng = config.safetyColumnTemporalGridLngDeg != null + ? Number(config.safetyColumnTemporalGridLngDeg) + : defaultConflictConfig.safetyColumnTemporalGridLngDeg + const gLat = config.safetyColumnTemporalGridLatDeg != null + ? Number(config.safetyColumnTemporalGridLatDeg) + : defaultConflictConfig.safetyColumnTemporalGridLatDeg + const gAlt = config.safetyColumnTemporalGridAltMeters != null + ? Number(config.safetyColumnTemporalGridAltMeters) + : defaultConflictConfig.safetyColumnTemporalGridAltMeters + + const cellMap = new Map() + + for (const rid of routeIds) { + for (let t = minMinutes; t <= maxMinutes; t += step) { + const pos = getPositionAtMinutesFromK(rid, t) + if (!pos || pos.lng == null || pos.lat == null) continue + const lng = Number(pos.lng) + const lat = Number(pos.lat) + const altN = Number(pos.alt) || 0 + const ix = gLng > 0 ? Math.floor(lng / gLng) : 0 + const iy = gLat > 0 ? Math.floor(lat / gLat) : 0 + const iz = gAlt > 0 ? Math.floor(altN / gAlt) : 0 + const key = `${ix}_${iy}_${iz}` + let arr = cellMap.get(key) + if (!arr) { + arr = [] + cellMap.set(key, arr) + } + if (arr.length < 200) { + arr.push({ routeId: rid, tMinutes: t, lng: pos.lng, lat: pos.lat, alt: Number(pos.alt) || 0 }) + } + } + } + + const reported = new Set() + const list = [] + + for (const [, samples] of cellMap) { + if (samples.length < 2) continue + samples.sort((a, b) => a.tMinutes - b.tMinutes) + for (let a = 0; a < samples.length; a++) { + for (let b = a + 1; b < samples.length; b++) { + const sa = samples[a] + const sb = samples[b] + if (sa.routeId === sb.routeId) continue + const dtSec = Math.abs(sb.tMinutes - sa.tMinutes) * 60 + if (dtSec <= 1e-6 || dtSec >= deltaSec) continue + const idA = String(sa.routeId) + const idB = String(sb.routeId) + const rLo = idA < idB ? idA : idB + const rHi = idA < idB ? idB : idA + const t1 = Math.min(sa.tMinutes, sb.tMinutes) + const t2 = Math.max(sa.tMinutes, sb.tMinutes) + const pairKey = `${rLo}_${rHi}_${t1.toFixed(5)}_${t2.toFixed(5)}` + if (reported.has(pairKey)) continue + reported.add(pairKey) + + const nameA = routeNamesById[sa.routeId] || `航线${sa.routeId}` + const nameB = routeNamesById[sb.routeId] || `航线${sb.routeId}` + const tEarly = sa.tMinutes <= sb.tMinutes ? sa : sb + const tLate = sa.tMinutes <= sb.tMinutes ? sb : sa + const needDelaySec = Math.ceil(deltaSec - dtSec) + const midLng = (tEarly.lng + tLate.lng) / 2 + const midLat = (tEarly.lat + tLate.lat) / 2 + const midAlt = (tEarly.alt + tLate.alt) / 2 + + list.push({ + type: CONFLICT_TYPE.TIME, + subType: 'safety_column_temporal', + title: '先后经过同点时间过近', + routeIds: [sa.routeId, sb.routeId], + routeNames: [nameA, nameB], + time: `先后约 ${Math.round(dtSec)} s(约 ${formatKLabel(tEarly.tMinutes)} 与 ${formatKLabel(tLate.tMinutes)})`, + position: `经度 ${midLng.toFixed(5)}°, 纬度 ${midLat.toFixed(5)}°, 高度约 ${midAlt.toFixed(0)} m`, + suggestion: + `两机先后经过同一空域位置,时间间隔仅约 ${Math.round(dtSec)} s,低于安全先后间隔要求。` + + `① 将其中一条航线相关航点的相对 K 时整体前移或后移,使先后到达间隔至少再拉开约 ${needDelaySec} s 以上。` + + `② 微调其中一机在邻近航段的飞行速度,使到达该位置的时刻错开。` + + `③ 若仍受航路结构限制,可为其中一机分配不同高度层,通过垂直间隔避免同点先后过近。`, + severity: 'high', + positionLng: midLng, + positionLat: midLat, + positionAlt: midAlt, + minutesFromK: tEarly.tMinutes, + safetyColumnTemporalDtSec: dtSec + }) + } + } + } + return list +} + /** * 空间冲突:推演航迹最小间隔 * getPositionAtMinutesFromK: (routeId, minutesFromK) => { position: { lng, lat, alt } } | null @@ -130,7 +354,7 @@ export function detectTrackSeparation(routeIds, minMinutes, maxMinutes, getPosit subType: 'track_separation', title: '航迹间隔过小', routeIds: [ridA, ridB], - time: `约 K+${formatMinutes(t)}`, + time: `约 ${formatKLabel(t)}`, position: `经度 ${(posA.lng + posB.lng) / 2}°, 纬度 ${(posA.lat + posB.lat) / 2}°`, suggestion: `两机在该时刻距离约 ${(d / 1000).toFixed(1)} km,小于最小间隔 ${minSep / 1000} km。建议调整航线或时间窗,或增大最小间隔配置。`, severity: 'high', @@ -239,7 +463,7 @@ export function detectRestrictedZoneIntrusion(routeIds, minMinutes, maxMinutes, title: '禁限区入侵', routeIds: [rid], zoneName: zone.name || '禁限区', - time: `约 K+${formatMinutes(t)}`, + time: `约 ${formatKLabel(t)}`, position: `经度 ${pos.lng.toFixed(5)}°, 纬度 ${pos.lat.toFixed(5)}°, 高度 ${alt} m`, suggestion: `航迹在 ${zone.name || '禁限区'} 内且高度在 [${altMin}, ${altMax}] m 范围内。建议调整航线避开该区域或调整禁限区高度带。`, severity: 'high', @@ -313,96 +537,6 @@ export function parseRestrictedZonesFromDrawings(entities, keywords = defaultCon return zones } -// --------------- 频谱冲突与台账 --------------- - -/** - * 频谱资源台账项:同频/邻频 + 时间 + 带宽 + 地理范围 - */ -export function createSpectrumLedgerEntry({ id, name, freqMHz, bandwidthMHz, startTimeMinutes, endTimeMinutes, geo }) { - return { - id: id || `spec_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`, - name: name || '', - freqMHz: Number(freqMHz) || 0, - bandwidthMHz: Number(bandwidthMHz) || 0, - startTimeMinutes: Number(startTimeMinutes) || 0, - endTimeMinutes: Number(endTimeMinutes) || 0, - geo: geo || null // { type: 'bbox', minLng, minLat, maxLng, maxLat } 或 { type: 'polygon', points: [{lng,lat}] } - } -} - -/** - * 两频段是否同频或邻频(中心频率差在带宽一半或 guard 内) - */ -export function isSameOrAdjacentFreq(freq1, bw1, freq2, bw2, guardMHz = defaultConflictConfig.adjacentFreqGuardMHz) { - const c1 = freq1 - const c2 = freq2 - const half = (bw1 + bw2) / 2 - const gap = Math.abs(c1 - c2) - if (gap <= 1e-6) return true // 同频 - if (gap <= half || gap <= guardMHz) return true // 重叠或邻频 - return false -} - -/** - * 两地理范围是否重叠(bbox 或 polygon 简化:bbox 与 bbox) - */ -export function geoOverlap(geo1, geo2) { - if (!geo1 || !geo2) return true // 未设范围视为全局重叠 - const toBbox = (g) => { - if (g.type === 'bbox') return g - if (g.type === 'polygon' && g.points && g.points.length) { - const lngs = g.points.map(p => p.lng != null ? p.lng : p.x) - const lats = g.points.map(p => p.lat != null ? p.lat : p.y) - return { minLng: Math.min(...lngs), maxLng: Math.max(...lngs), minLat: Math.min(...lats), maxLat: Math.max(...lats) } - } - return null - } - const b1 = toBbox(geo1) - const b2 = toBbox(geo2) - if (!b1 || !b2) return true - const minLng1 = b1.minLng ?? b1.west - const maxLng1 = b1.maxLng ?? b1.east - const minLat1 = b1.minLat ?? b1.south - const maxLat1 = b1.maxLat ?? b1.north - const minLng2 = b2.minLng ?? b2.west - const maxLng2 = b2.maxLng ?? b2.east - const minLat2 = b2.minLat ?? b2.south - const maxLat2 = b2.maxLat ?? b2.north - return !(maxLng1 < minLng2 || maxLng2 < minLng1 || maxLat1 < minLat2 || maxLat2 < minLat1) -} - -/** - * 频谱冲突检测:台账内两两判定 同频/邻频 + 时间重叠 + 地理重叠 - * spectrumLedger: createSpectrumLedgerEntry 数组 - */ -export function detectSpectrumConflicts(spectrumLedger, config = {}) { - const guard = config.adjacentFreqGuardMHz != null ? config.adjacentFreqGuardMHz : defaultConflictConfig.adjacentFreqGuardMHz - const checkGeo = config.spectrumConflictGeoOverlap !== false - const list = [] - for (let i = 0; i < spectrumLedger.length; i++) { - for (let j = i + 1; j < spectrumLedger.length; j++) { - const a = spectrumLedger[i] - const b = spectrumLedger[j] - if (!isSameOrAdjacentFreq(a.freqMHz, a.bandwidthMHz, b.freqMHz, b.bandwidthMHz, guard)) continue - if (!timeRangesOverlap(a.startTimeMinutes, a.endTimeMinutes, b.startTimeMinutes, b.endTimeMinutes, 0)) continue - if (checkGeo && !geoOverlap(a.geo, b.geo)) continue - list.push({ - type: CONFLICT_TYPE.SPECTRUM, - subType: 'spectrum_overlap', - title: '频谱冲突', - routeIds: [], - routeNames: [a.name || `频谱${a.id}`, b.name || `频谱${b.id}`], - time: `时间重叠:K+${formatMinutes(a.startTimeMinutes)}~K+${formatMinutes(a.endTimeMinutes)} 与 K+${formatMinutes(b.startTimeMinutes)}~K+${formatMinutes(b.endTimeMinutes)}`, - position: checkGeo && a.geo ? `地理范围重叠` : '时间与频段重叠', - suggestion: `同频/邻频且时间与地理范围重叠。建议错开使用时间、调整频点或带宽、或缩小地理范围。`, - severity: 'high', - spectrumIds: [a.id, b.id] - }) - } - } - return list -} - /** * 合并并规范化冲突列表,统一 id、severity、category 等供 UI 展示 */ diff --git a/ruoyi-ui/src/utils/timelinePosition.js b/ruoyi-ui/src/utils/timelinePosition.js index 9bd934e..373df54 100644 --- a/ruoyi-ui/src/utils/timelinePosition.js +++ b/ruoyi-ui/src/utils/timelinePosition.js @@ -77,28 +77,35 @@ return s.startPos } - if (s.type === 'hold' && s.holdPath && s.holdPath.length) { - if (s.holdClosedLoopPath && s.holdClosedLoopPath.length >= 2 && s.holdLoopLength > 0 && s.speedKmh != null) { - const distM = (minutesFromK - s.startTime) * (s.speedKmh * 1000 / 60) - const n = Math.max(0, s.holdN != null ? s.holdN : 0) - const totalFly = (s.holdExitDistanceOnLoop || 0) + n * s.holdLoopLength - if (totalFly <= 0) return s.startPos - if (distM >= totalFly) return s.endPos - if (distM < n * s.holdLoopLength) { - const distOnLoop = ((distM % s.holdLoopLength) + s.holdLoopLength) % s.holdLoopLength - const tPath = distOnLoop / s.holdLoopLength - return getPositionAlongPathSlice(s.holdClosedLoopPath, tPath) - } - const distToExit = distM - n * s.holdLoopLength - const exitDist = s.holdExitDistanceOnLoop || 1 - const tPath = Math.min(1, distToExit / exitDist) - if (s.holdEntryToExitPath && s.holdEntryToExitPath.length > 0) { - return getPositionAlongPathSlice(s.holdEntryToExitPath, tPath) - } - return getPositionAlongPathSlice(s.holdClosedLoopPath, distToExit / s.holdLoopLength) - } - return getPositionAlongPathSlice(s.holdPath, t) - } + if (s.type === 'hold' && s.holdPath && s.holdPath.length) { + if (s.holdClosedLoopPath && s.holdClosedLoopPath.length >= 2 && s.holdLoopLength > 0 && s.speedKmh != null) { + const distM = (minutesFromK - s.startTime) * (s.speedKmh * 1000 / 60) + const n = Math.max(0, s.holdN != null ? s.holdN : 0) + const totalFly = (s.holdExitDistanceOnLoop || 0) + n * s.holdLoopLength + if (totalFly <= 0) return s.startPos + if (distM >= totalFly) return s.endPos + if (distM < n * s.holdLoopLength) { + const distOnLoop = ((distM % s.holdLoopLength) + s.holdLoopLength) % s.holdLoopLength + const tPath = distOnLoop / s.holdLoopLength + return getPositionAlongPathSlice(s.holdClosedLoopPath, tPath) + } + const distToExit = distM - n * s.holdLoopLength + const exitDist = s.holdExitDistanceOnLoop || 1 + const tPath = Math.min(1, distToExit / exitDist) + if (s.holdEntryToExitPath && s.holdEntryToExitPath.length > 0) { + return getPositionAlongPathSlice(s.holdEntryToExitPath, tPath) + } + return getPositionAlongPathSlice(s.holdClosedLoopPath, distToExit / s.holdLoopLength) + } + 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 getPositionAlongPathSlice(s.holdPath, tPath) + } + return getPositionAlongPathSlice(s.holdPath, t) + } if (s.type === 'fly' && s.pathSlice && s.pathSlice.length) { return getPositionAlongPathSlice(s.pathSlice, t) diff --git a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue index 4f13a65..cd7149e 100644 --- a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue +++ b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue @@ -21,6 +21,10 @@ 📝 编辑航点 +