diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/RouteWaypointsMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/RouteWaypointsMapper.java index a269b65..dcaf40f 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/RouteWaypointsMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/RouteWaypointsMapper.java @@ -29,7 +29,7 @@ public interface RouteWaypointsMapper public List selectRouteWaypointsList(RouteWaypoints routeWaypoints); /** 查询指定航线下最大的序号 */ - public Integer selectMaxSeqByRouteId(Long routeId); + public Integer selectMaxSeqByRouteId(@Param("routeId") Long routeId); /** 将指定航线中 seq >= targetSeq 的航点序号均加 1,用于在指定位置插入新航点 */ int incrementSeqFrom(@Param("routeId") Long routeId, @Param("seq") Long targetSeq); @@ -56,7 +56,7 @@ public interface RouteWaypointsMapper * @param id 航线具体航点明细主键 * @return 结果 */ - public int deleteRouteWaypointsById(Long id); + public int deleteRouteWaypointsById(@Param("id") Long id); /** * 删除航线具体航点明细 @@ -64,7 +64,7 @@ public interface RouteWaypointsMapper * @param routeId 航线主键 * @return 结果 */ - public int deleteRouteWaypointsByRouteId(Long routeId); + public int deleteRouteWaypointsByRouteId(@Param("routeId") Long routeId); /** * 批量删除航线具体航点明细 @@ -72,5 +72,5 @@ public interface RouteWaypointsMapper * @param ids 需要删除的数据主键集合 * @return 结果 */ - public int deleteRouteWaypointsByIds(Long[] ids); + public int deleteRouteWaypointsByIds(@Param("ids") Long[] ids); } diff --git a/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml b/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml index 908e959..a9269ab 100644 --- a/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml @@ -114,9 +114,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" delete from route_waypoints where route_id = #{routeId} - + delete from route_waypoints where id in - + #{id} diff --git a/ruoyi-ui/src/lang/zh.js b/ruoyi-ui/src/lang/zh.js index ecc88ad..e7d0bbe 100644 --- a/ruoyi-ui/src/lang/zh.js +++ b/ruoyi-ui/src/lang/zh.js @@ -186,7 +186,7 @@ export default { color: '填充颜色', borderWidth: '边线宽度', vertices: '顶点坐标', - polygonPlaceholder: '至少 3 个顶点,十进制度。可每行一对「经度,纬度」;或一行写 (121.47,31.23)、(120.15,30.28) 用顿号分隔', + polygonPlaceholder: '至少3个顶点,可每行一对(经度,纬度)或用顿号分隔', rectangleSwCorner: '西南角经纬度', rectangleNeCorner: '东北角经纬度', cornerLonLatPlaceholder: '(经度,纬度)例如 (116.39, 39.90)', @@ -198,7 +198,7 @@ export default { cancel: '取消', confirm: '生成', defaultLabel: '空域', - errPolygonPoints: '多边形至少需要 3 个有效顶点(经度,纬度)', + errPolygonPoints: '多边形至少需要3个有效顶点(经度,纬度)', errRectNumbers: '请按(经度,纬度)格式填写有效的西南角与东北角', errCircle: '请按(经度,纬度)填写有效的圆心与半径(千米)', errSector: '请按(经度,纬度)填写有效的圆心、半径(千米)', diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index 25d43e5..6bc715f 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -2542,8 +2542,9 @@ export default { } }, //正式航线渲染函数 - renderRouteWaypoints(waypoints, routeId = 'default', platformId, platform, style) { - if (!waypoints || waypoints.length < 1) return; + renderRouteWaypoints(waypointsRaw, routeId = 'default', platformId, platform, style) { + if (!waypointsRaw || waypointsRaw.length < 1) return; + const waypoints = this.sortWaypointsBySeq(waypointsRaw); this.waypointDragPreview = null; this.unregisterWaypointMapDomLabelsForRoute(routeId); // 清理旧线(主航线 + 透明点击层;含历史柔边层 id 以便兼容旧数据) @@ -3232,6 +3233,21 @@ export default { const t = (wp && wp.pointType) || (wp && wp.point_type) || 'normal'; return t === 'hold_circle' || t === 'hold_ellipse'; }, + /** 航线几何与平台起点均以 seq 最小航点为「出发航点」;与数据库 seq 一致,不依赖接口返回数组下标顺序 */ + sortWaypointsBySeq(waypoints) { + if (!waypoints || !waypoints.length) return []; + return waypoints.slice().sort((a, b) => { + const saRaw = a.seq != null ? a.seq : a.Seq; + const sbRaw = b.seq != null ? b.seq : b.Seq; + const saNum = Number(saRaw); + const sbNum = Number(sbRaw); + // seq 不可解析时不要“抢到最前”,否则平台起点会错 + const sa = Number.isFinite(saNum) ? saNum : Number.POSITIVE_INFINITY; + const sb = Number.isFinite(sbNum) ? sbNum : Number.POSITIVE_INFINITY; + if (sa !== sb) return sa - sb; + return (Number(a.id) || 0) - (Number(b.id) || 0); + }); + }, parseHoldParams(wp) { const raw = (wp && wp.holdParams) || (wp && wp.hold_params); if (!raw) return null; @@ -4551,8 +4567,9 @@ export default { * @param options - 可选 { holdRadiusByLegIndex: { [legIndex]: number } } 为指定盘旋段覆盖半径(使落点精准在切点) * @returns {{ path, segmentEndIndices, holdArcRanges: { [legIndex]: { start, end } } }} */ - getRoutePathWithSegmentIndices(waypoints, options) { - if (!waypoints || waypoints.length === 0) return { path: [], segmentEndIndices: [], holdArcRanges: {} }; + getRoutePathWithSegmentIndices(waypointsRaw, options) { + if (!waypointsRaw || waypointsRaw.length === 0) return { path: [], segmentEndIndices: [], holdArcRanges: {} }; + const waypoints = this.sortWaypointsBySeq(waypointsRaw); const holdRadiusByLegIndex = (options && options.holdRadiusByLegIndex) || {}; const holdEllipseParamsByLegIndex = (options && options.holdEllipseParamsByLegIndex) || {}; const ellipsoid = this.viewer.scene.globe.ellipsoid; diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index b83cf64..c06c1a7 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -1374,7 +1374,16 @@ export default { return; } const others = list.filter(w => w.id !== newWp.id); - others.sort((a, b) => (Number(a.seq) || 0) - (Number(b.seq) || 0)); + others.sort((a, b) => { + const saRaw = a.seq != null ? a.seq : a.Seq; + const sbRaw = b.seq != null ? b.seq : b.Seq; + const saNum = Number(saRaw); + const sbNum = Number(sbRaw); + const sa = Number.isFinite(saNum) ? saNum : Number.POSITIVE_INFINITY; + const sb = Number.isFinite(sbNum) ? sbNum : Number.POSITIVE_INFINITY; + if (sa !== sb) return sa - sb; + return (Number(a.id) || 0) - (Number(b.id) || 0); + }); const reordered = [...others.slice(0, insertIndex), newWp, ...others.slice(insertIndex)]; const routeInListFirst = this.routes.find(r => r.id === routeId); if (routeInListFirst) routeInListFirst.waypoints = reordered; @@ -1424,7 +1433,16 @@ export default { this.$message.warning('刷新后未拿到航线航点'); return; } - const sortedWaypoints = updated.waypoints.slice().sort((a, b) => (Number(a.seq) || 0) - (Number(b.seq) || 0)); + const sortedWaypoints = updated.waypoints.slice().sort((a, b) => { + const saRaw = a.seq != null ? a.seq : a.Seq; + const sbRaw = b.seq != null ? b.seq : b.Seq; + const saNum = Number(saRaw); + const sbNum = Number(sbRaw); + const sa = Number.isFinite(saNum) ? saNum : Number.POSITIVE_INFINITY; + const sb = Number.isFinite(sbNum) ? sbNum : Number.POSITIVE_INFINITY; + if (sa !== sb) return sa - sb; + return (Number(a.id) || 0) - (Number(b.id) || 0); + }); updated.waypoints = sortedWaypoints; const routeInList = this.routes.find(r => r.id === routeId); if (routeInList) routeInList.waypoints = sortedWaypoints; @@ -1506,6 +1524,7 @@ export default { turnAngle = preserveTurnAngle(); } try { + // 不要在 startTime 为空时强行写入默认值(例如 K+00:00:00),否则会改变推演时间轴 min(K) ,导致平台从错误航点开始。 const payload = { id: wp.id, routeId, @@ -1515,9 +1534,9 @@ export default { lng: wp.lng, alt: wp.alt, speed: wp.speed, - startTime: wp.startTime != null && wp.startTime !== '' ? wp.startTime : 'K+00:00:00', turnAngle, - pointType + pointType, + ...(wp.startTime != null && wp.startTime !== '' ? { startTime: wp.startTime } : {}) }; if (holdParams != null) payload.holdParams = holdParams; else payload.holdParams = ''; @@ -1558,7 +1577,8 @@ export default { } this.$refs.cesiumMap.removeRouteById(routeId); this.$refs.cesiumMap.renderRouteWaypoints(r.waypoints, routeId, r.platformId, r.platform, this.parseRouteStyle(r.attributes)); - this.$nextTick(() => this.updateDeductionPositions()); + // 切换盘旋会影响推演所用的数据(尤其 startTime/hold 几何),需同步最新 min/max 下的 deductionMinutesFromK。 + this.$nextTick(() => this.updateTimeFromProgress()); if (roomId && r.waypoints && r.waypoints.length > 0) { this.updateMissilePositionsAfterRouteEdit(roomId, routeId, r.platformId != null ? r.platformId : 0, r.waypoints); } @@ -1628,9 +1648,9 @@ export default { 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') + pointType: (wp.pointType || wp.point_type || 'hold_circle'), + ...(wp.startTime != null && wp.startTime !== '' ? { startTime: wp.startTime } : {}) }; payload.holdParams = JSON.stringify(nextHoldParamsObj); if (wp.segmentMode != null) payload.segmentMode = wp.segmentMode; @@ -1668,7 +1688,7 @@ export default { } 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.$nextTick(() => this.updateTimeFromProgress()); } } this.$message.success(`盘旋速度已更新为 ${Math.round(targetSpeed * 10) / 10} km/h`); @@ -1829,6 +1849,21 @@ export default { this.$message.error('未找到对应航点'); return; } + // 兼容:分段模式/目标时间/目标速度可能在顶层,也可能在 displayStyle 内 + // 若拖拽后取不到,就会导致定速/定时重算逻辑不触发,平台与时间轴不同步。 + const getSegMode = (w) => (w?.segmentMode ?? w?.displayStyle?.segmentMode ?? null); + const getSegTargetMinutes = (w) => (w?.segmentTargetMinutes ?? w?.displayStyle?.segmentTargetMinutes ?? null); + const getSegTargetSpeed = (w) => (w?.segmentTargetSpeed ?? w?.displayStyle?.segmentTargetSpeed ?? null); + const wpSegMode = getSegMode(wp); + const wpSegTargetMinutes = getSegTargetMinutes(wp); + const wpSegTargetSpeed = getSegTargetSpeed(wp); + const segTargetMinutesNum = wpSegTargetMinutes != null ? Number(wpSegTargetMinutes) : null; + const fixedTimeStartTime = (wpSegMode === 'fixed_time' + && wpSegTargetMinutes != null + && wpSegTargetMinutes !== '' + && Number.isFinite(segTargetMinutesNum)) + ? this.minutesToStartTimeWithSeconds(segTargetMinutesNum) + : null; const payload = { id: wp.id, routeId: wp.routeId != null ? wp.routeId : routeId, @@ -1839,14 +1874,19 @@ export default { // 拖拽只允许修改经纬度,高度保持原值(避免 5000 -> 4999.999... 的数值抖动) alt: wp.alt, speed: wp.speed, - startTime: (wp.startTime != null && wp.startTime !== '') ? wp.startTime : 'K+00:00:00', + // fixed_time:始终用 segmentTargetMinutes 生成 startTime,避免 startTime 与目标时间不一致导致推演段错位(如 wait 段提前/滞后) + startTime: fixedTimeStartTime != null + ? fixedTimeStartTime + : (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 (wpSegMode != null) payload.segmentMode = wpSegMode; + if (wpSegTargetMinutes != null && wpSegTargetMinutes !== '') payload.segmentTargetMinutes = wpSegTargetMinutes; + if (wpSegTargetSpeed != null && wpSegTargetSpeed !== '') payload.segmentTargetSpeed = wpSegTargetSpeed; if (wp.color != null) payload.color = wp.color; if (wp.pixelSize != null) payload.pixelSize = wp.pixelSize; if (wp.outlineColor != null) payload.outlineColor = wp.outlineColor; @@ -1874,12 +1914,16 @@ export default { { lat: merged.lat, lng: merged.lng, alt: merged.alt } ); const prevMinutes = this.waypointStartTimeToMinutesDecimal(prev.startTime); - if (prev.segmentMode === 'fixed_speed') { - const speedKmh = Number(prev.segmentTargetSpeed ?? prev.speed) || 800; + const prevSegMode = getSegMode(prev); + const prevSegTargetSpeed = getSegTargetSpeed(prev); + const mergedSegMode = getSegMode(merged); + const mergedSegTargetMinutes = getSegTargetMinutes(merged); + if (prevSegMode === 'fixed_speed') { + const speedKmh = Number(prevSegTargetSpeed ?? prev.speed) || 800; const newMinutesFromK = prevMinutes + (distM / 1000) / speedKmh * 60; const newStartTime = this.minutesToStartTimeWithSeconds(newMinutesFromK); const startPayload = { ...merged, startTime: newStartTime }; - if (merged.segmentMode != null) startPayload.segmentMode = merged.segmentMode; + if (mergedSegMode != null) startPayload.segmentMode = mergedSegMode; try { const r2 = await updateWaypoints(startPayload, roomIdParam); if (r2.code === 200) { @@ -1893,14 +1937,16 @@ export default { } catch (e) { console.warn('定速重算相对K时失败', e); } - } else if (merged.segmentMode === 'fixed_time') { - const currMinutes = (merged.segmentTargetMinutes != null && merged.segmentTargetMinutes !== '') ? Number(merged.segmentTargetMinutes) : this.waypointStartTimeToMinutesDecimal(merged.startTime); + } else if (mergedSegMode === 'fixed_time') { + const currMinutes = (mergedSegTargetMinutes != null && mergedSegTargetMinutes !== '') + ? Number(mergedSegTargetMinutes) + : this.waypointStartTimeToMinutesDecimal(merged.startTime); const deltaMin = currMinutes - prevMinutes; if (deltaMin > 0.001) { const newSpeedKmh = (distM / 1000) / (deltaMin / 60); const speedVal = Math.round(newSpeedKmh * 10) / 10; const speedPayload = { ...prev, speed: speedVal }; - if (prev.segmentMode != null) speedPayload.segmentMode = prev.segmentMode; + if (prevSegMode != null) speedPayload.segmentMode = prevSegMode; try { const r2 = await updateWaypoints(speedPayload, roomIdParam); if (r2.code === 200) { @@ -1921,19 +1967,26 @@ export default { // 下一航点是定时点时:重算本航点(被拖动的)速度,使平台能在下一航点的 K 时到达 if (idx >= 0 && idx < waypoints.length - 1) { const next = waypoints[idx + 1]; - if (next.segmentMode === 'fixed_time') { + const nextSegMode = getSegMode(next); + const nextSegTargetMinutes = getSegTargetMinutes(next); + if (nextSegMode === 'fixed_time') { const distToNextM = this.segmentDistance( { lat: merged.lat, lng: merged.lng, alt: merged.alt }, { lat: next.lat, lng: next.lng, alt: next.alt } ); - const currMinutes = this.waypointStartTimeToMinutesDecimal(merged.startTime); - const nextMinutes = (next.segmentTargetMinutes != null && next.segmentTargetMinutes !== '') ? Number(next.segmentTargetMinutes) : this.waypointStartTimeToMinutesDecimal(next.startTime); + const mergedSegTargetMinutes = getSegTargetMinutes(merged); + const currMinutes = (mergedSegTargetMinutes != null && mergedSegTargetMinutes !== '') + ? Number(mergedSegTargetMinutes) + : this.waypointStartTimeToMinutesDecimal(merged.startTime); + const nextMinutes = (nextSegTargetMinutes != null && nextSegTargetMinutes !== '') + ? Number(nextSegTargetMinutes) + : this.waypointStartTimeToMinutesDecimal(next.startTime); const deltaMin = nextMinutes - currMinutes; if (deltaMin > 0.001) { const newSpeedKmh = (distToNextM / 1000) / (deltaMin / 60); const speedVal = Math.round(newSpeedKmh * 10) / 10; const currPayload = { ...merged, speed: speedVal }; - if (merged.segmentMode != null) currPayload.segmentMode = merged.segmentMode; + if (wpSegMode != null) currPayload.segmentMode = wpSegMode; if (merged.labelFontSize != null) currPayload.labelFontSize = merged.labelFontSize; if (merged.labelColor != null) currPayload.labelColor = merged.labelColor; if (merged.color != null) currPayload.color = merged.color; @@ -1963,6 +2016,11 @@ export default { if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data); } catch (_) {} } + // 与列表数据一致:避免仅选中详情为深拷贝时 routes 仍为旧引用,推演用 routes 与地图渲染脱节 + const routeInList = this.routes.find(r => r.id === routeId); + if (routeInList && routeInList.waypoints !== waypoints) { + this.$set(routeInList, 'waypoints', waypoints); + } this.$refs.cesiumMap.renderRouteWaypoints( waypoints, routeId, @@ -2746,7 +2804,16 @@ export default { platform: item.platform, attributes: item.attributes, points: item.waypoints ? item.waypoints.length : 0, - waypoints: item.waypoints || [], + waypoints: (item.waypoints || []).slice().sort((a, b) => { + const saRaw = a.seq != null ? a.seq : a.Seq; + const sbRaw = b.seq != null ? b.seq : b.Seq; + const saNum = Number(saRaw); + const sbNum = Number(sbRaw); + const sa = Number.isFinite(saNum) ? saNum : Number.POSITIVE_INFINITY; + const sb = Number.isFinite(sbNum) ? sbNum : Number.POSITIVE_INFINITY; + if (sa !== sb) return sa - sb; + return (Number(a.id) || 0) - (Number(b.id) || 0); + }), conflict: false, scenarioId: item.scenarioId })); @@ -5074,7 +5141,16 @@ export default { 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 oldWpSorted = (route.waypoints || []).slice().sort((a, b) => { + const saRaw = a.seq != null ? a.seq : a.Seq; + const sbRaw = b.seq != null ? b.seq : b.Seq; + const saNum = Number(saRaw); + const sbNum = Number(sbRaw); + const sa = Number.isFinite(saNum) ? saNum : Number.POSITIVE_INFINITY; + const sb = Number.isFinite(sbNum) ? sbNum : Number.POSITIVE_INFINITY; + if (sa !== sb) return sa - sb; + return (Number(a.id) || 0) - (Number(b.id) || 0); + }); const oldToNewById = {}; let prevNewMinutes = null; oldWpSorted.forEach(wp => { @@ -5433,7 +5509,8 @@ export default { /** 将航点 startTime 字符串转为相对 K 的分钟数 */ waypointStartTimeToMinutes(s) { if (!s || typeof s !== 'string') return 0; - const m = s.match(/K([+-])(\d{2}):(\d{2})/); + // 兼容小时为 1~2 位(例如 K+0:18:00 或 K+00:18:00) + const m = s.match(/K([+-])(\d{1,2}):(\d{2})(?::(\d{2}))?/); if (!m) return 0; const sign = m[1] === '+' ? 1 : -1; const h = parseInt(m[2], 10); @@ -5443,7 +5520,8 @@ export default { /** 将 startTime(如 K+00:19:30)转为相对 K 的分钟数(含秒,保留小数) */ waypointStartTimeToMinutesDecimal(s) { if (!s || typeof s !== 'string') return 0; - const m = s.match(/K([+-])(\d{2}):(\d{2})(?::(\d{2}))?/); + // 兼容小时为 1~2 位(例如 K+0:18:00 或 K+00:18:00) + const m = s.match(/K([+-])(\d{1,2}):(\d{2})(?::(\d{2}))?/); if (!m) return 0; const sign = m[1] === '+' ? 1 : -1; const h = parseInt(m[2], 10); @@ -5549,6 +5627,18 @@ export default { buildRouteTimeline(waypoints, globalMin, globalMax, pathData) { const warnings = []; if (!waypoints || waypoints.length === 0) return { segments: [], warnings }; + // 含盘旋时不按分钟排序 points,故航点顺序必须与航线几何/数据库 seq 一致。 + // 注意:seq 缺失/不可解析时不要当成 0 抢到最前,否则平台起点会错。 + waypoints = waypoints.slice().sort((a, b) => { + const saRaw = a.seq != null ? a.seq : a.Seq; + const sbRaw = b.seq != null ? b.seq : b.Seq; + const saNum = Number(saRaw); + const sbNum = Number(sbRaw); + const sa = Number.isFinite(saNum) ? saNum : Number.POSITIVE_INFINITY; + const sb = Number.isFinite(sbNum) ? sbNum : Number.POSITIVE_INFINITY; + if (sa !== sb) return sa - sb; + return (Number(a.id) || 0) - (Number(b.id) || 0); + }); const points = waypoints.map((wp, idx) => ({ lng: parseFloat(wp.lng), lat: parseFloat(wp.lat), @@ -5559,6 +5649,11 @@ export default { })); const hasHold = points.some(p => p.isHold); const allSame = points.every(p => p.minutes === points[0].minutes); + // 若所有航点 minutes 完全相同,通常表示 startTime 还没正确初始化/解析。 + // 这种情况下无论是否存在盘旋点,都应先把时间展开成递增,否则 segments 的 start/endTime 会塌缩, + // getPositionFromTimeline 可能会落到错误的段,从而导致平台位置“跳错/错乱”。 + // 但:盘旋点切换不应影响 K+0 的起始段几何/位置,因此只有在“无盘旋”时才展开 minutes。 + // 否则切到有盘旋后会触发展开并重建 segments,导致当前位置跳到后续航段/等待段。 if (allSame && points.length > 1 && !hasHold) { const span = Math.max(globalMax - globalMin, 1); points.forEach((p, i) => { @@ -5889,8 +5984,15 @@ export default { /** 从时间轴中取当前推演时间对应的位置;支持 fly/wait/hold,hold 沿 holdPath 弧长插值 */ getPositionFromTimeline(segments, minutesFromK, path, segmentEndIndices) { if (!segments || segments.length === 0) return null; - if (minutesFromK <= segments[0].startTime) return segments[0].startPos; - const last = segments[segments.length - 1]; + // buildRouteTimeline 在盘旋分支中会先 push wait 段再 push 后续 fly/hold 段, + // 因此这里必须按时间排序并且只在 [startTime, endTime) 内命中,避免提前“命中”wait段导致图标停在错误航点。 + const sortedSegments = segments.slice().sort((a, b) => { + const ds = Number(a.startTime) - Number(b.startTime); + if (ds !== 0) return ds; + return Number(a.endTime) - Number(b.endTime); + }); + if (minutesFromK <= sortedSegments[0].startTime) return sortedSegments[0].startPos; + const last = sortedSegments[sortedSegments.length - 1]; if (minutesFromK >= last.endTime) { if (last.type === 'wait' && path && segmentEndIndices && last.legIndex != null && last.legIndex < segmentEndIndices.length && path[segmentEndIndices[last.legIndex]]) { return path[segmentEndIndices[last.legIndex]]; @@ -5898,10 +6000,13 @@ export default { if (last.type === 'hold' && last.holdPath && last.holdPath.length) return last.holdPath[last.holdPath.length - 1]; return last.endPos; } - for (let i = 0; i < segments.length; i++) { - const s = segments[i]; - if (minutesFromK < s.endTime) { - const t = Math.max(0, Math.min(1, (minutesFromK - s.startTime) / (s.endTime - s.startTime))); + for (let i = 0; i < sortedSegments.length; i++) { + const s = sortedSegments[i]; + if (minutesFromK >= s.startTime && minutesFromK < s.endTime) { + const duration = s.endTime - s.startTime; + const t = duration !== 0 + ? Math.max(0, Math.min(1, (minutesFromK - s.startTime) / duration)) + : 0; if (s.type === 'wait') { if (path && segmentEndIndices && s.legIndex != null && s.legIndex < segmentEndIndices.length) { const endIdx = segmentEndIndices[s.legIndex]; @@ -5966,6 +6071,17 @@ export default { buildPathDataForRouteTimeline(waypoints, routeId) { const cesiumMap = this.$refs.cesiumMap; if (!waypoints || waypoints.length === 0 || !cesiumMap || !cesiumMap.getRoutePathWithSegmentIndices) return null; + // 与 buildRouteTimeline 一致:必须按 seq 排序,否则含盘旋时不会按 K 时重排,推演首段可能与时间轴 min(K) 不一致(飞机卡在错误航点) + waypoints = waypoints.slice().sort((a, b) => { + const saRaw = a.seq != null ? a.seq : a.Seq; + const sbRaw = b.seq != null ? b.seq : b.Seq; + const saNum = Number(saRaw); + const sbNum = Number(sbRaw); + const sa = Number.isFinite(saNum) ? saNum : Number.POSITIVE_INFINITY; + const sb = Number.isFinite(sbNum) ? sbNum : Number.POSITIVE_INFINITY; + if (sa !== sb) return sa - sb; + return (Number(a.id) || 0) - (Number(b.id) || 0); + }); const cachedEllipse = (routeId != null && cesiumMap._routeHoldEllipseParamsByRoute && cesiumMap._routeHoldEllipseParamsByRoute[routeId]) ? cesiumMap._routeHoldEllipseParamsByRoute[routeId] : {}; @@ -5980,6 +6096,16 @@ export default { /** 根据当前推演时间(相对 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 }; + waypoints = waypoints.slice().sort((a, b) => { + const saRaw = a.seq != null ? a.seq : a.Seq; + const sbRaw = b.seq != null ? b.seq : b.Seq; + const saNum = Number(saRaw); + const sbNum = Number(sbRaw); + const sa = Number.isFinite(saNum) ? saNum : Number.POSITIVE_INFINITY; + const sb = Number.isFinite(sbNum) ? sbNum : Number.POSITIVE_INFINITY; + if (sa !== sb) return sa - sb; + return (Number(a.id) || 0) - (Number(b.id) || 0); + }); const cesiumMap = this.$refs.cesiumMap; let pathData = this.buildPathDataForRouteTimeline(waypoints, routeId); let { segments, warnings, earlyArrivalLegs } = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData); @@ -6117,7 +6243,19 @@ export default { 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; + // 方向点优先取 nextPosition,但避免在边界/等待段 nextPosition 与 position 重合导致 heading=0 + const isSamePos = (p1, p2) => { + if (!p1 || !p2) return false; + const dLng = Number(p1.lng) - Number(p2.lng); + const dLat = Number(p1.lat) - Number(p2.lat); + const dAlt = (Number(p1.alt != null ? p1.alt : 0) - Number(p2.alt != null ? p2.alt : 0)); + // 经度/纬度阈值用极小值,alt 用米级阈值 + return Math.abs(dLng) < 1e-8 && Math.abs(dLat) < 1e-8 && Math.abs(dAlt) < 2; + }; + let directionPoint = nextPosition; + if (nextPosition && isSamePos(position, nextPosition)) directionPoint = null; + if (!directionPoint && previousPosition && !isSamePos(position, previousPosition)) directionPoint = previousPosition; + if (!directionPoint) directionPoint = nextPosition || previousPosition; const labelData = { name: (route.platform && route.platform.name) ? route.platform.name : '平台', altitude: position.alt != null ? Number(position.alt) : 0, diff --git a/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue b/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue index 88312d2..d259a2d 100644 --- a/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue +++ b/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue @@ -24,7 +24,8 @@