using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using MultiWheelC.TrajectoryPlanning.CoarsePath;
using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing;
using MultiWheelC.TrajectoryPlanning.Utils;
namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.LocalG2;
/// 按硬方向边界生成并合并局部 G2 曲率事件的候选窗口。
internal sealed class LocalG2WindowPlanner
{
private const double MergeToleranceMeters = 1e-9d;
internal bool TryPlan(
PreparedPath originalPath,
IReadOnlyList transitions,
LocalG2OptionsSnapshot options,
out IReadOnlyList regions,
out string reason)
{
regions = Empty();
reason = string.Empty;
if (originalPath == null || transitions == null || options == null)
{
reason = "局部 G2 窗口规划输入无效。";
return false;
}
if (!TryGetSegmentLengths(originalPath, out Dictionary segmentLengths, out reason)) return false;
var ordered = new List(transitions.Count);
for (int index = 0; index < transitions.Count; index++)
{
CurvatureTransition transition = transitions[index];
if (transition == null || !segmentLengths.TryGetValue(transition.SegmentIndex, out double length) ||
!NumericGuard.IsFinite(transition.LocalArcLengthMeters) || transition.LocalArcLengthMeters < 0d ||
transition.LocalArcLengthMeters > length + MergeToleranceMeters)
{
reason = "局部 G2 曲率事件不属于有效方向分段。";
return false;
}
ordered.Add(transition);
}
ordered.Sort(CompareTransitions);
var planned = new List();
int cursor = 0;
while (cursor < ordered.Count)
{
CurvatureTransition first = ordered[cursor];
double segmentLength = segmentLengths[first.SegmentIndex];
var group = new List { first };
IReadOnlyList variants =
BuildVariants(group, segmentLength, options);
if (variants.Count == 0)
{
reason = "局部 G2 单事件无法生成满足总长度约束的窗口。";
return false;
}
cursor++;
while (cursor < ordered.Count &&
ordered[cursor].SegmentIndex == first.SegmentIndex)
{
var tentative = new List(group)
{
ordered[cursor],
};
IReadOnlyList tentativeVariants =
BuildVariants(tentative, segmentLength, options);
if (tentativeVariants.Count == 0) break;
group = tentative;
variants = tentativeVariants;
cursor++;
}
double minimumStart = double.PositiveInfinity;
double maximumEnd = double.NegativeInfinity;
for (int variantIndex = 0; variantIndex < variants.Count; variantIndex++)
{
minimumStart = Math.Min(
minimumStart,
variants[variantIndex].StartArcLengthMeters);
maximumEnd = Math.Max(
maximumEnd,
variants[variantIndex].EndArcLengthMeters);
}
planned.Add(new LocalG2SmoothingRegion(
first.SegmentIndex,
group,
minimumStart,
maximumEnd,
variants));
}
regions = new ReadOnlyCollection(planned);
return true;
}
private static IReadOnlyList BuildVariants(
IReadOnlyList transitions,
double segmentLength,
LocalG2OptionsSnapshot options)
{
var variants = new List();
double firstEvent = transitions[0].LocalArcLengthMeters;
double lastEvent = transitions[transitions.Count - 1].LocalArcLengthMeters;
double anchor = (firstEvent + lastEvent) / 2d;
IReadOnlyList targets = BuildTargets(options, segmentLength);
double[] ratios = { 0.5d, 0.4d, 0.6d };
for (int ratioIndex = 0; ratioIndex < ratios.Length; ratioIndex++)
{
for (int targetIndex = 0; targetIndex < targets.Count; targetIndex++)
{
if (variants.Count >= options.MaximumCandidatesPerRegion)
return new ReadOnlyCollection(variants);
AddIfLegal(
variants,
targets[targetIndex],
ratios[ratioIndex],
anchor,
firstEvent,
lastEvent,
segmentLength,
ratioIndex == 0,
options);
}
}
return new ReadOnlyCollection(variants);
}
private static void AddIfLegal(List variants, double target, double leftRatio,
double anchor, double firstEvent, double lastEvent, double segmentLength, bool permitBoundaryShift,
LocalG2OptionsSnapshot options)
{
if (variants.Count >= options.MaximumCandidatesPerRegion) return;
double left = target * leftRatio;
double right = target - left;
double availableLeft = anchor;
double availableRight = segmentLength - anchor;
if (permitBoundaryShift)
{
left = Math.Min(left, availableLeft);
right = Math.Min(right, availableRight);
double missing = target - left - right;
double addRight = Math.Min(missing, availableRight - right);
right += addRight;
left += Math.Min(missing - addRight, availableLeft - left);
}
else if (left > availableLeft + MergeToleranceMeters || right > availableRight + MergeToleranceMeters)
{
return;
}
double start = anchor - left;
double end = anchor + right;
if (start > firstEvent + MergeToleranceMeters || end + MergeToleranceMeters < lastEvent || end - start + MergeToleranceMeters < target)
return;
double actualLength = end - start;
if (actualLength + MergeToleranceMeters < options.MinimumWindowLengthMeters ||
actualLength > options.MaximumWindowLengthMeters + MergeToleranceMeters)
{
return;
}
variants.Add(new LocalG2WindowVariant(variants.Count, start, end, left, right));
}
private static IReadOnlyList BuildTargets(LocalG2OptionsSnapshot options, double segmentLength)
{
if (segmentLength + MergeToleranceMeters < options.MinimumWindowLengthMeters)
return new ReadOnlyCollection(new List());
double[] requested =
{
options.PreferredWindowLengthMeters,
options.MinimumWindowLengthMeters,
options.MaximumWindowLengthMeters,
0.75d * options.PreferredWindowLengthMeters,
1.25d * options.PreferredWindowLengthMeters,
};
var targets = new List(requested.Length);
for (int index = 0; index < requested.Length; index++)
{
double target = Math.Min(segmentLength,
Math.Max(options.MinimumWindowLengthMeters, Math.Min(options.MaximumWindowLengthMeters, requested[index])));
bool duplicate = false;
for (int prior = 0; prior < targets.Count; prior++)
{
if (Math.Abs(targets[prior] - target) <= MergeToleranceMeters)
{
duplicate = true;
break;
}
}
if (!duplicate) targets.Add(target);
}
return targets;
}
private static bool TryGetSegmentLengths(PreparedPath originalPath, out Dictionary lengths, out string reason)
{
lengths = new Dictionary();
reason = string.Empty;
for (int position = 0; position < originalPath.Segments.Count; position++)
{
PreparedDirectionSegment segment = originalPath.Segments[position];
if (segment == null || segment.SegmentIndex != position || segment.Points == null || segment.Points.Count == 0)
{
reason = "局部 G2 窗口规划的预处理方向分段无效。";
return false;
}
double previousArc = -1d;
for (int pointIndex = 0; pointIndex < segment.Points.Count; pointIndex++)
{
double arc = segment.Points[pointIndex].ArcLength;
if (!NumericGuard.IsFinite(arc) || arc < 0d || arc < previousArc)
{
reason = "局部 G2 窗口规划要求分段弧长有限且非递减。";
return false;
}
previousArc = arc;
}
lengths.Add(segment.SegmentIndex, previousArc);
}
return true;
}
private static int CompareTransitions(CurvatureTransition left, CurvatureTransition right)
{
int segment = left.SegmentIndex.CompareTo(right.SegmentIndex);
if (segment != 0) return segment;
int arc = left.LocalArcLengthMeters.CompareTo(right.LocalArcLengthMeters);
return arc != 0 ? arc : left.LeftCoarsePathIndex.CompareTo(right.LeftCoarsePathIndex);
}
private static IReadOnlyList Empty() => new ReadOnlyCollection(new List());
public static class TestHooks
{
public static WindowPlanningTestSnapshot Execute(string scenario)
{
if (string.IsNullOrWhiteSpace(scenario))
throw new ArgumentException("A scenario is required.", nameof(scenario));
IReadOnlyList transitions;
double segmentLength;
switch (scenario)
{
case "SeparatedByOneMeter":
transitions = new[]
{
Transition(0.2d, 0),
Transition(1.2d, 1),
};
segmentLength = 1.4d;
break;
case "Mergeable":
transitions = new[]
{
Transition(0.4d, 0),
Transition(0.7d, 1),
};
segmentLength = 1.4d;
break;
case "ThreeEventPartition":
transitions = new[]
{
Transition(0.2d, 0),
Transition(0.6d, 1),
Transition(1.2d, 2),
};
segmentLength = 1.4d;
break;
case "NearBoundary":
transitions = new[] { Transition(0.1d, 0) };
segmentLength = 1d;
break;
case "InteriorCoverage":
transitions = new[] { Transition(1d, 0) };
segmentLength = 2d;
break;
default:
throw new ArgumentOutOfRangeException(nameof(scenario));
}
var planner = new LocalG2WindowPlanner();
if (!planner.TryPlan(
CreatePreparedPath(segmentLength),
transitions,
new LocalG2OptionsSnapshot(new PathSmoothingConfiguration()),
out IReadOnlyList regions,
out string reason))
{
throw new InvalidOperationException(reason);
}
double maximumLength = 0d;
bool exactEnvelope = true;
var counts = new List(regions.Count);
var signature = new List();
for (int regionIndex = 0; regionIndex < regions.Count; regionIndex++)
{
LocalG2SmoothingRegion region = regions[regionIndex];
counts.Add(region.Transitions.Count.ToString());
double minimumStart = double.PositiveInfinity;
double maximumEnd = double.NegativeInfinity;
for (int variantIndex = 0; variantIndex < region.WindowVariants.Count; variantIndex++)
{
LocalG2WindowVariant variant = region.WindowVariants[variantIndex];
maximumLength = Math.Max(
maximumLength,
variant.EndArcLengthMeters - variant.StartArcLengthMeters);
minimumStart = Math.Min(minimumStart, variant.StartArcLengthMeters);
maximumEnd = Math.Max(maximumEnd, variant.EndArcLengthMeters);
signature.Add(
region.SegmentIndex + ":" +
variant.CandidateIndex + ":" +
variant.StartArcLengthMeters.ToString("R") + ":" +
variant.EndArcLengthMeters.ToString("R"));
}
exactEnvelope &= Math.Abs(region.MaximumStartArcLengthMeters - minimumStart) <= 1e-9d;
exactEnvelope &= Math.Abs(region.MaximumEndArcLengthMeters - maximumEnd) <= 1e-9d;
}
IReadOnlyList firstVariants = regions[0].WindowVariants;
bool representativeTargetsFirst =
firstVariants.Count >= 3 &&
Math.Abs(WindowLength(firstVariants[0]) - 0.50d) <= 1e-9d &&
Math.Abs(WindowLength(firstVariants[1]) - 0.20d) <= 1e-9d &&
Math.Abs(WindowLength(firstVariants[2]) - 0.80d) <= 1e-9d;
bool hasAsymmetricVariant = false;
for (int index = 0; index < firstVariants.Count; index++)
{
if (Math.Abs(
firstVariants[index].LeftWindowLengthMeters -
firstVariants[index].RightWindowLengthMeters) > 1e-9d)
{
hasAsymmetricVariant = true;
break;
}
}
LocalG2WindowVariant first = firstVariants[0];
return new WindowPlanningTestSnapshot(
regions.Count,
string.Join(",", counts),
maximumLength,
exactEnvelope,
first.LeftWindowLengthMeters,
first.RightWindowLengthMeters,
string.Join("|", signature),
firstVariants.Count,
representativeTargetsFirst,
hasAsymmetricVariant);
}
public sealed class WindowPlanningTestSnapshot
{
internal WindowPlanningTestSnapshot(
int regionCount,
string transitionCounts,
double maximumWindowLength,
bool exactEnvelope,
double firstLeftLength,
double firstRightLength,
string signature,
int firstRegionVariantCount,
bool representativeTargetsFirst,
bool hasAsymmetricVariant)
{
RegionCount = regionCount;
TransitionCounts = transitionCounts;
MaximumWindowLength = maximumWindowLength;
ExactEnvelope = exactEnvelope;
FirstLeftLength = firstLeftLength;
FirstRightLength = firstRightLength;
Signature = signature;
FirstRegionVariantCount = firstRegionVariantCount;
RepresentativeTargetsFirst = representativeTargetsFirst;
HasAsymmetricVariant = hasAsymmetricVariant;
}
public int RegionCount { get; }
public string TransitionCounts { get; }
public double MaximumWindowLength { get; }
public bool ExactEnvelope { get; }
public double FirstLeftLength { get; }
public double FirstRightLength { get; }
public string Signature { get; }
public int FirstRegionVariantCount { get; }
public bool RepresentativeTargetsFirst { get; }
public bool HasAsymmetricVariant { get; }
}
private static double WindowLength(LocalG2WindowVariant variant) =>
variant.EndArcLengthMeters - variant.StartArcLengthMeters;
private static CurvatureTransition Transition(double arcLength, int index)
{
return new CurvatureTransition(
0,
index,
index + 1,
arcLength,
arcLength,
0d,
0d,
index % 2 == 0 ? 0d : 0.5d,
index % 2 == 0 ? 0.5d : 0d);
}
private static PreparedPath CreatePreparedPath(double length)
{
var points = new[]
{
new SmoothingPoint2D(
0d, 0d, 0d, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor),
new SmoothingPoint2D(
length, 0d, length, 0d, 0d, 1d, false, SmoothedPathPointSource.Anchor),
};
return new PreparedPath(new[]
{
new PreparedDirectionSegment(
0, TravelDirection.Forward, points, false, false),
});
}
}
}