156 lines
6.5 KiB
C#
156 lines
6.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using MultiWheelC.TrajectoryPlanning.PathSmoothing;
|
|
using MultiWheelC.TrajectoryPlanning.Utils;
|
|
|
|
namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing;
|
|
|
|
/// <summary>按单一方向段的弧长线性插值并保留精确锚点的确定性重采样器。</summary>
|
|
public sealed class ArcLengthResampler
|
|
{
|
|
private const double Tolerance = 1e-10d;
|
|
|
|
/// <summary>以目标间距重采样单一方向段;段首/段末精确锚点始终原样保留。</summary>
|
|
/// <param name="points">同一方向段内按严格递增局部弧长排序的点集合;位置和弧长单位为 m,航向为 rad。</param>
|
|
/// <param name="spacingMeters">目标采样间距,单位 m,必须为有限正数。</param>
|
|
/// <param name="resampled">成功时为只读重采样点集合;失败时为空只读集合。</param>
|
|
/// <param name="reason">失败时描述非法点、退化几何或间距;成功时为空字符串。</param>
|
|
/// <returns>输入属于一个有效同方向区间且可安全插值时为 <see langword="true"/>;否则为 <see langword="false"/>。</returns>
|
|
public bool TryResample(
|
|
IReadOnlyList<SmoothingPoint2D> points,
|
|
double spacingMeters,
|
|
out IReadOnlyList<SmoothingPoint2D> resampled,
|
|
out string reason)
|
|
{
|
|
resampled = EmptyPoints();
|
|
reason = string.Empty;
|
|
if (points == null || points.Count == 0 || !NumericGuard.IsPositiveFinite(spacingMeters))
|
|
{
|
|
reason = "重采样点集或采样间距无效。";
|
|
return false;
|
|
}
|
|
|
|
for (int index = 0; index < points.Count; index++)
|
|
{
|
|
if (!IsValidPoint(points[index]))
|
|
{
|
|
reason = "重采样输入包含非法数值。";
|
|
return false;
|
|
}
|
|
if (index == 0) continue;
|
|
|
|
SmoothingPoint2D previous = points[index - 1];
|
|
SmoothingPoint2D current = points[index];
|
|
if (current.ArcLength <= previous.ArcLength + Tolerance)
|
|
{
|
|
reason = "同一方向段的弧长必须严格增加。";
|
|
return false;
|
|
}
|
|
double distance = Distance(previous, current);
|
|
if (!NumericGuard.IsFinite(distance) || distance <= Tolerance)
|
|
{
|
|
reason = "同一方向段中不允许重复位姿或数值溢出的距离。";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (points.Count == 1)
|
|
{
|
|
resampled = CopyReadOnly(points);
|
|
return true;
|
|
}
|
|
|
|
var output = new List<SmoothingPoint2D> { points[0] };
|
|
double firstArc = points[0].ArcLength;
|
|
double finalArc = points[points.Count - 1].ArcLength;
|
|
int rightIndex = 1;
|
|
for (double targetArc = firstArc + spacingMeters;
|
|
targetArc < finalArc - Tolerance;
|
|
targetArc += spacingMeters)
|
|
{
|
|
while (rightIndex < points.Count - 1 && points[rightIndex].ArcLength < targetArc)
|
|
rightIndex++;
|
|
|
|
SmoothingPoint2D left = points[rightIndex - 1];
|
|
SmoothingPoint2D right = points[rightIndex];
|
|
double ratio = (targetArc - left.ArcLength) / (right.ArcLength - left.ArcLength);
|
|
double unwrappedHeading = left.UnwrappedHeading +
|
|
ratio * (right.UnwrappedHeading - left.UnwrappedHeading);
|
|
output.Add(new SmoothingPoint2D(
|
|
left.X + ratio * (right.X - left.X),
|
|
left.Y + ratio * (right.Y - left.Y),
|
|
targetArc,
|
|
AngleMath.NormalizeRadians(unwrappedHeading),
|
|
unwrappedHeading,
|
|
Math.Min(left.BodyClearance, right.BodyClearance),
|
|
false,
|
|
SmoothedPathPointSource.Interpolated));
|
|
}
|
|
|
|
// Appending the original object, rather than interpolating at the final arc, preserves the exact endpoint.
|
|
output.Add(points[points.Count - 1]);
|
|
resampled = new ReadOnlyCollection<SmoothingPoint2D>(output);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>重采样完整预处理方向段并保留段编号、方向和换向拓扑标记。</summary>
|
|
/// <param name="segment">待重采样的单一前进或倒车方向段。</param>
|
|
/// <param name="spacingMeters">目标局部弧长间距,单位 m。</param>
|
|
/// <param name="resampled">成功时为新不可变预处理方向段;失败时为 <see langword="null"/>。</param>
|
|
/// <param name="reason">失败原因;成功时为空字符串。</param>
|
|
/// <returns>底层点集重采样成功时为 <see langword="true"/>;否则为 <see langword="false"/>。</returns>
|
|
public bool TryResample(
|
|
PreparedDirectionSegment segment,
|
|
double spacingMeters,
|
|
out PreparedDirectionSegment resampled,
|
|
out string reason)
|
|
{
|
|
resampled = null;
|
|
reason = string.Empty;
|
|
if (segment == null)
|
|
{
|
|
reason = "待重采样方向段为空。";
|
|
return false;
|
|
}
|
|
if (!TryResample(segment.Points, spacingMeters, out IReadOnlyList<SmoothingPoint2D> points, out reason))
|
|
return false;
|
|
|
|
resampled = new PreparedDirectionSegment(
|
|
segment.SegmentIndex,
|
|
segment.Direction,
|
|
points,
|
|
segment.StartsAtGearSwitch,
|
|
segment.EndsAtGearSwitch,
|
|
segment.StartVehicleCurvaturePerMeter);
|
|
return true;
|
|
}
|
|
|
|
private static bool IsValidPoint(SmoothingPoint2D point)
|
|
{
|
|
return point != null && NumericGuard.IsFinite(point.X) && NumericGuard.IsFinite(point.Y) &&
|
|
NumericGuard.IsFinite(point.ArcLength) && NumericGuard.IsFinite(point.Heading) &&
|
|
NumericGuard.IsFinite(point.UnwrappedHeading) && NumericGuard.IsFinite(point.BodyClearance) &&
|
|
point.BodyClearance >= 0d;
|
|
}
|
|
|
|
private static double Distance(SmoothingPoint2D left, SmoothingPoint2D right)
|
|
{
|
|
double x = right.X - left.X;
|
|
double y = right.Y - left.Y;
|
|
return Math.Sqrt(x * x + y * y);
|
|
}
|
|
|
|
private static IReadOnlyList<T> CopyReadOnly<T>(IReadOnlyList<T> source)
|
|
{
|
|
var copy = new List<T>(source.Count);
|
|
for (int index = 0; index < source.Count; index++) copy.Add(source[index]);
|
|
return new ReadOnlyCollection<T>(copy);
|
|
}
|
|
|
|
private static IReadOnlyList<SmoothingPoint2D> EmptyPoints()
|
|
{
|
|
return new ReadOnlyCollection<SmoothingPoint2D>(new List<SmoothingPoint2D>());
|
|
}
|
|
}
|