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 07ff430..a269b65 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 @@ -1,6 +1,7 @@ package com.ruoyi.system.mapper; import java.util.List; +import org.apache.ibatis.annotations.Param; import com.ruoyi.system.domain.RouteWaypoints; /** @@ -30,6 +31,9 @@ public interface RouteWaypointsMapper /** 查询指定航线下最大的序号 */ public Integer selectMaxSeqByRouteId(Long routeId); + /** 将指定航线中 seq >= targetSeq 的航点序号均加 1,用于在指定位置插入新航点 */ + int incrementSeqFrom(@Param("routeId") Long routeId, @Param("seq") Long targetSeq); + /** * 新增航线具体航点明细 * diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RouteWaypointsServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RouteWaypointsServiceImpl.java index 75c5ea1..d5cab49 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RouteWaypointsServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RouteWaypointsServiceImpl.java @@ -52,11 +52,14 @@ public class RouteWaypointsServiceImpl implements IRouteWaypointsService @Override public int insertRouteWaypoints(RouteWaypoints routeWaypoints) { - // 1. 获取该航线当前的最高序号 + Long requestedSeq = routeWaypoints.getSeq(); Integer maxSeq = routeWaypointsMapper.selectMaxSeqByRouteId(routeWaypoints.getRouteId()); - // 2. 如果是第一条,序号为1;否则在最大值基础上 +1 - if (maxSeq == null) { + // 若前端传入有效 seq(在指定位置插入),则先将该位置及之后的航点 seq 均加 1,再插入 + if (requestedSeq != null && requestedSeq > 0 && maxSeq != null && requestedSeq <= maxSeq) { + routeWaypointsMapper.incrementSeqFrom(routeWaypoints.getRouteId(), requestedSeq); + // 使用前端传入的 seq + } else if (maxSeq == null) { routeWaypoints.setSeq(1L); } else { routeWaypoints.setSeq((long) (maxSeq + 1)); diff --git a/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml b/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml index 707b059..908e959 100644 --- a/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml @@ -51,6 +51,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select max(seq) from ry.route_waypoints where route_id = #{routeId} + + update route_waypoints set seq = seq + 1 where route_id = #{routeId} and seq >= #{seq} + + insert into route_waypoints diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index f9a6d35..e7ca358 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -1448,9 +1448,12 @@ export default { } }); } else { + const prev = this.drawingPoints && this.drawingPoints.length >= 1 ? this.drawingPoints[this.drawingPoints.length - 1] : null; + const clockwise = p.clockwise !== false; + const circleCenter = isCircle && prev ? this.getHoldCenterFromPrevNext(prev, center, radius, clockwise) : center; this.tempHoldOutlineEntity = this.viewer.entities.add({ id: 'temp_hold_outline', - position: center, + position: circleCenter, ellipse: { semiMajorAxis: semiMajor, semiMinorAxis: semiMinor, @@ -2108,26 +2111,11 @@ export default { // 判断航点 i 是否为“转弯半径”航点(将用弧线两端两个点替代中心点);非首尾默认 45° 坡度 const isTurnWaypointWithArc = (i) => { if (i < 1 || i >= waypoints.length - 1) return false; - // 下一航点是盘旋点时,不隐藏当前航点(只画直线到盘旋入口,不画转弯弧),否则会丢失航点显示 - if (this.isHoldWaypoint(waypoints[i + 1])) return false; const wp = waypoints[i]; const effectiveAngle = this.getEffectiveTurnAngle(wp, i, waypoints.length); if (this.getWaypointRadius({ ...wp, turnAngle: effectiveAngle }) <= 0) return false; const nextPos = originalPositions[i + 1]; - let nextLogical = nextPos; - if (this.isHoldWaypoint(waypoints[i + 1])) { - const nextWp = waypoints[i + 1]; - const holdParams = this.parseHoldParams(nextWp); - const nextHoldIsCircle = (nextWp.pointType || nextWp.point_type) === 'hold_circle'; - if (nextHoldIsCircle) { - const clock = holdParams && holdParams.clockwise !== false; - const r = holdParams && holdParams.radius != null ? holdParams.radius : 500; - nextLogical = this.getCircleTangentEntryPoint(originalPositions[i + 1], originalPositions[i], r, clock); - } else { - nextLogical = originalPositions[i + 1]; - } - } - return !!nextLogical; + return !!nextPos; }; // 遍历并绘制航点标记:盘旋处在圆心画点+标签;有转弯半径时只画 entry/exit 两个虚拟点(见下方连线逻辑),不画原航点中心。支持每航点 pixelSize/color/outlineColor waypoints.forEach((wp, index) => { @@ -2420,15 +2408,16 @@ export default { const legIndexHold = i - 1; const pt = wp.pointType || wp.point_type; const useCircle = pt === 'hold_circle'; - const defaultRadius = params && params.radius != null ? params.radius : 500; - const radius = (this._routeHoldRadiiByRoute && this._routeHoldRadiiByRoute[routeId] && this._routeHoldRadiiByRoute[routeId][legIndexHold] != null) - ? this._routeHoldRadiiByRoute[routeId][legIndexHold] - : defaultRadius; + const effectiveTurnAngle = this.getEffectiveTurnAngle(wp, i, waypoints.length); + const turnRadius = this.getWaypointRadius({ ...wp, turnAngle: effectiveTurnAngle }) || 500; + // 地图显示半径始终用速度+坡度公式计算,推演反算半径仅用于推演内部,不影响显示 + const radius = turnRadius; + const turnRadiusForHold = turnRadius; const defaultSemiMajor = params && (params.semiMajor != null || params.semiMajorAxis != null) ? (params.semiMajor ?? params.semiMajorAxis) : 500; const defaultSemiMinor = params && (params.semiMinor != null || params.semiMinorAxis != null) ? (params.semiMinor ?? params.semiMinorAxis) : 300; const defaultHeadingRad = ((params && params.headingDeg != null ? params.headingDeg : 0) * Math.PI) / 180; const edgeLengthM = Math.max(1000, params && params.edgeLength != null ? params.edgeLength : this.DEFAULT_RACE_TRACK_EDGE_LENGTH_M); - const arcRadiusM = this.getWaypointRadius({ ...wp, turnAngle: this.getEffectiveTurnAngle(wp, i, waypoints.length) }) || 500; + const arcRadiusM = turnRadius; const clockwise = params && params.clockwise !== false; const currPosCloned = Cesium.Cartesian3.clone(currPos); const lastPosCloned = Cesium.Cartesian3.clone(lastPos); @@ -2441,27 +2430,28 @@ export default { const smj = isCircleArg ? defaultSemiMajor : (radiusOrEllipse.semiMajor ?? defaultSemiMajor); const smn = isCircleArg ? defaultSemiMinor : (radiusOrEllipse.semiMinor ?? defaultSemiMinor); const hd = isCircleArg ? defaultHeadingRad : ((radiusOrEllipse.headingDeg != null ? radiusOrEllipse.headingDeg * Math.PI / 180 : defaultHeadingRad)); - const entry = useCircle - ? that.getCircleTangentEntryPoint(currPosCloned, lastPosCloned, R, clockwise) - : that.getEllipseTangentEntryPoint(currPosCloned, lastPosCloned, smj, smn, hd, clockwise); - const exit = useCircle - ? that.getCircleTangentExitPoint(currPosCloned, nextPosCloned || currPosCloned, R, clockwise) - : that.getEllipseTangentExitPoint(currPosCloned, nextPosCloned || currPosCloned, smj, smn, hd, clockwise); + let entry; let exit; let centerForCircle; + if (useCircle) { + centerForCircle = that.getHoldCenterFromPrevNext(lastPosCloned, currPosCloned, R, clockwise); + entry = that.getCircleTangentEntryPoint(centerForCircle, lastPosCloned, R, clockwise); + exit = that.getCircleTangentExitPoint(centerForCircle, nextPosCloned || currPosCloned, R, clockwise); + } else { + entry = that.getEllipseTangentEntryPoint(currPosCloned, lastPosCloned, smj, smn, hd, clockwise); + exit = that.getEllipseTangentExitPoint(currPosCloned, nextPosCloned || currPosCloned, smj, smn, hd, clockwise); + } let arcPoints; if (useCircle) { - // 圆形盘旋:先画闭合整圆(360°),再接 entry→exit 弧段以正确连到下一航段 - const entryAngle = (() => { - const enu = Cesium.Transforms.eastNorthUpToFixedFrame(currPosCloned); - const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); - const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); - const toP = Cesium.Cartesian3.subtract(entry, currPosCloned, new Cesium.Cartesian3()); - return Math.atan2(Cesium.Cartesian3.dot(toP, east), Cesium.Cartesian3.dot(toP, north)); - })(); - const circlePts = that.getCircleFullCircle(currPosCloned, R, entryAngle, clockwise, 64); - const arcToExit = that.getCircleArcEntryToExit(currPosCloned, R, entry, exit, clockwise, 24); - arcPoints = circlePts && circlePts.length - ? (arcToExit && arcToExit.length > 1 ? [...circlePts, ...arcToExit.slice(1)] : circlePts) - : (arcToExit && arcToExit.length ? arcToExit : [Cesium.Cartesian3.clone(entry), Cesium.Cartesian3.clone(exit)]); + const center = centerForCircle; + // 圆形盘旋:先绕整圈,再从出口切线飞出(与椭圆盘旋逻辑一致) + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(center); + const eastVec = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const northVec = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const toEntry = Cesium.Cartesian3.subtract(entry, center, new Cesium.Cartesian3()); + const startAngle = Math.atan2(Cesium.Cartesian3.dot(toEntry, eastVec), Cesium.Cartesian3.dot(toEntry, northVec)); + const fullCirclePoints = that.getCircleFullCircle(center, R, startAngle, clockwise, 64); + const arcToExit = that.getCircleArcEntryToExit(center, R, entry, exit, clockwise, 32); + arcPoints = [entry, ...(fullCirclePoints || []).slice(1), ...(arcToExit || []).slice(1)]; + if (!arcPoints || arcPoints.length < 2) arcPoints = [Cesium.Cartesian3.clone(entry), Cesium.Cartesian3.clone(exit)]; return arcPoints; } else { const tEntry = that.cartesianToEllipseParam(currPosCloned, smj, smn, hd, entry); @@ -2477,14 +2467,14 @@ export default { nextPosCloned, params && params.headingDeg != null ? params.headingDeg : 0 ); - const buildRaceTrackPositions = () => that.getRaceTrackLoopPositions(currPosCloned, raceTrackDirectionRad, edgeLengthM, arcRadiusM, clockwise, 24); + const buildRaceTrackPositions = () => that.buildRaceTrackWithEntryExit(currPosCloned, lastPosCloned, nextPosCloned, raceTrackDirectionRad, edgeLengthM, arcRadiusM, clockwise, 24); const holdPositions = useCircle ? buildHoldPositions(radius) : buildRaceTrackPositions(); for (let k = 0; k < holdPositions.length; k++) finalPathPositions.push(holdPositions[k]); const getHoldPositions = () => { if (useCircle) { const R = (that._routeHoldRadiiByRoute && that._routeHoldRadiiByRoute[routeIdHold] && that._routeHoldRadiiByRoute[routeIdHold][legIndexHold] != null) ? that._routeHoldRadiiByRoute[routeIdHold][legIndexHold] - : defaultRadius; + : turnRadiusForHold; return buildHoldPositions(R); } return buildRaceTrackPositions(); @@ -2501,11 +2491,7 @@ export default { }, properties: { routeId: routeId } }); - if (!useCircle && nextPosCloned) { - lastPos = this.getRaceTrackTangentExitPoint(currPosCloned, nextPosCloned, raceTrackDirectionRad, edgeLengthM, arcRadiusM, clockwise); - } else { - lastPos = holdPositions[holdPositions.length - 1]; - } + lastPos = holdPositions[holdPositions.length - 1]; } else { const effectiveAngle = this.getEffectiveTurnAngle(wp, i, waypoints.length); const radius = this.getWaypointRadius({ ...wp, turnAngle: effectiveAngle }); @@ -2515,11 +2501,12 @@ export default { const holdParams = this.parseHoldParams(nextWp); const nextHoldIsCircle = (nextWp.pointType || nextWp.point_type) === 'hold_circle'; if (nextHoldIsCircle) { - const holdRadius = (this._routeHoldRadiiByRoute && this._routeHoldRadiiByRoute[routeId] && this._routeHoldRadiiByRoute[routeId][i] != null) - ? this._routeHoldRadiiByRoute[routeId][i] - : (holdParams && holdParams.radius != null ? holdParams.radius : 500); - const holdClock = holdParams && holdParams.clockwise !== false; - nextLogical = this.getCircleTangentEntryPoint(originalPositions[i + 1], lastPos, holdRadius, holdClock); + const holdPos = originalPositions[i + 1]; + const computedNextR = this.getWaypointRadius({ ...nextWp, turnAngle: this.getEffectiveTurnAngle(nextWp, i + 1, waypoints.length) }); + const r = (computedNextR > 0) ? computedNextR : 500; + const clock = holdParams && holdParams.clockwise !== false; + const center = this.getHoldCenterFromPrevNext(currPos, holdPos, r, clock); + nextLogical = this.getCircleTangentEntryPoint(center, currPos, r, clock); } else { const nextNext = i + 2 < waypoints.length ? originalPositions[i + 2] : null; const nextDir = this.getRaceTrackDirectionRad(originalPositions[i + 1], currPos, nextNext, holdParams && holdParams.headingDeg != null ? holdParams.headingDeg : 0); @@ -2531,24 +2518,8 @@ export default { if (i < waypoints.length - 1 && nextLogical) { const lastPosCloned = Cesium.Cartesian3.clone(lastPos); const nextLogicalCloned = Cesium.Cartesian3.clone(nextLogical); - const nextIsHold = nextPos && i + 1 < waypoints.length && this.isHoldWaypoint(waypoints[i + 1]); - if (nextIsHold) { - // 下一航点是盘旋:只画 lastPos→切线入口,不画转弯弧,避免椭圆内弦、切入顺滑 - finalPathPositions.push(nextLogicalCloned); - lastPos = nextLogicalCloned; - this.viewer.entities.add({ - id: `arc-line-${routeId}-${i}`, - show: false, - polyline: { - positions: new Cesium.CallbackProperty(() => [Cesium.Cartesian3.clone(lastPosCloned), Cesium.Cartesian3.clone(nextLogicalCloned)], false), - width: lineWidth, - material: lineMaterial, - arcType: Cesium.ArcType.NONE, - zIndex: 20 - }, - properties: { routeId: routeId } - }); - } else if (radius > 0) { + const currPosClonedForArc = Cesium.Cartesian3.clone(currPos); + if (radius > 0) { const currPosCloned = Cesium.Cartesian3.clone(currPos); const routeIdCloned = routeId; const dbIdCloned = wp.id; @@ -2717,7 +2688,7 @@ export default { if (index === 0 || index === waypointsLength - 1) return wp.turnAngle != null ? wp.turnAngle : 0; return wp.turnAngle != null ? wp.turnAngle : 45; }, - // 计算单个点的转弯半径 + /** 转弯半径 R = v²/(g·tanθ),v 为速度(m/s),g=9.8,θ 为转弯坡度/坡度角(弧度) */ getWaypointRadius(wp) { const speed = wp.speed || 800; const bankAngle = wp.turnAngle || 0; @@ -2870,6 +2841,256 @@ export default { }, /** + * 跑道形盘旋完整轨迹(含进入/出口切线): + * 进入切线点(弧上)→ 绕跑道一整圈 → 出口切线点(弧上,沿弧连接,无直线)→ 切线飞出。 + * 进入点和出口点均在跑道的某段圆弧上,不会产生进入点与出口点之间的直线连接。 + * + * @param centerCartesian - 盘旋航点(跑道起点 A) + * @param prevCartesian - 上一航点(用于计算进入切线) + * @param nextCartesian - 下一航点(用于计算出口切线),可为 null + * @param directionRad - 跑道轴线方向(弧度) + * @param edgeLengthM - 跑道直边长度(米) + * @param arcRadiusM - 圆弧半径(米) + * @param clockwise - 是否顺时针 + * @param numPointsPerArc - 每段弧采样点数 + * @returns {Cesium.Cartesian3[]} 完整轨迹点数组(进入切线点开始,到出口切线点结束) + */ + buildRaceTrackWithEntryExit(centerCartesian, prevCartesian, nextCartesian, directionRad, edgeLengthM, arcRadiusM, clockwise, numPointsPerArc) { + if (!centerCartesian || edgeLengthM <= 0 || arcRadiusM <= 0) return []; + const R = Math.min(Math.max(Number(arcRadiusM), 1), edgeLengthM / 2); + const dir = Number.isFinite(directionRad) ? directionRad : 0; + const L = edgeLengthM; + const nArc = Math.max(8, numPointsPerArc || 24); + + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + + const toCartesian = (eM, nM) => + Cesium.Cartesian3.add( + Cesium.Cartesian3.add(Cesium.Cartesian3.clone(centerCartesian), Cesium.Cartesian3.multiplyByScalar(east, eM, new Cesium.Cartesian3()), new Cesium.Cartesian3()), + Cesium.Cartesian3.multiplyByScalar(north, nM, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ); + + const dirE = Math.sin(dir); + const dirN = Math.cos(dir); + const rightE = Math.cos(dir); + const rightN = -Math.sin(dir); + const side = clockwise ? 1 : -1; + const offsetE = side * rightE; + const offsetN = side * rightN; + + // 跑道四个角点和两个圆弧圆心(ENU坐标) + const A = { e: 0, n: 0 }; + const B = { e: L * dirE, n: L * dirN }; + const center1ENU = { e: B.e + R * offsetE, n: B.n + R * offsetN }; // 前端圆弧圆心(B侧) + const C = { e: B.e + 2 * R * offsetE, n: B.n + 2 * R * offsetN }; + const D_pt = { e: C.e - L * dirE, n: C.n - L * dirN }; + const center2ENU = { e: A.e + R * offsetE, n: A.n + R * offsetN }; // 后端圆弧圆心(A侧) + + const arc1Center = toCartesian(center1ENU.e, center1ENU.n); + const arc2Center = toCartesian(center2ENU.e, center2ENU.n); + const A_cart = toCartesian(A.e, A.n); + const B_cart = toCartesian(B.e, B.n); + const C_cart = toCartesian(C.e, C.n); + const D_cart = toCartesian(D_pt.e, D_pt.n); + + // 辅助:在圆弧圆心的ENU坐标 + const getCircleCenterENU = (circleCenter) => ({ + e: Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(circleCenter, centerCartesian, new Cesium.Cartesian3()), east), + n: Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(circleCenter, centerCartesian, new Cesium.Cartesian3()), north) + }); + + // 判断点 p 是否在圆弧 arcCenter 上从 arcFrom 到 arcTo 的有效范围内(按盘旋方向) + const isPointOnArcRange = (p, arcCenter, arcFrom, arcTo, isClockwise) => { + const angleOf = (pt) => { + const e = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(pt, arcCenter, new Cesium.Cartesian3()), east); + const n = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(pt, arcCenter, new Cesium.Cartesian3()), north); + return Math.atan2(e, n); + }; + const a0 = angleOf(arcFrom); + const a1 = angleOf(arcTo); + const ap = angleOf(p); + // 将 ap 规范化到从 a0 出发按盘旋方向的范围内 + let span = a1 - a0; + let pos = ap - a0; + if (isClockwise) { + if (span <= 0) span += 2 * Math.PI; + pos = ((pos % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI); + } else { + if (span >= 0) span -= 2 * Math.PI; + pos = -(((-pos % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI)); + } + return isClockwise ? (pos >= 0 && pos <= span) : (pos <= 0 && pos >= span); + }; + + // 从圆弧圆心到外部点的切点,限定在 arcFrom→arcTo 的有效半圆范围内 + // isEntry=false:出口切线(弧上点→外部点方向与切线方向一致) + const getTangentPointOnArc = (circleCenter, externalPoint, isClockwise, arcFrom, arcTo) => { + const toExt = Cesium.Cartesian3.subtract(externalPoint, circleCenter, new Cesium.Cartesian3()); + const d = Cesium.Cartesian3.magnitude(toExt); + if (d <= R) return null; + const eComp = Cesium.Cartesian3.dot(toExt, east); + const nComp = Cesium.Cartesian3.dot(toExt, north); + const theta = Math.atan2(eComp, nComp); + const alpha = Math.acos(Math.min(1, R / d)); + const cENU = getCircleCenterENU(circleCenter); + const pointAtLocal = (angle) => toCartesian(cENU.e + R * Math.sin(angle), cENU.n + R * Math.cos(angle)); + const p1 = pointAtLocal(theta + alpha); + const p2 = pointAtLocal(theta - alpha); + // 切线方向(飞行方向):顺时针=(rN,-rE),逆时针=(-rN,rE) + const tangentDir = (p) => { + const toP_e = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(p, circleCenter, new Cesium.Cartesian3()), east); + const toP_n = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(p, circleCenter, new Cesium.Cartesian3()), north); + const tE = isClockwise ? toP_n : -toP_n; + const tN = isClockwise ? -toP_e : toP_e; + const len = Math.sqrt(tE * tE + tN * tN); + if (len < 1e-9) return null; + return { e: tE / len, n: tN / len }; + }; + // 出口方向:弧上点→外部点 + const connectionDir = (p) => { + const v = Cesium.Cartesian3.subtract(externalPoint, p, new Cesium.Cartesian3()); + const vLen = Cesium.Cartesian3.magnitude(v); + if (vLen < 1e-9) return null; + return { e: Cesium.Cartesian3.dot(v, east) / vLen, n: Cesium.Cartesian3.dot(v, north) / vLen }; + }; + const score = (p) => { + // 必须在有效弧段范围内 + if (!isPointOnArcRange(p, circleCenter, arcFrom, arcTo, isClockwise)) return -3; + const td = tangentDir(p); + const cd = connectionDir(p); + if (!td || !cd) return -2; + return td.e * cd.e + td.n * cd.n; + }; + const s1 = score(p1); + const s2 = score(p2); + if (s1 < -2 && s2 < -2) return null; // 两个切点都不在有效弧段内 + return s1 >= s2 ? p1 : p2; + }; + + // 在圆弧上从 fromPoint 到 toPoint 采样(按盘旋方向,不包含 fromPoint,包含 toPoint) + const arcSegment = (arcCenter, fromPoint, toPoint, isClockwise, nPts) => { + if (Cesium.Cartesian3.distance(fromPoint, toPoint) < 1e-3) return []; + const arcCenterE = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(arcCenter, centerCartesian, new Cesium.Cartesian3()), east); + const arcCenterN = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(arcCenter, centerCartesian, new Cesium.Cartesian3()), north); + const fromE = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(fromPoint, arcCenter, new Cesium.Cartesian3()), east); + const fromN = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(fromPoint, arcCenter, new Cesium.Cartesian3()), north); + const toE = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(toPoint, arcCenter, new Cesium.Cartesian3()), east); + const toN = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(toPoint, arcCenter, new Cesium.Cartesian3()), north); + let a0 = Math.atan2(fromE, fromN); + let a1 = Math.atan2(toE, toN); + let d = a1 - a0; + if (isClockwise) { + if (d <= 0) d += 2 * Math.PI; + } else { + if (d >= 0) d -= 2 * Math.PI; + } + const pts = []; + for (let i = 1; i <= nPts; i++) { + const t = i / nPts; + const a = a0 + d * t; + pts.push(toCartesian(arcCenterE + R * Math.sin(a), arcCenterN + R * Math.cos(a))); + } + return pts; + }; + + // 2D 线段相交检测(ENU 平面),用于判断出口线是否穿越跑道直边 + const toENU2D = (cart) => ({ + e: Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(cart, centerCartesian, new Cesium.Cartesian3()), east), + n: Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(cart, centerCartesian, new Cesium.Cartesian3()), north) + }); + const segmentsIntersect = (p1, p2, p3, p4) => { + const d1e = p2.e - p1.e, d1n = p2.n - p1.n; + const d2e = p4.e - p3.e, d2n = p4.n - p3.n; + const cross = d1e * d2n - d1n * d2e; + if (Math.abs(cross) < 1e-9) return false; + const t = ((p3.e - p1.e) * d2n - (p3.n - p1.n) * d2e) / cross; + const u = ((p3.e - p1.e) * d1n - (p3.n - p1.n) * d1e) / cross; + return t > 0.01 && t < 0.99 && u > 0.01 && u < 0.99; + }; + const exitCrossesTrack = (ep) => { + if (!nextCartesian) return false; + const epENU = toENU2D(ep); + const nextENU = toENU2D(nextCartesian); + const aENU = toENU2D(A_cart); + const bENU = toENU2D(B_cart); + const cENU = toENU2D(C_cart); + const dENU = toENU2D(D_cart); + return segmentsIntersect(epENU, nextENU, aENU, bENU) || + segmentsIntersect(epENU, nextENU, cENU, dENU); + }; + + // 在 arc1(前端半圆 B→C)和 arc2(后端半圆 D→A)上分别寻找切线出口点 + let exitPoint = null; + let exitOnArc1 = true; + if (nextCartesian) { + const ex1 = getTangentPointOnArc(arc1Center, nextCartesian, clockwise, B_cart, C_cart); + const ex2 = getTangentPointOnArc(arc2Center, nextCartesian, clockwise, D_cart, A_cart); + const cross1 = ex1 ? exitCrossesTrack(ex1) : true; + const cross2 = ex2 ? exitCrossesTrack(ex2) : true; + if (ex1 && ex2) { + if (cross1 && !cross2) { + exitPoint = ex2; exitOnArc1 = false; + } else if (!cross1 && cross2) { + exitPoint = ex1; exitOnArc1 = true; + } else { + // 两个都不穿越或都穿越,选 score 更高的 + const scoreOf = (p, arcCenter) => { + const v = Cesium.Cartesian3.subtract(nextCartesian, p, new Cesium.Cartesian3()); + const vLen = Cesium.Cartesian3.magnitude(v); + if (vLen < 1e-9) return -2; + const cd = { e: Cesium.Cartesian3.dot(v, east) / vLen, n: Cesium.Cartesian3.dot(v, north) / vLen }; + const toP_e = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(p, arcCenter, new Cesium.Cartesian3()), east); + const toP_n = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(p, arcCenter, new Cesium.Cartesian3()), north); + const tE = clockwise ? toP_n : -toP_n; + const tN = clockwise ? -toP_e : toP_e; + const tLen = Math.sqrt(tE * tE + tN * tN); + if (tLen < 1e-9) return -2; + return (tE / tLen) * cd.e + (tN / tLen) * cd.n; + }; + const s1 = scoreOf(ex1, arc1Center); + const s2 = scoreOf(ex2, arc2Center); + exitPoint = s1 >= s2 ? ex1 : ex2; + exitOnArc1 = s1 >= s2; + } + } else if (ex1) { + exitPoint = ex1; exitOnArc1 = true; + } else if (ex2) { + exitPoint = ex2; exitOnArc1 = false; + } + } + if (!exitPoint) { + exitPoint = Cesium.Cartesian3.clone(C_cart); + exitOnArc1 = true; + } + + // 跑道形盘旋轨迹:完整一圈 + 第二圈走到出口点 + // 出口在 arc1:一圈 + A→B→arc1(B→exitPoint) + // 出口在 arc2:一圈 + A→B→arc1(B→C)→C→D→arc2(D→exitPoint) + const points = []; + points.push(Cesium.Cartesian3.clone(A_cart)); + points.push(Cesium.Cartesian3.clone(B_cart)); + points.push(...arcSegment(arc1Center, B_cart, C_cart, clockwise, nArc)); + points.push(Cesium.Cartesian3.clone(D_cart)); + points.push(...arcSegment(arc2Center, D_cart, A_cart, clockwise, nArc)); + const loopEndIndex = points.length - 1; + if (exitOnArc1) { + points.push(Cesium.Cartesian3.clone(B_cart)); + points.push(...arcSegment(arc1Center, B_cart, exitPoint, clockwise, nArc)); + } else { + points.push(Cesium.Cartesian3.clone(B_cart)); + points.push(...arcSegment(arc1Center, B_cart, C_cart, clockwise, nArc)); + points.push(Cesium.Cartesian3.clone(D_cart)); + points.push(...arcSegment(arc2Center, D_cart, exitPoint, clockwise, nArc)); + } + + points._loopEndIndex = loopEndIndex; + return points; + }, + + /** * 跑道弧上切线出口点:从跑道飞往下一航点时,弧上与下一航点连成切线的点(航线=弧上该点→下一航点)。 * 与圆轨迹一致:当前盘旋点与下一航点之间的航线为弧上一点与下一航点之间形成的切线。 */ @@ -2894,108 +3115,134 @@ export default { const side = clockwise ? 1 : -1; const offsetE = side * rightE; const offsetN = side * rightN; + const A_cart = Cesium.Cartesian3.clone(centerCartesian); const B = { e: L * dirE, n: L * dirN }; - const A = Cesium.Cartesian3.clone(centerCartesian); + const B_cart = toCartesian(B.e, B.n); + const C = { e: B.e + 2 * R * offsetE, n: B.n + 2 * R * offsetN }; + const C_cart = toCartesian(C.e, C.n); + const D_pt = { e: C.e - L * dirE, n: C.n - L * dirN }; + const D_cart = toCartesian(D_pt.e, D_pt.n); const arc1Center = toCartesian(B.e + R * offsetE, B.n + R * offsetN); const arc2Center = toCartesian(R * offsetE, R * offsetN); - const B_cart = toCartesian(B.e, B.n); - const tangentFromCircle = (circleCenter, oppositePoint, revDir) => { - const toNext = Cesium.Cartesian3.subtract(nextPointCartesian, circleCenter, new Cesium.Cartesian3()); - const d = Cesium.Cartesian3.magnitude(toNext); - if (d < 1e-6) return null; - if (R >= d) return null; - const e = Cesium.Cartesian3.dot(toNext, east); - const n = Cesium.Cartesian3.dot(toNext, north); - const theta = Math.atan2(e, n); - const alpha = Math.acos(Math.min(1, R / d)); - const pointAt = (angle) => { - const off = Cesium.Cartesian3.add( - Cesium.Cartesian3.multiplyByScalar(north, Math.cos(angle) * R, new Cesium.Cartesian3()), - Cesium.Cartesian3.multiplyByScalar(east, Math.sin(angle) * R, new Cesium.Cartesian3()), - new Cesium.Cartesian3() - ); - return Cesium.Cartesian3.add(circleCenter, off, new Cesium.Cartesian3()); + + const isPointOnArcRange = (p, arcCenter, arcFrom, arcTo, isClockwise) => { + const angleOf = (pt) => { + const e = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(pt, arcCenter, new Cesium.Cartesian3()), east); + const n = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(pt, arcCenter, new Cesium.Cartesian3()), north); + return Math.atan2(e, n); }; + const a0 = angleOf(arcFrom); + const a1 = angleOf(arcTo); + const ap = angleOf(p); + let span = a1 - a0; + let pos = ap - a0; + if (isClockwise) { + if (span <= 0) span += 2 * Math.PI; + pos = ((pos % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI); + } else { + if (span >= 0) span -= 2 * Math.PI; + pos = -(((-pos % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI)); + } + return isClockwise ? (pos >= 0 && pos <= span) : (pos <= 0 && pos >= span); + }; + const getTangentOnArc = (circleCenter, arcFrom, arcTo) => { + const toExt = Cesium.Cartesian3.subtract(nextPointCartesian, circleCenter, new Cesium.Cartesian3()); + const dist = Cesium.Cartesian3.magnitude(toExt); + if (dist <= R) return null; + const eComp = Cesium.Cartesian3.dot(toExt, east); + const nComp = Cesium.Cartesian3.dot(toExt, north); + const theta = Math.atan2(eComp, nComp); + const alpha = Math.acos(Math.min(1, R / dist)); + const cE = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(circleCenter, centerCartesian, new Cesium.Cartesian3()), east); + const cN = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(circleCenter, centerCartesian, new Cesium.Cartesian3()), north); + const pointAt = (angle) => toCartesian(cE + R * Math.sin(angle), cN + R * Math.cos(angle)); const p1 = pointAt(theta + alpha); const p2 = pointAt(theta - alpha); - const onArc = (p) => { - const toP = Cesium.Cartesian3.subtract(p, circleCenter, new Cesium.Cartesian3()); - const toOpp = Cesium.Cartesian3.subtract(oppositePoint, circleCenter, new Cesium.Cartesian3()); - return Cesium.Cartesian3.dot(toP, toOpp) <= 0; + const tangentDir = (p) => { + const toP_e = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(p, circleCenter, new Cesium.Cartesian3()), east); + const toP_n = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(p, circleCenter, new Cesium.Cartesian3()), north); + const tE = clockwise ? toP_n : -toP_n; + const tN = clockwise ? -toP_e : toP_e; + const len = Math.sqrt(tE * tE + tN * tN); + if (len < 1e-9) return null; + return { e: tE / len, n: tN / len }; }; - const exitDir = (p) => Cesium.Cartesian3.normalize(Cesium.Cartesian3.subtract(nextPointCartesian, p, new Cesium.Cartesian3()), new Cesium.Cartesian3()); - const tangentAt = (p) => { - const toP = Cesium.Cartesian3.subtract(p, circleCenter, new Cesium.Cartesian3()); - const rn = Cesium.Cartesian3.dot(toP, north); - const re = Cesium.Cartesian3.dot(toP, east); - const tn = revDir ? re : -re; - const te = revDir ? -rn : rn; - return Cesium.Cartesian3.normalize(Cesium.Cartesian3.add( - Cesium.Cartesian3.multiplyByScalar(north, tn, new Cesium.Cartesian3()), - Cesium.Cartesian3.multiplyByScalar(east, te, new Cesium.Cartesian3()), - new Cesium.Cartesian3() - ), new Cesium.Cartesian3()); + const connectionDir = (p) => { + const v = Cesium.Cartesian3.subtract(nextPointCartesian, p, new Cesium.Cartesian3()); + const vLen = Cesium.Cartesian3.magnitude(v); + if (vLen < 1e-9) return null; + return { e: Cesium.Cartesian3.dot(v, east) / vLen, n: Cesium.Cartesian3.dot(v, north) / vLen }; }; - let best = null; - let bestDot = -2; - [p1, p2].forEach((p) => { - if (!onArc(p)) return; - const ex = exitDir(p); - const ta = tangentAt(p); - const dot = Cesium.Cartesian3.dot(ex, ta); - if (dot > bestDot) { bestDot = dot; best = p; } - }); - return best; + const score = (p) => { + if (!isPointOnArcRange(p, circleCenter, arcFrom, arcTo, clockwise)) return -3; + const td = tangentDir(p); + const cd = connectionDir(p); + if (!td || !cd) return -2; + return td.e * cd.e + td.n * cd.n; + }; + const s1 = score(p1); + const s2 = score(p2); + if (s1 < -2 && s2 < -2) return null; + return s1 >= s2 ? p1 : p2; }; - // 弧的遍历方向与跑道一致:顺时针=角度递减,切线方向 revDir=clockwise - const t1 = tangentFromCircle(arc1Center, A, clockwise); - const t2 = tangentFromCircle(arc2Center, B_cart, clockwise); - let candidates = [t1, t2].filter(Boolean); - if (candidates.length === 0) { - const fallback = (circleCenter, revDir) => { - const toNext = Cesium.Cartesian3.subtract(nextPointCartesian, circleCenter, new Cesium.Cartesian3()); - const d = Cesium.Cartesian3.magnitude(toNext); - if (d < 1e-6 || R >= d) return null; - const e = Cesium.Cartesian3.dot(toNext, east); - const n = Cesium.Cartesian3.dot(toNext, north); - const theta = Math.atan2(e, n); - const alpha = Math.acos(Math.min(1, R / d)); - const pointAt = (angle) => { - const off = Cesium.Cartesian3.add( - Cesium.Cartesian3.multiplyByScalar(north, Math.cos(angle) * R, new Cesium.Cartesian3()), - Cesium.Cartesian3.multiplyByScalar(east, Math.sin(angle) * R, new Cesium.Cartesian3()), - new Cesium.Cartesian3() - ); - return Cesium.Cartesian3.add(circleCenter, off, new Cesium.Cartesian3()); - }; - const p1 = pointAt(theta + alpha); - const p2 = pointAt(theta - alpha); - const exitDir = (p) => Cesium.Cartesian3.normalize(Cesium.Cartesian3.subtract(nextPointCartesian, p, new Cesium.Cartesian3()), new Cesium.Cartesian3()); - const tangentAt = (p) => { - const toP = Cesium.Cartesian3.subtract(p, circleCenter, new Cesium.Cartesian3()); - const rn = Cesium.Cartesian3.dot(toP, north); - const re = Cesium.Cartesian3.dot(toP, east); - const tn = revDir ? re : -re; - const te = revDir ? -rn : rn; - return Cesium.Cartesian3.normalize(Cesium.Cartesian3.add( - Cesium.Cartesian3.multiplyByScalar(north, tn, new Cesium.Cartesian3()), - Cesium.Cartesian3.multiplyByScalar(east, te, new Cesium.Cartesian3()), - new Cesium.Cartesian3() - ), new Cesium.Cartesian3()); + const toENU2D = (cart) => ({ + e: Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(cart, centerCartesian, new Cesium.Cartesian3()), east), + n: Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(cart, centerCartesian, new Cesium.Cartesian3()), north) + }); + const segmentsIntersect = (p1, p2, p3, p4) => { + const d1e = p2.e - p1.e, d1n = p2.n - p1.n; + const d2e = p4.e - p3.e, d2n = p4.n - p3.n; + const cross = d1e * d2n - d1n * d2e; + if (Math.abs(cross) < 1e-9) return false; + const t = ((p3.e - p1.e) * d2n - (p3.n - p1.n) * d2e) / cross; + const u = ((p3.e - p1.e) * d1n - (p3.n - p1.n) * d1e) / cross; + return t > 0.01 && t < 0.99 && u > 0.01 && u < 0.99; + }; + const exitCrossesTrack = (ep) => { + const epENU = toENU2D(ep); + const nextENU = toENU2D(nextPointCartesian); + const aENU = toENU2D(A_cart); + const bENU = toENU2D(B_cart); + const cENU = toENU2D(C_cart); + const dENU = toENU2D(D_cart); + return segmentsIntersect(epENU, nextENU, aENU, bENU) || + segmentsIntersect(epENU, nextENU, cENU, dENU); + }; + const ex1 = getTangentOnArc(arc1Center, B_cart, C_cart); + const ex2 = getTangentOnArc(arc2Center, D_cart, A_cart); + const cross1 = ex1 ? exitCrossesTrack(ex1) : true; + const cross2 = ex2 ? exitCrossesTrack(ex2) : true; + let exitPoint = null; + if (ex1 && ex2) { + if (cross1 && !cross2) { + exitPoint = ex2; + } else if (!cross1 && cross2) { + exitPoint = ex1; + } else { + const scoreOf = (p, arcCenter) => { + const v = Cesium.Cartesian3.subtract(nextPointCartesian, p, new Cesium.Cartesian3()); + const vLen = Cesium.Cartesian3.magnitude(v); + if (vLen < 1e-9) return -2; + const cd = { e: Cesium.Cartesian3.dot(v, east) / vLen, n: Cesium.Cartesian3.dot(v, north) / vLen }; + const toP_e = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(p, arcCenter, new Cesium.Cartesian3()), east); + const toP_n = Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(p, arcCenter, new Cesium.Cartesian3()), north); + const tE = clockwise ? toP_n : -toP_n; + const tN = clockwise ? -toP_e : toP_e; + const tLen = Math.sqrt(tE * tE + tN * tN); + if (tLen < 1e-9) return -2; + return (tE / tLen) * cd.e + (tN / tLen) * cd.n; }; - const d1 = Cesium.Cartesian3.dot(exitDir(p1), tangentAt(p1)); - const d2 = Cesium.Cartesian3.dot(exitDir(p2), tangentAt(p2)); - return d1 >= d2 ? p1 : p2; - }; - const f1 = fallback(arc1Center, clockwise); - const f2 = fallback(arc2Center, clockwise); - candidates = [f1, f2].filter(Boolean); + const s1 = scoreOf(ex1, arc1Center); + const s2 = scoreOf(ex2, arc2Center); + exitPoint = s1 >= s2 ? ex1 : ex2; + } + } else if (ex1) { + exitPoint = ex1; + } else if (ex2) { + exitPoint = ex2; } - if (candidates.length === 0) return Cesium.Cartesian3.clone(A); - if (candidates.length === 1) return Cesium.Cartesian3.clone(candidates[0]); - const dist0 = Cesium.Cartesian3.distance(candidates[0], nextPointCartesian); - const dist1 = Cesium.Cartesian3.distance(candidates[1], nextPointCartesian); - return Cesium.Cartesian3.clone(dist0 <= dist1 ? candidates[0] : candidates[1]); + if (!exitPoint) exitPoint = C_cart; + return Cesium.Cartesian3.clone(exitPoint); }, /** @@ -3051,18 +3298,24 @@ export default { const toOpp = Cesium.Cartesian3.subtract(oppositePoint, circleCenter, new Cesium.Cartesian3()); return Cesium.Cartesian3.dot(toP, toOpp) <= 0; }; - const approachDir = (p) => Cesium.Cartesian3.normalize(Cesium.Cartesian3.subtract(p, prevPointCartesian, new Cesium.Cartesian3()), new Cesium.Cartesian3()); + const approachDir = (p) => { + const v = Cesium.Cartesian3.subtract(p, prevPointCartesian, new Cesium.Cartesian3()); + if (Cesium.Cartesian3.magnitude(v) < 1e-9) return null; + return Cesium.Cartesian3.normalize(v, new Cesium.Cartesian3()); + }; const tangentAt = (p) => { const toP = Cesium.Cartesian3.subtract(p, circleCenter, new Cesium.Cartesian3()); const rn = Cesium.Cartesian3.dot(toP, north); const re = Cesium.Cartesian3.dot(toP, east); const tn = revDir ? re : -re; const te = revDir ? -rn : rn; - return Cesium.Cartesian3.normalize(Cesium.Cartesian3.add( + const sum = Cesium.Cartesian3.add( Cesium.Cartesian3.multiplyByScalar(north, tn, new Cesium.Cartesian3()), Cesium.Cartesian3.multiplyByScalar(east, te, new Cesium.Cartesian3()), new Cesium.Cartesian3() - ), new Cesium.Cartesian3()); + ); + if (Cesium.Cartesian3.magnitude(sum) < 1e-9) return null; + return Cesium.Cartesian3.normalize(sum, new Cesium.Cartesian3()); }; let best = null; let bestDot = -2; @@ -3070,6 +3323,7 @@ export default { if (!onArc(p)) return; const ap = approachDir(p); const ta = tangentAt(p); + if (!ap || !ta) return; const dot = Cesium.Cartesian3.dot(ap, ta); if (dot > bestDot) { bestDot = dot; best = p; } }); @@ -3235,26 +3489,77 @@ export default { computeArcPositions(p1, p2, p3, radius) { const v1 = Cesium.Cartesian3.subtract(p1, p2, new Cesium.Cartesian3()); const v2 = Cesium.Cartesian3.subtract(p3, p2, new Cesium.Cartesian3()); + const m1 = Cesium.Cartesian3.magnitude(v1); + const m2 = Cesium.Cartesian3.magnitude(v2); + if (m1 < 1e-9 || m2 < 1e-9) return [Cesium.Cartesian3.clone(p2), Cesium.Cartesian3.clone(p3)]; + const dir1 = Cesium.Cartesian3.normalize(v1, new Cesium.Cartesian3()); const dir2 = Cesium.Cartesian3.normalize(v2, new Cesium.Cartesian3()); - const angle = Cesium.Cartesian3.angleBetween(dir1, dir2); - const dist = radius / Math.tan(angle / 2); - - const t1 = Cesium.Cartesian3.add(p2, Cesium.Cartesian3.multiplyByScalar(dir1, dist, new Cesium.Cartesian3()), new Cesium.Cartesian3()); - const t2 = Cesium.Cartesian3.add(p2, Cesium.Cartesian3.multiplyByScalar(dir2, dist, new Cesium.Cartesian3()), new Cesium.Cartesian3()); - - let arc = []; - // 采样15个点让弧线丝滑 - for (let t = 0; t <= 1; t += 0.07) { - const c1 = Math.pow(1 - t, 2); - const c2 = 2 * (1 - t) * t; - const c3 = Math.pow(t, 2); - arc.push(new Cesium.Cartesian3( - c1 * t1.x + c2 * p2.x + c3 * t2.x, - c1 * t1.y + c2 * p2.y + c3 * t2.y, - c1 * t1.z + c2 * p2.z + c3 * t2.z - )); + + // 两段航线近似平行或反向时,退化为直线经过航点 + if (angle < 1e-4 || angle > Math.PI - 1e-4) { + return [Cesium.Cartesian3.clone(p2), Cesium.Cartesian3.clone(p3)]; + } + + const halfAngle = angle / 2; + // 切点到航点的距离 + const dist = radius / Math.tan(halfAngle); + // 圆心到航点的距离(沿角平分线方向) + const centerDist = radius / Math.sin(halfAngle); + + // 限制切点不超出前后两段航线的实际长度,避免切点飞出航段范围 + const safeDist = Math.min(dist, m1 * 0.95, m2 * 0.95); + const safeRadius = safeDist * Math.tan(halfAngle); + const safeCenterDist = safeRadius / Math.sin(halfAngle); + + const t1 = Cesium.Cartesian3.add(p2, Cesium.Cartesian3.multiplyByScalar(dir1, safeDist, new Cesium.Cartesian3()), new Cesium.Cartesian3()); + const t2 = Cesium.Cartesian3.add(p2, Cesium.Cartesian3.multiplyByScalar(dir2, safeDist, new Cesium.Cartesian3()), new Cesium.Cartesian3()); + + // 角平分线方向指向内侧(转弯圆心所在方向) + const bisector = Cesium.Cartesian3.normalize( + Cesium.Cartesian3.add(dir1, dir2, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ); + const center = Cesium.Cartesian3.add( + p2, + Cesium.Cartesian3.multiplyByScalar(bisector, safeCenterDist, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ); + + // 在圆心处建立 ENU 坐标系,计算 t1/t2 对应的角度 + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(center); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + + const toT1 = Cesium.Cartesian3.subtract(t1, center, new Cesium.Cartesian3()); + const toT2 = Cesium.Cartesian3.subtract(t2, center, new Cesium.Cartesian3()); + const startAngle = Math.atan2(Cesium.Cartesian3.dot(toT1, east), Cesium.Cartesian3.dot(toT1, north)); + const endAngle = Math.atan2(Cesium.Cartesian3.dot(toT2, east), Cesium.Cartesian3.dot(toT2, north)); + + // 用叉积判断转弯方向(顺/逆时针) + const cross = Cesium.Cartesian3.cross(dir1, dir2, new Cesium.Cartesian3()); + const up = Cesium.Cartesian3.normalize(center, new Cesium.Cartesian3()); + const clockwise = Cesium.Cartesian3.dot(cross, up) < 0; + + let delta = endAngle - startAngle; + if (clockwise) { + if (delta > 0) delta -= 2 * Math.PI; + } else { + if (delta < 0) delta += 2 * Math.PI; + } + + // 按弧长自适应采样点数,保证弧线平滑 + const numPoints = Math.max(8, Math.ceil(Math.abs(delta) / (Math.PI / 32))); + const arc = []; + for (let i = 0; i <= numPoints; i++) { + const a = startAngle + (delta * i) / numPoints; + const offset = Cesium.Cartesian3.add( + Cesium.Cartesian3.multiplyByScalar(north, Math.cos(a) * safeRadius, new Cesium.Cartesian3()), + Cesium.Cartesian3.multiplyByScalar(east, Math.sin(a) * safeRadius, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ); + arc.push(Cesium.Cartesian3.add(center, offset, new Cesium.Cartesian3())); } return arc; }, @@ -3303,6 +3608,37 @@ export default { return Cesium.Cartesian3.add(prevPointCartesian, Cesium.Cartesian3.multiplyByScalar(unit, dist - radiusMeters, new Cesium.Cartesian3()), new Cesium.Cartesian3()); }, + /** + * 圆形盘旋:圆心在两航点连线的垂线上,第二个航点(hold 盘旋点)为垂足,圆心距垂足为半径 R。 + * 根据半径可算出两个圆心位置(垂线两侧),由 clockwise 选择顺时针/逆时针对应的圆心。 + * 注意:holdPoint 为盘旋航点位置,使盘旋发生在该点附近而非下一航点。 + * @param prevPointCartesian - 上一航点 + * @param holdPointCartesian - 盘旋航点位置(垂足,在圆上) + * @param radiusMeters - 圆半径(米) + * @param clockwise - 是否顺时针盘旋,用于确定取哪侧圆心 + */ + getHoldCenterFromPrevNext(prevPointCartesian, holdPointCartesian, radiusMeters, clockwise) { + const v = Cesium.Cartesian3.subtract(holdPointCartesian, prevPointCartesian, new Cesium.Cartesian3()); + const dist = Cesium.Cartesian3.magnitude(v); + if (dist < 1e-6) return Cesium.Cartesian3.clone(holdPointCartesian); + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(holdPointCartesian); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const vE = Cesium.Cartesian3.dot(v, east); + const vN = Cesium.Cartesian3.dot(v, north); + // 垂直于 prev->next 的单位向量:右垂向 (vN, -vE),顺时针盘旋时圆心在右侧 + const perpE = vN; + const perpN = -vE; + const perpLen = Math.sqrt(perpE * perpE + perpN * perpN) || 1; + const sign = clockwise ? 1 : -1; + const perp = Cesium.Cartesian3.add( + Cesium.Cartesian3.multiplyByScalar(east, (sign * perpE) / perpLen, new Cesium.Cartesian3()), + Cesium.Cartesian3.multiplyByScalar(north, (sign * perpN) / perpLen, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ); + return Cesium.Cartesian3.add(holdPointCartesian, Cesium.Cartesian3.multiplyByScalar(perp, radiusMeters, new Cesium.Cartesian3()), new Cesium.Cartesian3()); + }, + /** 圆上切线进入点:从 prev 飞向圆时在圆上的切点。选使「接近方向」与「圆上飞行方向」最一致的那侧,进入时转弯最小、顺滑(见图三:顺着大方向盘旋) */ getCircleTangentEntryPoint(centerCartesian, prevPointCartesian, radiusMeters, clockwise) { const toPrev = Cesium.Cartesian3.subtract(prevPointCartesian, centerCartesian, new Cesium.Cartesian3()); @@ -3330,8 +3666,10 @@ export default { const p2 = pointAt(angle2); const approach1 = Cesium.Cartesian3.subtract(p1, prevPointCartesian, new Cesium.Cartesian3()); const approach2 = Cesium.Cartesian3.subtract(p2, prevPointCartesian, new Cesium.Cartesian3()); - Cesium.Cartesian3.normalize(approach1, approach1); - Cesium.Cartesian3.normalize(approach2, approach2); + const mag1 = Cesium.Cartesian3.magnitude(approach1); + const mag2 = Cesium.Cartesian3.magnitude(approach2); + if (mag1 >= 1e-9) Cesium.Cartesian3.normalize(approach1, approach1); + if (mag2 >= 1e-9) Cesium.Cartesian3.normalize(approach2, approach2); const tangentAt = (angle) => { const tn = clockwise ? -Math.sin(angle) : Math.sin(angle); const te = clockwise ? Math.cos(angle) : -Math.cos(angle); @@ -3349,28 +3687,27 @@ export default { }, /** - * 根据盘旋总距离反算圆半径,使从进入点到切点出口的弧长 = totalHoldDistM(整圈 + entry→exit 弧), - * 从而在相对K时时刻飞机自然落在切点。使用与 getRoutePath 相同的 circleHoldArcLengthM 度量,保证一致性。 - * @param centerCartesian - 盘旋中心 + * 根据盘旋总距离反算圆半径(圆心在垂线上、垂足=hold)。 + * 进入点与出口点可不同,弧长由切线入口到切线出口,二分搜索 R。 * @param prevPointCartesian - 上一航点 + * @param holdPointCartesian - 盘旋航点位置(垂足) * @param nextPointCartesian - 下一航点 * @param clockwise - 是否顺时针 * @param totalHoldDistM - 盘旋段总飞行距离(米) * @returns 半径(米),若无解返回 null */ - computeHoldRadiusForDuration(centerCartesian, prevPointCartesian, nextPointCartesian, clockwise, totalHoldDistM) { + computeHoldRadiusForDuration(prevPointCartesian, holdPointCartesian, nextPointCartesian, clockwise, totalHoldDistM) { if (!totalHoldDistM || totalHoldDistM <= 0) return null; - const dToNext = Cesium.Cartesian3.distance(centerCartesian, nextPointCartesian); - const dFromPrev = Cesium.Cartesian3.distance(centerCartesian, prevPointCartesian); - if (dToNext < 1e-6) return null; - const Rmax = Math.min(dToNext * 0.999, Math.max(1, dFromPrev - 1)); + const dToHold = Cesium.Cartesian3.distance(prevPointCartesian, holdPointCartesian || prevPointCartesian); + if (dToHold < 1e-6) return null; + const Rmax = Math.min(dToHold * 0.999, Math.max(1, totalHoldDistM / (2 * Math.PI) * 2)); if (Rmax < 1) return null; const toleranceM = 0.1; let Rlo = 10; let Rhi = Rmax; for (let iter = 0; iter < 80; iter++) { const R = (Rlo + Rhi) / 2; - const len = this.circleHoldArcLengthM(centerCartesian, prevPointCartesian, nextPointCartesian || centerCartesian, R, clockwise); + const len = this.circleHoldArcLengthMFromPrevNext(prevPointCartesian, holdPointCartesian, nextPointCartesian, R, clockwise); if (!Number.isFinite(len) || len <= 0) return null; if (Math.abs(len - totalHoldDistM) < toleranceM) return Math.min(Rmax, R); if (len < totalHoldDistM) Rlo = R; else Rhi = R; @@ -3414,20 +3751,24 @@ export default { return Math.sqrt(horizontal * horizontal + dalt * dalt); }, - /** 圆形盘旋总弧长(切线入口 → 整圆一圈 → 弧至切线出口),与 getRoutePath 构建的路径同度量(弦长累加) */ + /** 圆形盘旋总弧长(圆心在垂线上、垂足=hold):从切线入口沿圆到切线出口的弧长 */ + circleHoldArcLengthMFromPrevNext(prevPointCartesian, holdPointCartesian, nextPointCartesian, radiusMeters, clockwise) { + const center = this.getHoldCenterFromPrevNext(prevPointCartesian, holdPointCartesian, radiusMeters, clockwise); + const entry = this.getCircleTangentEntryPoint(center, prevPointCartesian, radiusMeters, clockwise); + const exit = this.getCircleTangentExitPoint(center, nextPointCartesian || holdPointCartesian, radiusMeters, clockwise); + const pts = this.getCircleArcEntryToExit(center, radiusMeters, entry, exit, clockwise, 64); + if (!pts || pts.length < 2) return 0; + let len = 0; + for (let i = 1; i < pts.length; i++) len += this.segmentDistanceFromCartesian(pts[i - 1], pts[i]); + return len; + }, + + /** @deprecated 旧模型(圆心=航点),保留兼容。新模型请用 circleHoldArcLengthMFromPrevNext */ circleHoldArcLengthM(centerCartesian, prevPointCartesian, nextPointCartesian, radiusMeters, clockwise) { - const CIRCLE_POINTS = 64; - const ARC_POINTS = 24; const entry = this.getCircleTangentEntryPoint(centerCartesian, prevPointCartesian, radiusMeters, clockwise); const exit = this.getCircleTangentExitPoint(centerCartesian, nextPointCartesian || centerCartesian, radiusMeters, clockwise); - const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian); - const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); - const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); - const toEntry = Cesium.Cartesian3.subtract(entry, centerCartesian, new Cesium.Cartesian3()); - const entryAngle = Math.atan2(Cesium.Cartesian3.dot(toEntry, east), Cesium.Cartesian3.dot(toEntry, north)); - const circlePts = this.getCircleFullCircle(centerCartesian, radiusMeters, entryAngle, clockwise, CIRCLE_POINTS); - const arcToExit = this.getCircleArcEntryToExit(centerCartesian, radiusMeters, entry, exit, clockwise, ARC_POINTS); - const pts = circlePts && circlePts.length ? (arcToExit && arcToExit.length > 1 ? [...circlePts, ...arcToExit.slice(1)] : circlePts) : [entry, exit]; + const pts = this.getCircleArcEntryToExit(centerCartesian, radiusMeters, entry, exit, clockwise, 64); + if (!pts || pts.length < 2) return 0; let len = 0; for (let i = 1; i < pts.length; i++) len += this.segmentDistanceFromCartesian(pts[i - 1], pts[i]); return len; @@ -3511,8 +3852,10 @@ export default { const p2 = pointAt(angle2); const exitDir1 = Cesium.Cartesian3.subtract(nextPointCartesian, p1, new Cesium.Cartesian3()); const exitDir2 = Cesium.Cartesian3.subtract(nextPointCartesian, p2, new Cesium.Cartesian3()); - Cesium.Cartesian3.normalize(exitDir1, exitDir1); - Cesium.Cartesian3.normalize(exitDir2, exitDir2); + const mag1 = Cesium.Cartesian3.magnitude(exitDir1); + const mag2 = Cesium.Cartesian3.magnitude(exitDir2); + if (mag1 >= 1e-9) Cesium.Cartesian3.normalize(exitDir1, exitDir1); + if (mag2 >= 1e-9) Cesium.Cartesian3.normalize(exitDir2, exitDir2); const tangentAt = (angle) => { const tn = clockwise ? -Math.sin(angle) : Math.sin(angle); const te = clockwise ? Math.cos(angle) : -Math.cos(angle); @@ -3585,8 +3928,10 @@ export default { const p2 = toCart(t2); const approach1 = Cesium.Cartesian3.subtract(p1, prevPointCartesian, new Cesium.Cartesian3()); const approach2 = Cesium.Cartesian3.subtract(p2, prevPointCartesian, new Cesium.Cartesian3()); - Cesium.Cartesian3.normalize(approach1, approach1); - Cesium.Cartesian3.normalize(approach2, approach2); + const mag1 = Cesium.Cartesian3.magnitude(approach1); + const mag2 = Cesium.Cartesian3.magnitude(approach2); + if (mag1 >= 1e-9) Cesium.Cartesian3.normalize(approach1, approach1); + if (mag2 >= 1e-9) Cesium.Cartesian3.normalize(approach2, approach2); const tangent1 = this.ellipseTangentAtParam(centerCartesian, semiMajorM, semiMinorM, headingRad, t1, clockwise); const tangent2 = this.ellipseTangentAtParam(centerCartesian, semiMajorM, semiMinorM, headingRad, t2, clockwise); const dot1 = Cesium.Cartesian3.dot(approach1, tangent1); @@ -3660,8 +4005,10 @@ export default { const p2 = toCart(t2); const exitDir1 = Cesium.Cartesian3.subtract(nextPointCartesian, p1, new Cesium.Cartesian3()); const exitDir2 = Cesium.Cartesian3.subtract(nextPointCartesian, p2, new Cesium.Cartesian3()); - Cesium.Cartesian3.normalize(exitDir1, exitDir1); - Cesium.Cartesian3.normalize(exitDir2, exitDir2); + const mag1 = Cesium.Cartesian3.magnitude(exitDir1); + const mag2 = Cesium.Cartesian3.magnitude(exitDir2); + if (mag1 >= 1e-9) Cesium.Cartesian3.normalize(exitDir1, exitDir1); + if (mag2 >= 1e-9) Cesium.Cartesian3.normalize(exitDir2, exitDir2); const tangent1 = this.ellipseTangentAtParam(centerCartesian, semiMajorM, semiMinorM, headingRad, t1, clockwise); const tangent2 = this.ellipseTangentAtParam(centerCartesian, semiMajorM, semiMinorM, headingRad, t2, clockwise); const dot1 = Cesium.Cartesian3.dot(exitDir1, tangent1); @@ -3709,23 +4056,23 @@ export default { const arcStartIdx = path.length; let holdPositions; if (useCircle) { - const overrideRadius = holdRadiusByLegIndex[legIndex]; - const radius = (overrideRadius != null && Number.isFinite(overrideRadius)) - ? overrideRadius - : (params && params.radius != null ? params.radius : 500); - const entry = this.getCircleTangentEntryPoint(currPos, lastPos, radius, clockwise); - const exit = this.getCircleTangentExitPoint(currPos, nextPos || currPos, radius, clockwise); - const enu = Cesium.Transforms.eastNorthUpToFixedFrame(currPos); - const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); - const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); - const toP = Cesium.Cartesian3.subtract(entry, currPos, new Cesium.Cartesian3()); - const entryAngle = Math.atan2(Cesium.Cartesian3.dot(toP, east), Cesium.Cartesian3.dot(toP, north)); - const circlePts = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 64); - const arcToExit = this.getCircleArcEntryToExit(currPos, radius, entry, exit, clockwise, 24); - const arcPoints = circlePts && circlePts.length - ? (arcToExit && arcToExit.length > 1 ? [...circlePts, ...arcToExit.slice(1)] : circlePts) - : (arcToExit && arcToExit.length ? arcToExit : [entry, exit]); - holdPositions = arcPoints && arcPoints.length ? arcPoints : [entry, exit]; + const turnRadius = this.getWaypointRadius({ ...wp, turnAngle: this.getEffectiveTurnAngle(wp, i, waypoints.length) }) || 500; + const radius = (holdRadiusByLegIndex[legIndex] != null && Number.isFinite(holdRadiusByLegIndex[legIndex])) + ? holdRadiusByLegIndex[legIndex] + : turnRadius; + const center = this.getHoldCenterFromPrevNext(lastPos, currPos, radius, clockwise); + const entry = this.getCircleTangentEntryPoint(center, lastPos, radius, clockwise); + const exit = this.getCircleTangentExitPoint(center, nextPos || currPos, radius, clockwise); + // 圆形盘旋:先绕整圈,再从出口切线飞出(与椭圆盘旋逻辑一致) + const enuPath = Cesium.Transforms.eastNorthUpToFixedFrame(center); + const eastPath = Cesium.Matrix4.getColumn(enuPath, 0, new Cesium.Cartesian3()); + const northPath = Cesium.Matrix4.getColumn(enuPath, 1, new Cesium.Cartesian3()); + const toEntryPath = Cesium.Cartesian3.subtract(entry, center, new Cesium.Cartesian3()); + const startAnglePath = Math.atan2(Cesium.Cartesian3.dot(toEntryPath, eastPath), Cesium.Cartesian3.dot(toEntryPath, northPath)); + const fullCirclePath = this.getCircleFullCircle(center, radius, startAnglePath, clockwise, 64); + const arcToExitPath = this.getCircleArcEntryToExit(center, radius, entry, exit, clockwise, 32); + holdPositions = [entry, ...(fullCirclePath || []).slice(1), ...(arcToExitPath || []).slice(1)]; + if (!holdPositions || holdPositions.length < 2) holdPositions = [Cesium.Cartesian3.clone(entry), Cesium.Cartesian3.clone(exit)]; lastPos = exit; } else { const edgeLengthM = Math.max(1000, params && params.edgeLength != null ? params.edgeLength : this.DEFAULT_RACE_TRACK_EDGE_LENGTH_M); @@ -3736,15 +4083,16 @@ export default { nextPos, params && params.headingDeg != null ? params.headingDeg : 0 ); - holdPositions = this.getRaceTrackLoopPositions(currPos, directionRad, edgeLengthM, arcRadiusM, clockwise, 24); - if (nextPos) { - lastPos = this.getRaceTrackTangentExitPoint(currPos, nextPos, directionRad, edgeLengthM, arcRadiusM, clockwise); - } else { - lastPos = holdPositions.length ? holdPositions[holdPositions.length - 1] : currPos; - } + holdPositions = this.buildRaceTrackWithEntryExit(currPos, lastPos, nextPos, directionRad, edgeLengthM, arcRadiusM, clockwise, 24); + lastPos = holdPositions.length ? holdPositions[holdPositions.length - 1] : currPos; } + const holdLoopEndOffset = holdPositions._loopEndIndex != null ? holdPositions._loopEndIndex : null; for (let k = 0; k < holdPositions.length; k++) path.push(toLngLatAlt(holdPositions[k])); - holdArcRanges[i - 1] = { start: arcStartIdx, end: path.length - 1 }; + holdArcRanges[i - 1] = { + start: arcStartIdx, + end: path.length - 1, + loopEndIndex: holdLoopEndOffset != null ? arcStartIdx + holdLoopEndOffset : null + }; segmentEndIndices[i - 1] = path.length - 1; prevWasHold = true; } else { @@ -3754,9 +4102,12 @@ export default { const holdParams = this.parseHoldParams(nextWp); const nextHoldIsCircle = (nextWp.pointType || nextWp.point_type) === 'hold_circle'; if (nextHoldIsCircle) { - const holdClock = holdParams && holdParams.clockwise !== false; - const holdR = (holdRadiusByLegIndex && holdRadiusByLegIndex[i] != null) ? holdRadiusByLegIndex[i] : (holdParams && holdParams.radius != null ? holdParams.radius : 500); - nextLogical = this.getCircleTangentEntryPoint(originalPositions[i + 1], lastPos, holdR, holdClock); + const holdPos = originalPositions[i + 1]; + const computedNextR2 = this.getWaypointRadius({ ...nextWp, turnAngle: this.getEffectiveTurnAngle(nextWp, i + 1, waypoints.length) }); + const r = (computedNextR2 > 0) ? computedNextR2 : 500; + const clock = holdParams && holdParams.clockwise !== false; + const center = this.getHoldCenterFromPrevNext(currPos, holdPos, r, clock); + nextLogical = this.getCircleTangentEntryPoint(center, currPos, r, clock); } else { const nextNext = i + 2 < waypoints.length ? originalPositions[i + 2] : null; const nextDir = this.getRaceTrackDirectionRad(originalPositions[i + 1], currPos, nextNext, holdParams && holdParams.headingDeg != null ? holdParams.headingDeg : 0); @@ -3767,17 +4118,19 @@ export default { } const effectiveAngle = this.getEffectiveTurnAngle(wp, i, waypoints.length); const radius = this.getWaypointRadius({ ...wp, turnAngle: effectiveAngle }); - const nextIsHold = nextPos && i + 1 < waypoints.length && this.isHoldWaypoint(waypoints[i + 1]); if (i < waypoints.length - 1 && nextLogical) { if (prevWasHold) { - // 上一航点为盘旋时:航线严格为「盘旋弧上切点→下一航点」的切线,保证平台移动流畅 + // 上一航点为盘旋时:先把盘旋出口加入路径,再按转弯半径处理 path.push(toLngLatAlt(lastPos)); - path.push(toLngLatAlt(nextLogical)); - lastPos = nextLogical; + if (radius > 0) { + const arcPoints = this.computeArcPositions(lastPos, currPos, nextLogical, radius); + arcPoints.forEach(p => path.push(toLngLatAlt(p))); + lastPos = arcPoints[arcPoints.length - 1]; + } else { + path.push(toLngLatAlt(currPos)); + lastPos = currPos; + } prevWasHold = false; - } else if (nextIsHold) { - path.push(toLngLatAlt(nextLogical)); - lastPos = nextLogical; } else if (radius > 0) { const arcPoints = this.computeArcPositions(lastPos, currPos, nextLogical, radius); arcPoints.forEach(p => path.push(toLngLatAlt(p))); @@ -3810,9 +4163,12 @@ export default { for (let i = entityList.length - 1; i >= 0; i--) { const entity = entityList[i]; let shouldRemove = false; - // 平台图标与标牌实体:通过 id 匹配 + // 平台图标与标牌实体、航线弧线/盘旋:通过 id 匹配 + const idStr = typeof entity.id === 'string' ? entity.id : ''; if (entity.id === `route-platform-${routeId}` || entity.id === `route-platform-label-${routeId}` || entity.id === `detection-zone-${routeId}` || entity.id === `power-zone-${routeId}`) { shouldRemove = true; + } else if (idStr.startsWith(`hold-line-${routeId}-`) || idStr.startsWith(`arc-line-${routeId}-`)) { + shouldRemove = true; } else if (entity.properties && entity.properties.routeId) { const id = entity.properties.routeId.getValue && entity.properties.routeId.getValue(); if (id === routeId) shouldRemove = true; @@ -4829,13 +5185,13 @@ export default { } } } - // 盘旋弧线(hold-line-routeId-i)右键视为在该盘旋航点处,waypointIndex = i+1 + // 盘旋弧线(hold-line-routeId-i)右键视为在该盘旋航点处,i 为航点数组下标,waypointIndex = i if (!entityData && idStr && idStr.startsWith('hold-line-')) { const parts = idStr.split('-'); if (parts.length >= 4) { const routeId = parts[2]; const segIdx = parseInt(parts[3], 10); - if (!isNaN(segIdx)) entityData = { type: 'routeWaypoint', routeId, waypointIndex: segIdx + 1, fromHold: true }; + if (!isNaN(segIdx)) entityData = { type: 'routeWaypoint', routeId, waypointIndex: segIdx, fromHold: true }; } } } diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index 213d83b..9626a46 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -4113,9 +4113,9 @@ export default { /** * 按速度与计划时间构建航线时间轴:含飞行段、盘旋段与“提前到达则等待”的等待段。 * pathData 可选:{ path, segmentEndIndices, holdArcRanges },由 getRoutePathWithSegmentIndices 提供,用于输出 hold 段。 - * holdRadiusByLegIndex 可选:{ [legIndex]: number },为盘旋段指定半径(用于推演时落点精准在切点)。 + * 圆形盘旋半径由速度+坡度公式固定计算,盘旋时间靠多转圈数解决,不反算半径。 */ - buildRouteTimeline(waypoints, globalMin, globalMax, pathData, holdRadiusByLegIndex) { + buildRouteTimeline(waypoints, globalMin, globalMax, pathData) { const warnings = []; if (!waypoints || waypoints.length === 0) return { segments: [], warnings }; const points = waypoints.map((wp, idx) => ({ @@ -4153,7 +4153,12 @@ export default { const path = pathData && pathData.path; const segmentEndIndices = pathData && pathData.segmentEndIndices; const holdArcRanges = pathData && pathData.holdArcRanges || {}; + let skipNextLeg = false; for (let i = 0; i < points.length - 1; i++) { + if (skipNextLeg) { + skipNextLeg = false; + continue; + } if (this.isHoldWaypoint(waypoints[i + 1]) && path && segmentEndIndices && holdArcRanges[i]) { const range = holdArcRanges[i]; const startIdx = i === 0 ? 0 : segmentEndIndices[i - 1] + 1; @@ -4183,25 +4188,35 @@ export default { } 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, 200); k++) { - if (this.segmentDistance(holdPathSlice[0], holdPathSlice[k]) < 80) { loopEndIdx = k; break; } + let loopEndIdx; + if (range.loopEndIndex != null) { + loopEndIdx = range.loopEndIndex - range.start; + } else { + const minSearchIdx = Math.max(2, Math.floor(holdPathSlice.length * 0.33)); + loopEndIdx = holdPathSlice.length - 1; + for (let k = minSearchIdx; k < holdPathSlice.length; k++) { + if (this.segmentDistance(holdPathSlice[0], holdPathSlice[k]) < 80) { loopEndIdx = k; break; } + } } - const holdClosedLoopPath = holdPathSlice.slice(0, loopEndIdx + 1); + if (loopEndIdx < 1) loopEndIdx = 1; + if (loopEndIdx >= holdPathSlice.length) loopEndIdx = holdPathSlice.length - 1; + const holdClosedLoopRaw = holdPathSlice.slice(0, loopEndIdx + 1); + const holdClosedLoopPath = holdClosedLoopRaw.length >= 2 + ? [...holdClosedLoopRaw.slice(0, -1), { ...holdClosedLoopRaw[0] }] + : holdClosedLoopRaw; const holdLoopLength = this.pathSliceDistance(holdClosedLoopPath) || 1; // 出口必须在整条盘旋路径上找,不能只在“整圈”段内找,否则会误把圈上某点当出口导致飞半圈就停或折回 - let exitIdxOnLoop = holdPathSlice.length - 1; - let minD = 1e9; - for (let k = 0; k < holdPathSlice.length; 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 holdEntryToExitRaw = holdPathSlice.slice(loopEndIdx); + const holdEntryToExitSlice = holdEntryToExitRaw.length >= 2 + ? [{ ...holdClosedLoopPath[0] }, ...holdEntryToExitRaw.slice(1)] + : holdEntryToExitRaw; + const holdExitDistanceOnLoop = this.pathSliceDistance(holdEntryToExitSlice); const holdSpeedKmh = points[i + 1].speed || 800; const HOLD_SPEED_KMH = 800; const speedMpMin = (HOLD_SPEED_KMH * 1000) / 60; const requiredDistAtK10 = (holdEndTime - arrivalEntry) * speedMpMin; - let n = Math.ceil((requiredDistAtK10 - holdExitDistanceOnLoop) / holdLoopLength); + const rawLoops = (requiredDistAtK10 - holdExitDistanceOnLoop) / holdLoopLength; + let n = Math.ceil(rawLoops - 1e-9); if (n < 0 || !Number.isFinite(n)) n = 0; const segmentEndTime = arrivalEntry + (holdExitDistanceOnLoop + n * holdLoopLength) / speedMpMin; if (segmentEndTime > holdEndTime) { @@ -4222,22 +4237,22 @@ export default { const distExitToNext = this.pathSliceDistance(toNextSlice); const travelExitMin = (distExitToNext / 1000) * (60 / holdSpeedKmh); const arrivalNext = segmentEndTime + travelExitMin; - effectiveTime[i + 1] = holdEndTime; + effectiveTime[i + 1] = segmentEndTime; 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 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 overrideR = holdRadiusByLegIndex && holdRadiusByLegIndex[i] != null ? holdRadiusByLegIndex[i] : null; - const holdRadius = (overrideR != null && Number.isFinite(overrideR)) ? overrideR : (holdParams && holdParams.radius != null ? holdParams.radius : null); + const computedR = this.$refs.cesiumMap ? this.$refs.cesiumMap.getWaypointRadius(holdWp) : null; + const holdRadius = (computedR != null && computedR > 0) ? computedR : 500; const holdClockwise = holdParams && holdParams.clockwise !== false; const holdCircumference = holdRadius != null ? 2 * Math.PI * holdRadius : null; const holdEntryAngle = holdCenter && entryPos && holdRadius != null ? this.angleFromCenterToPoint(holdCenter.lng, holdCenter.lat, entryPos.lng, entryPos.lat) : null; segments.push({ startTime: effectiveTime[i], endTime: arrivalEntry, startPos: posCur, endPos: entryPos, type: 'fly', legIndex: i, pathSlice: toEntrySlice, speedKmh: speedKmhForLeg }); - const holdEntryToExitPath = holdClosedLoopPath.slice(0, exitIdxOnLoop + 1); + const holdEntryToExitPath = holdEntryToExitSlice; segments.push({ startTime: arrivalEntry, endTime: segmentEndTime, @@ -4259,8 +4274,18 @@ export default { holdClockwise, holdEntryAngle }); - segments.push({ startTime: segmentEndTime, endTime: arrivalNext, startPos: exitPos, endPos: toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos, type: 'fly', legIndex: i, pathSlice: toNextSlice, speedKmh: holdSpeedKmh }); - continue; // 不 i++,让下次迭代处理下一航段(含连续盘旋点如 WP2→WP3 均为盘旋) + // 出口→下一航点的 fly 段 + const exitEndPos = toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos; + // 如果下一个航点(WP_{i+2})也是盘旋,不创建 fly 段(让下一次循环处理), + // 只更新 effectiveTime 使下一次循环的起始时间正确 + if (i + 2 < points.length && this.isHoldWaypoint(waypoints[i + 2])) { + segments.push({ startTime: segmentEndTime, endTime: arrivalNext, startPos: exitPos, endPos: exitEndPos, type: 'fly', legIndex: i + 1, pathSlice: toNextSlice, speedKmh: holdSpeedKmh }); + } else { + segments.push({ startTime: segmentEndTime, endTime: arrivalNext, startPos: exitPos, endPos: exitEndPos, type: 'fly', legIndex: i + 1, pathSlice: toNextSlice, speedKmh: holdSpeedKmh }); + // 下一航点不是盘旋,fly 段已覆盖 leg i+1,跳过 + skipNextLeg = true; + } + continue; } const dist = this.segmentDistance(points[i], points[i + 1]); const speedKmh = points[i].speed || 800; @@ -4287,7 +4312,15 @@ export default { effectiveTime[i + 1] = Math.max(actualArrival, scheduled); const posCur = { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt }; const posNext = { lng: points[i + 1].lng, lat: points[i + 1].lat, alt: points[i + 1].alt }; - segments.push({ startTime: effectiveTime[i], endTime: actualArrival, startPos: posCur, endPos: posNext, type: 'fly', legIndex: i, speedKmh: speedKmh }); + let flyPathSlice = null; + if (path && segmentEndIndices) { + const startIdx = i === 0 ? 0 : (segmentEndIndices[i - 1] != null ? segmentEndIndices[i - 1] : 0); + const endIdx = segmentEndIndices[i]; + if (endIdx != null && endIdx >= startIdx) { + flyPathSlice = path.slice(startIdx, endIdx + 1); + } + } + segments.push({ startTime: effectiveTime[i], endTime: actualArrival, startPos: posCur, endPos: posNext, type: 'fly', legIndex: i, speedKmh: speedKmh, pathSlice: flyPathSlice }); if (actualArrival < effectiveTime[i + 1]) { segments.push({ startTime: actualArrival, endTime: effectiveTime[i + 1], startPos: posNext, endPos: posNext, type: 'wait', legIndex: i }); } @@ -4402,24 +4435,27 @@ export default { const cesiumMap = this.$refs.cesiumMap; let pathData = null; if (cesiumMap && cesiumMap.getRoutePathWithSegmentIndices) { - const cachedRadii = (routeId != null && cesiumMap._routeHoldRadiiByRoute && cesiumMap._routeHoldRadiiByRoute[routeId]) ? cesiumMap._routeHoldRadiiByRoute[routeId] : {}; const cachedEllipse = (routeId != null && cesiumMap._routeHoldEllipseParamsByRoute && cesiumMap._routeHoldEllipseParamsByRoute[routeId]) ? cesiumMap._routeHoldEllipseParamsByRoute[routeId] : {}; - const opts = (Object.keys(cachedRadii).length > 0 || Object.keys(cachedEllipse).length > 0) ? { holdRadiusByLegIndex: cachedRadii, holdEllipseParamsByLegIndex: cachedEllipse } : {}; + const opts = Object.keys(cachedEllipse).length > 0 ? { holdEllipseParamsByLegIndex: cachedEllipse } : {}; const ret = cesiumMap.getRoutePathWithSegmentIndices(waypoints, opts); if (ret.path && ret.path.length > 0 && ret.segmentEndIndices && ret.segmentEndIndices.length > 0) { pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} }; } } let { segments, warnings, earlyArrivalLegs } = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData); - const holdRadiusByLegIndex = {}; + // 圆形盘旋:半径固定由速度+坡度公式计算,盘旋时间靠多转圈数解决,不反算半径。 + // 椭圆/跑道形盘旋:通过反算椭圆参数(semiMajor/semiMinor)来匹配盘旋时间。 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 holdEndTime = s.holdEndTime != null ? s.holdEndTime : this.waypointStartTimeToMinutesDecimal(waypoints[i + 1]?.startTime); const holdWp = waypoints[i + 1]; + if (!holdWp) continue; + const isHoldEllipse = (holdWp.pointType || holdWp.point_type) === 'hold_ellipse'; + if (!isHoldEllipse || !cesiumMap.computeEllipseParamsForDuration) continue; + const holdEndTime = s.holdEndTime != null ? s.holdEndTime : this.waypointStartTimeToMinutesDecimal(holdWp?.startTime); const segTarget = holdWp && (holdWp.segmentTargetMinutes ?? holdWp.displayStyle?.segmentTargetMinutes); const arrivalAtHold = (holdWp && holdWp.segmentMode === 'fixed_time' && segTarget != null && segTarget !== '') ? Number(segTarget) : s.startTime; @@ -4428,123 +4464,36 @@ export default { const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000; const prevWp = waypoints[i]; const nextWp = (i + 2) < waypoints.length ? waypoints[i + 2] : holdWp; - if (!prevWp || !holdWp) continue; + if (!prevWp) 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 isHoldEllipse = waypoints[i + 1] && (waypoints[i + 1].pointType || waypoints[i + 1].point_type) === 'hold_ellipse'; - const isEllipse = isHoldEllipse || s.holdRadius == null; - if (isEllipse && !isHoldEllipse && 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 - }; - } - } else if (!isEllipse && cesiumMap.computeHoldRadiusForDuration) { - let R = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, clockwise, totalHoldDistM); - if (R == null || !Number.isFinite(R)) { - R = totalHoldDistM / (2 * Math.PI); - } - if (R != null && Number.isFinite(R) && R > 0) { - holdRadiusByLegIndex[i] = R; - } + 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, s.holdClockwise !== false, totalHoldDistM, headingDeg, a0, b0); + if (out && out.semiMajor != null && out.semiMinor != null) { + holdEllipseParamsByLegIndex[i] = { semiMajor: out.semiMajor, semiMinor: out.semiMinor, headingDeg }; } } - 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 < 4; 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 holdWpCircle = waypoints[i + 1]; - const holdEndTimeCircle = s.holdEndTime != null ? s.holdEndTime : this.waypointStartTimeToMinutesDecimal(holdWpCircle?.startTime); - const segTargetCircle = holdWpCircle && (holdWpCircle.segmentTargetMinutes ?? holdWpCircle.displayStyle?.segmentTargetMinutes); - const arrivalAtHoldCircle = (holdWpCircle && holdWpCircle.segmentMode === 'fixed_time' && segTargetCircle != null && segTargetCircle !== '') - ? Number(segTargetCircle) : s.startTime; - const holdDurationMin = Math.max(0, holdEndTimeCircle - arrivalAtHoldCircle); - const speedKmh = s.speedKmh != null ? s.speedKmh : (Number(holdWpCircle?.speed) || 800); - const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000; - const prevWp = waypoints[i]; - const holdWp = holdWpCircle; - 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; - let Rnew = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, s.holdClockwise !== false, totalHoldDistM); - if (Rnew == null || !Number.isFinite(Rnew)) Rnew = totalHoldDistM / (2 * Math.PI); - if (Rnew != null && Number.isFinite(Rnew) && Rnew > 0) { - nextRadii[i] = Rnew; - if (holdRadiusByLegIndex[i] == null || Math.abs(nextRadii[i] - holdRadiusByLegIndex[i]) > 1) changed = true; - } - } - 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]; - if ((holdWp && (holdWp.pointType || holdWp.point_type) === 'hold_ellipse')) continue; - const holdParams = this.parseHoldParams(holdWp); - const holdEndTime = s.holdEndTime != null ? s.holdEndTime : this.waypointStartTimeToMinutesDecimal(holdWp?.startTime); - const segTargetEllipse = holdWp && (holdWp.segmentTargetMinutes ?? holdWp.displayStyle?.segmentTargetMinutes); - const arrivalAtHold = (holdWp && holdWp.segmentMode === 'fixed_time' && segTargetEllipse != null && segTargetEllipse !== '') - ? Number(segTargetEllipse) : s.startTime; - const holdDurationMin = Math.max(0, holdEndTime - arrivalAtHold); - const speedKmh = s.speedKmh != null ? s.speedKmh : (Number(holdWp?.speed) || 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 smj = out.semiMajor; - const smn = out.semiMinor; - const old = holdEllipseParamsByLegIndex[i]; - if (!old || Math.abs(smj - old.semiMajor) > 1) changed = true; - holdEllipseParamsByLegIndex[i] = { semiMajor: smj, semiMinor: smn, headingDeg }; - } - } - } - if (!changed || iter === 3) break; + if (hasEllipse) { + const ret2 = cesiumMap.getRoutePathWithSegmentIndices(waypoints, { holdEllipseParamsByLegIndex }); + if (ret2.path && ret2.path.length > 0 && ret2.segmentEndIndices && ret2.segmentEndIndices.length > 0) { + pathData = { path: ret2.path, segmentEndIndices: ret2.segmentEndIndices, holdArcRanges: ret2.holdArcRanges || {} }; + const out2 = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData); + segments = out2.segments; } - 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); + if (routeId != null && cesiumMap.setRouteHoldEllipseParams) { + cesiumMap.setRouteHoldEllipseParams(routeId, holdEllipseParamsByLegIndex); } - } else if (routeId != null) { - if (cesiumMap.setRouteHoldRadii) cesiumMap.setRouteHoldRadii(routeId, {}); - if (cesiumMap.setRouteHoldEllipseParams) cesiumMap.setRouteHoldEllipseParams(routeId, {}); + } else if (routeId != null && cesiumMap.setRouteHoldEllipseParams) { + cesiumMap.setRouteHoldEllipseParams(routeId, {}); + } + // 圆形盘旋不再使用反算半径,清空旧缓存 + if (routeId != null && cesiumMap.setRouteHoldRadii) { + cesiumMap.setRouteHoldRadii(routeId, {}); } } const path = pathData ? pathData.path : null;