using System; using System.Collections.Generic; using System.Collections.ObjectModel; using MultiWheelC.TrajectoryPlanning.CoarsePath; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; /// Resamples a compatible published trajectory onto the current ST knots as soft longitudinal references. public sealed class LongitudinalPreviousTrajectorySeed { private static readonly LongitudinalPreviousTrajectorySeed empty = new LongitudinalPreviousTrajectorySeed( Array.Empty(), Array.Empty()); public LongitudinalPreviousTrajectorySeed(IReadOnlyList pathS, IReadOnlyList progressSpeedMetersPerSecond) { if (pathS == null) throw new ArgumentNullException(nameof(pathS)); if (progressSpeedMetersPerSecond == null) throw new ArgumentNullException(nameof(progressSpeedMetersPerSecond)); if (pathS.Count != progressSpeedMetersPerSecond.Count) throw new ArgumentException("Previous path-S and progress-speed samples must have matching counts."); var copiedPathS = new List(pathS.Count); var copiedSpeed = new List(progressSpeedMetersPerSecond.Count); for (int index = 0; index < pathS.Count; index++) { if (!IsFinite(pathS[index]) || pathS[index] < 0d || !IsFinite(progressSpeedMetersPerSecond[index]) || progressSpeedMetersPerSecond[index] < 0d) { throw new ArgumentOutOfRangeException(nameof(pathS)); } copiedPathS.Add(pathS[index]); copiedSpeed.Add(progressSpeedMetersPerSecond[index]); } PathS = new ReadOnlyCollection(copiedPathS); ProgressSpeedMetersPerSecond = new ReadOnlyCollection(copiedSpeed); } public IReadOnlyList PathS { get; } public IReadOnlyList ProgressSpeedMetersPerSecond { get; } public static LongitudinalPreviousTrajectorySeed Empty { get { return empty; } } private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } } /// Builds monotone PathS and progress-speed soft references from a prior published trajectory. public sealed class LongitudinalPreviousTrajectorySeedBuilder { private const double ProjectionTolerance = 1e-10d; public LongitudinalPreviousTrajectorySeed Build(EmTrajectory previous, LateralPath currentPath, DateTimeOffset newEffectiveAtUtc, LongitudinalKnotSchedule knotSchedule, int segmentIndex, TravelDirection direction) { if (knotSchedule == null) return LongitudinalPreviousTrajectorySeed.Empty; return Build(previous, currentPath, newEffectiveAtUtc, knotSchedule.KnotTimes, segmentIndex, direction); } public LongitudinalPreviousTrajectorySeed Build(EmTrajectory previous, LateralPath currentPath, DateTimeOffset newEffectiveAtUtc, IReadOnlyList newKnotTimes, int segmentIndex, TravelDirection direction) { try { if (!IsCompatible(previous, currentPath, newKnotTimes, segmentIndex, direction)) return LongitudinalPreviousTrajectorySeed.Empty; var pathS = new List(newKnotTimes.Count); var progressSpeed = new List(newKnotTimes.Count); double previousProjectedPathS = double.NegativeInfinity; for (int index = 0; index < newKnotTimes.Count; index++) { DateTimeOffset sampleUtc = newEffectiveAtUtc.AddSeconds(newKnotTimes[index]); double previousTimeSeconds = (sampleUtc - previous.Metadata.EffectiveAtUtc).TotalSeconds; if (!TryInterpolate(previous.Points, previousTimeSeconds, out InterpolatedPreviousSample sample) || !TryProjectMonotonically(currentPath, sample.X, sample.Y, previousProjectedPathS, out double projectedPathS)) { return LongitudinalPreviousTrajectorySeed.Empty; } pathS.Add(projectedPathS); progressSpeed.Add(Math.Abs(sample.SignedSpeedMetersPerSecond)); previousProjectedPathS = projectedPathS; } return new LongitudinalPreviousTrajectorySeed(pathS, progressSpeed); } catch { return LongitudinalPreviousTrajectorySeed.Empty; } } private static bool IsCompatible(EmTrajectory previous, LateralPath currentPath, IReadOnlyList newKnotTimes, int segmentIndex, TravelDirection direction) { if (previous == null || currentPath == null || newKnotTimes == null || segmentIndex < 0 || !Enum.IsDefined(typeof(TravelDirection), direction) || !currentPath.IsIndependentlyValidated || currentPath.Points.Count < 2 || previous.Metadata == null || previous.Points.Count < 2 || previous.Metadata.SegmentIndex != segmentIndex || previous.Metadata.Direction != direction) { return false; } double previousKnotTime = double.NegativeInfinity; for (int index = 0; index < newKnotTimes.Count; index++) { if (!IsFinite(newKnotTimes[index]) || newKnotTimes[index] < 0d || newKnotTimes[index] <= previousKnotTime) { return false; } previousKnotTime = newKnotTimes[index]; } if (newKnotTimes.Count == 0) return false; double previousTime = double.NegativeInfinity; for (int index = 0; index < previous.Points.Count; index++) { EmTrajectoryPoint point = previous.Points[index]; if (point == null || point.Direction != direction || !IsFinite(point.TimeFromStart) || !IsFinite(point.X) || !IsFinite(point.Y) || !IsFinite(point.SignedLongitudinalVelocity) || point.TimeFromStart <= previousTime) { return false; } previousTime = point.TimeFromStart; } double previousPathS = double.NegativeInfinity; for (int index = 0; index < currentPath.Points.Count; index++) { LateralPathPoint point = currentPath.Points[index]; if (point == null || !IsFinite(point.PathS) || !IsFinite(point.X) || !IsFinite(point.Y) || point.PathS <= previousPathS) { return false; } previousPathS = point.PathS; } return true; } private static bool TryInterpolate(IReadOnlyList points, double sampleTimeSeconds, out InterpolatedPreviousSample sample) { sample = default; if (!IsFinite(sampleTimeSeconds) || sampleTimeSeconds < points[0].TimeFromStart - ProjectionTolerance || sampleTimeSeconds > points[points.Count - 1].TimeFromStart + ProjectionTolerance) { return false; } if (sampleTimeSeconds <= points[0].TimeFromStart + ProjectionTolerance) { sample = InterpolatedPreviousSample.From(points[0]); return true; } for (int index = 1; index < points.Count; index++) { EmTrajectoryPoint right = points[index]; if (sampleTimeSeconds <= right.TimeFromStart + ProjectionTolerance) { EmTrajectoryPoint left = points[index - 1]; double ratio = (sampleTimeSeconds - left.TimeFromStart) / (right.TimeFromStart - left.TimeFromStart); ratio = Math.Max(0d, Math.Min(1d, ratio)); sample = new InterpolatedPreviousSample( Linear(left.X, right.X, ratio), Linear(left.Y, right.Y, ratio), Linear(left.SignedLongitudinalVelocity, right.SignedLongitudinalVelocity, ratio)); return true; } } return false; } private static bool TryProjectMonotonically(LateralPath path, double x, double y, double minimumPathS, out double projectedPathS) { projectedPathS = 0d; double bestDistanceSquared = double.PositiveInfinity; bool found = false; for (int index = 1; index < path.Points.Count; index++) { LateralPathPoint left = path.Points[index - 1]; LateralPathPoint right = path.Points[index]; double dx = right.X - left.X; double dy = right.Y - left.Y; double lengthSquared = dx * dx + dy * dy; if (!IsFinite(lengthSquared) || lengthSquared <= ProjectionTolerance) continue; double ratio = ((x - left.X) * dx + (y - left.Y) * dy) / lengthSquared; ratio = Math.Max(0d, Math.Min(1d, ratio)); double candidatePathS = Linear(left.PathS, right.PathS, ratio); if (candidatePathS + ProjectionTolerance < minimumPathS) continue; double projectedX = Linear(left.X, right.X, ratio); double projectedY = Linear(left.Y, right.Y, ratio); double distanceSquared = (x - projectedX) * (x - projectedX) + (y - projectedY) * (y - projectedY); if (!found || distanceSquared < bestDistanceSquared - ProjectionTolerance || (Math.Abs(distanceSquared - bestDistanceSquared) <= ProjectionTolerance && candidatePathS < projectedPathS)) { projectedPathS = candidatePathS; bestDistanceSquared = distanceSquared; found = true; } } if (!found) return false; if (minimumPathS > double.NegativeInfinity) projectedPathS = Math.Max(minimumPathS, projectedPathS); return true; } private static double Linear(double left, double right, double ratio) { return left + (right - left) * ratio; } private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } private readonly struct InterpolatedPreviousSample { public InterpolatedPreviousSample(double x, double y, double signedSpeedMetersPerSecond) { X = x; Y = y; SignedSpeedMetersPerSecond = signedSpeedMetersPerSecond; } public double X { get; } public double Y { get; } public double SignedSpeedMetersPerSecond { get; } public static InterpolatedPreviousSample From(EmTrajectoryPoint point) { return new InterpolatedPreviousSample(point.X, point.Y, point.SignedLongitudinalVelocity); } } }