using System; using MyParking.Shared; namespace MultiWheelC.Trajectory { /// /// 将实际车体中心位姿投影到二维离散参考轨迹,并支持按上次进度限制搜索范围。 /// public static class TrajectoryProjector { private const double DistanceTieToleranceSquaredMeters = 1e-12; /// /// 在整条轨迹上查找距离实际车体中心最近的线段投影结果。 /// public static TrajectoryProjection Project( Trajectory2D trajectory, Pose2D vehiclePoseInWorld) { ValidateProjectionInput( trajectory, vehiclePoseInWorld, nameof(vehiclePoseInWorld)); return ProjectRange( trajectory, vehiclePoseInWorld, firstSegmentStartIndex: 0, lastSegmentStartIndex: trajectory.Count - 2, preferredArcLengthMeters: null); } /// /// 以上次投影弧长为中心,仅在指定前后物理距离窗口内查找最近线段。 /// public static TrajectoryProjection Project( Trajectory2D trajectory, Pose2D vehiclePoseInWorld, double previousArcLengthMeters, double maximumBackwardSearchDistanceMeters, double maximumForwardSearchDistanceMeters) { ValidateProjectionInput( trajectory, vehiclePoseInWorld, nameof(vehiclePoseInWorld)); NumericGuard.EnsureFiniteNonNegative( previousArcLengthMeters, nameof(previousArcLengthMeters)); NumericGuard.EnsureFiniteNonNegative( maximumBackwardSearchDistanceMeters, nameof(maximumBackwardSearchDistanceMeters)); NumericGuard.EnsureFinitePositive( maximumForwardSearchDistanceMeters, nameof(maximumForwardSearchDistanceMeters)); if (previousArcLengthMeters > trajectory.TotalLengthMeters) { throw new ArgumentOutOfRangeException( nameof(previousArcLengthMeters), "上次投影弧长不能超过轨迹总长度。"); } var searchStartArcLengthMeters = Math.Max( 0.0, previousArcLengthMeters - maximumBackwardSearchDistanceMeters); var searchEndArcLengthMeters = Math.Min( trajectory.TotalLengthMeters, previousArcLengthMeters + maximumForwardSearchDistanceMeters); var firstSegmentStartIndex = trajectory.FindSegmentStartIndex( searchStartArcLengthMeters); var lastSegmentStartIndex = trajectory.FindSegmentStartIndex( searchEndArcLengthMeters); return ProjectRange( trajectory, vehiclePoseInWorld, firstSegmentStartIndex, lastSegmentStartIndex, previousArcLengthMeters); } /// /// 在闭区间线段索引范围内查找最近投影,并在距离并列时优先保持原进度。 /// private static TrajectoryProjection ProjectRange( Trajectory2D trajectory, Pose2D vehiclePoseInWorld, int firstSegmentStartIndex, int lastSegmentStartIndex, double? preferredArcLengthMeters) { var bestSegmentStartIndex = 0; var bestInterpolationRatio = 0.0; var bestDistanceSquared = double.PositiveInfinity; var bestProgressDifferenceMeters = double.PositiveInfinity; for (var segmentStartIndex = firstSegmentStartIndex; segmentStartIndex <= lastSegmentStartIndex; segmentStartIndex++) { var segmentStart = trajectory[segmentStartIndex]; var segmentEnd = trajectory[segmentStartIndex + 1]; var segmentX = segmentEnd.PoseInWorld.XMeters - segmentStart.PoseInWorld.XMeters; var segmentY = segmentEnd.PoseInWorld.YMeters - segmentStart.PoseInWorld.YMeters; var segmentLengthSquared = segmentX * segmentX + segmentY * segmentY; var vehicleFromSegmentStartX = vehiclePoseInWorld.XMeters - segmentStart.PoseInWorld.XMeters; var vehicleFromSegmentStartY = vehiclePoseInWorld.YMeters - segmentStart.PoseInWorld.YMeters; var interpolationRatio = InterpolationMath.Clamp01( (vehicleFromSegmentStartX * segmentX + vehicleFromSegmentStartY * segmentY) / segmentLengthSquared); var projectedX = InterpolationMath.Lerp( segmentStart.PoseInWorld.XMeters, segmentEnd.PoseInWorld.XMeters, interpolationRatio); var projectedY = InterpolationMath.Lerp( segmentStart.PoseInWorld.YMeters, segmentEnd.PoseInWorld.YMeters, interpolationRatio); var projectionErrorX = projectedX - vehiclePoseInWorld.XMeters; var projectionErrorY = projectedY - vehiclePoseInWorld.YMeters; var distanceSquared = projectionErrorX * projectionErrorX + projectionErrorY * projectionErrorY; var progressDifferenceMeters = preferredArcLengthMeters.HasValue ? Math.Abs( InterpolationMath.Lerp( segmentStart.ArcLengthMeters, segmentEnd.ArcLengthMeters, interpolationRatio) - preferredArcLengthMeters.Value) : 0.0; var hasMeaningfullyShorterDistance = distanceSquared < bestDistanceSquared - DistanceTieToleranceSquaredMeters; var hasEquivalentDistanceAndCloserProgress = preferredArcLengthMeters.HasValue && Math.Abs( distanceSquared - bestDistanceSquared) <= DistanceTieToleranceSquaredMeters && progressDifferenceMeters < bestProgressDifferenceMeters; if (!hasMeaningfullyShorterDistance && !hasEquivalentDistanceAndCloserProgress) { continue; } bestSegmentStartIndex = segmentStartIndex; bestInterpolationRatio = interpolationRatio; bestDistanceSquared = distanceSquared; bestProgressDifferenceMeters = progressDifferenceMeters; } return BuildProjection( trajectory, vehiclePoseInWorld, bestSegmentStartIndex, bestInterpolationRatio, bestDistanceSquared); } /// /// 根据最近线段和插值比例生成控制器使用的完整投影结果。 /// private static TrajectoryProjection BuildProjection( Trajectory2D trajectory, Pose2D vehiclePoseInWorld, int segmentStartIndex, double interpolationRatio, double distanceSquared) { var segmentStart = trajectory[segmentStartIndex]; var segmentEnd = trajectory[segmentStartIndex + 1]; var referencePoint = trajectory.InterpolateSegment( segmentStartIndex, interpolationRatio); var segmentX = segmentEnd.PoseInWorld.XMeters - segmentStart.PoseInWorld.XMeters; var segmentY = segmentEnd.PoseInWorld.YMeters - segmentStart.PoseInWorld.YMeters; var segmentLength = Math.Sqrt( segmentX * segmentX + segmentY * segmentY); // 以轨迹线段的前进方向判断左右: // 从车辆指向参考轨迹的向量位于轨迹左侧时为正。 var vehicleToProjectionX = referencePoint.PoseInWorld.XMeters - vehiclePoseInWorld.XMeters; var vehicleToProjectionY = referencePoint.PoseInWorld.YMeters - vehiclePoseInWorld.YMeters; var lateralErrorMeters = (segmentX * vehicleToProjectionY - segmentY * vehicleToProjectionX) / segmentLength; var headingErrorRadians = AngleMath.ShortestDifferenceRadians( referencePoint.PoseInWorld.YawRadians, vehiclePoseInWorld.YawRadians); return new TrajectoryProjection( segmentStartIndex, referencePoint, lateralErrorMeters, headingErrorRadians, Math.Sqrt(distanceSquared), trajectory.GetRemainingDistanceMeters( referencePoint.ArcLengthMeters)); } /// /// 检查轨迹对象和用于投影的实际车体中心位姿。 /// private static void ValidateProjectionInput( Trajectory2D trajectory, Pose2D pose, string parameterName) { if (trajectory == null) { throw new ArgumentNullException( nameof(trajectory)); } NumericGuard.EnsureFinite( pose, parameterName); } } }