172 lines
8.8 KiB
C#
172 lines
8.8 KiB
C#
using System;
|
|||
|
|
using System.Collections.Generic;
|
||
|
|
using System.Collections.ObjectModel;
|
||
|
|
|
||
|
|
namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization;
|
||
|
|
|
||
|
|
/// <summary>从一份比较模型构建六张含义固定、相机聚焦的报告图。</summary>
|
||
|
|
public sealed class SmoothingFigureSetBuilder
|
||
|
|
{
|
||
|
|
private const double MinimumExtentMeters = 0.25d;
|
||
|
|
private const double PaddingFraction = 0.10d;
|
||
|
|
|
||
|
|
public SmoothingFigureSet Build(SmoothingFigureModel model)
|
||
|
|
{
|
||
|
|
if (model == null) throw new ArgumentNullException(nameof(model));
|
||
|
|
SmoothingFigureSeries raw = Find(model, "raw");
|
||
|
|
SmoothingFigureSeries bSpline = Find(model, "bspline");
|
||
|
|
SmoothingFigureSeries bezier = Find(model, "bezier");
|
||
|
|
SmoothingFigureSeries quintic = Find(model, "quintic");
|
||
|
|
var figures = new List<SmoothingFigureDefinition>
|
||
|
|
{
|
||
|
|
BuildOverhead(SmoothingFigureKind.CoarsePathOverview, "01-coarse-path-overview", "Hybrid A* 原始粗路径", model, true, View(raw, 1d)),
|
||
|
|
BuildOverhead(SmoothingFigureKind.AllPathsComparison, "02-all-paths-comparison", "路径平滑结果对比", model, false, View(raw, 1d), View(bSpline, 1d), View(bezier, 1d), View(quintic, 1d)),
|
||
|
|
BuildOverhead(SmoothingFigureKind.CubicBSplineOverview, "03-cubic-bspline-overview", "三次 B 样条平滑", model, true, View(raw, 0.28d), View(bSpline, 1d)),
|
||
|
|
BuildOverhead(SmoothingFigureKind.LocalCubicBezierOverview, "04-local-cubic-bezier-overview", "局部三次 Bézier 平滑", model, true, View(raw, 0.28d), View(bezier, 1d)),
|
||
|
|
BuildOverhead(SmoothingFigureKind.PiecewiseQuinticOverview, "05-piecewise-quintic-overview", "分段五次平滑", model, true, View(raw, 0.28d), View(quintic, 1d)),
|
||
|
|
BuildCurvature(model, View(raw, 1d), View(bSpline, 1d), View(bezier, 1d), View(quintic, 1d)),
|
||
|
|
};
|
||
|
|
return new SmoothingFigureSet(figures);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static SmoothingFigureDefinition BuildOverhead(SmoothingFigureKind kind, string stem, string title, SmoothingFigureModel model, bool mapContext, params SmoothingFigureSeriesView[] series)
|
||
|
|
{
|
||
|
|
Bounds bounds = CalculateWorldBounds(model, mapContext, series);
|
||
|
|
return new SmoothingFigureDefinition(kind, stem, title, model, mapContext, series,
|
||
|
|
bounds.XMin, bounds.XMax, bounds.YMin, bounds.YMax,
|
||
|
|
BuildTicks(bounds.XMin, bounds.XMax), BuildTicks(bounds.YMin, bounds.YMax),
|
||
|
|
1d, -1d, 1d, Array.Empty<double>(), Array.Empty<double>());
|
||
|
|
}
|
||
|
|
|
||
|
|
private static SmoothingFigureDefinition BuildCurvature(SmoothingFigureModel model, params SmoothingFigureSeriesView[] series)
|
||
|
|
{
|
||
|
|
double arcMaximum = 0d;
|
||
|
|
double curvatureMinimum = 0d;
|
||
|
|
double curvatureMaximum = 0d;
|
||
|
|
for (int viewIndex = 0; viewIndex < series.Length; viewIndex++)
|
||
|
|
{
|
||
|
|
IReadOnlyList<SmoothingFigurePoint> points = series[viewIndex].Series.Points;
|
||
|
|
for (int pointIndex = 0; pointIndex < points.Count; pointIndex++)
|
||
|
|
{
|
||
|
|
SmoothingFigurePoint point = points[pointIndex];
|
||
|
|
if (point.ArcLength > arcMaximum) arcMaximum = point.ArcLength;
|
||
|
|
if (point.VehicleCurvature < curvatureMinimum) curvatureMinimum = point.VehicleCurvature;
|
||
|
|
if (point.VehicleCurvature > curvatureMaximum) curvatureMaximum = point.VehicleCurvature;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
arcMaximum = ExpandMaximum(arcMaximum, MinimumExtentMeters);
|
||
|
|
ExpandRange(ref curvatureMinimum, ref curvatureMaximum, MinimumExtentMeters);
|
||
|
|
return new SmoothingFigureDefinition(SmoothingFigureKind.CurvatureComparison, "06-curvature-comparison", "车辆曲率对比", model, false, series,
|
||
|
|
0d, 1d, 0d, 1d, Array.Empty<double>(), Array.Empty<double>(),
|
||
|
|
arcMaximum, curvatureMinimum, curvatureMaximum, BuildTicks(0d, arcMaximum), BuildTicks(curvatureMinimum, curvatureMaximum));
|
||
|
|
}
|
||
|
|
|
||
|
|
private static Bounds CalculateWorldBounds(SmoothingFigureModel model, bool includesEndpoints, IReadOnlyList<SmoothingFigureSeriesView> series)
|
||
|
|
{
|
||
|
|
bool hasPoint = false;
|
||
|
|
double xMin = 0d, xMax = 0d, yMin = 0d, yMax = 0d;
|
||
|
|
for (int viewIndex = 0; viewIndex < series.Count; viewIndex++)
|
||
|
|
{
|
||
|
|
IReadOnlyList<SmoothingFigurePoint> points = series[viewIndex].Series.Points;
|
||
|
|
for (int pointIndex = 0; pointIndex < points.Count; pointIndex++) Include(points[pointIndex].X, points[pointIndex].Y, ref hasPoint, ref xMin, ref xMax, ref yMin, ref yMax);
|
||
|
|
}
|
||
|
|
if (includesEndpoints)
|
||
|
|
{
|
||
|
|
Include(model.Start.X, model.Start.Y, ref hasPoint, ref xMin, ref xMax, ref yMin, ref yMax);
|
||
|
|
Include(model.Goal.X, model.Goal.Y, ref hasPoint, ref xMin, ref xMax, ref yMin, ref yMax);
|
||
|
|
}
|
||
|
|
if (!hasPoint)
|
||
|
|
{
|
||
|
|
xMin = model.WorldXMinMeters; xMax = model.WorldXMaxMeters;
|
||
|
|
yMin = model.WorldYMinMeters; yMax = model.WorldYMaxMeters;
|
||
|
|
}
|
||
|
|
ExpandRange(ref xMin, ref xMax, MinimumExtentMeters);
|
||
|
|
ExpandRange(ref yMin, ref yMax, MinimumExtentMeters);
|
||
|
|
double xPadding = (xMax - xMin) * PaddingFraction;
|
||
|
|
double yPadding = (yMax - yMin) * PaddingFraction;
|
||
|
|
xMin -= xPadding; xMax += xPadding; yMin -= yPadding; yMax += yPadding;
|
||
|
|
double desiredAspect = 400d / 245d;
|
||
|
|
double width = xMax - xMin;
|
||
|
|
double height = yMax - yMin;
|
||
|
|
if (width / height < desiredAspect)
|
||
|
|
{
|
||
|
|
double halfWidth = height * desiredAspect / 2d;
|
||
|
|
double center = (xMin + xMax) / 2d;
|
||
|
|
xMin = center - halfWidth; xMax = center + halfWidth;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
double halfHeight = width / desiredAspect / 2d;
|
||
|
|
double center = (yMin + yMax) / 2d;
|
||
|
|
yMin = center - halfHeight; yMax = center + halfHeight;
|
||
|
|
}
|
||
|
|
return new Bounds(xMin, xMax, yMin, yMax);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void Include(double x, double y, ref bool hasPoint, ref double xMin, ref double xMax, ref double yMin, ref double yMax)
|
||
|
|
{
|
||
|
|
if (!hasPoint) { hasPoint = true; xMin = xMax = x; yMin = yMax = y; return; }
|
||
|
|
if (x < xMin) xMin = x;
|
||
|
|
if (x > xMax) xMax = x;
|
||
|
|
if (y < yMin) yMin = y;
|
||
|
|
if (y > yMax) yMax = y;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void ExpandRange(ref double minimum, ref double maximum, double minimumExtent)
|
||
|
|
{
|
||
|
|
double extent = maximum - minimum;
|
||
|
|
if (extent >= minimumExtent) return;
|
||
|
|
double center = (minimum + maximum) / 2d;
|
||
|
|
minimum = center - minimumExtent / 2d;
|
||
|
|
maximum = center + minimumExtent / 2d;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static double ExpandMaximum(double value, double minimum) { return value < minimum ? minimum : value * (1d + PaddingFraction); }
|
||
|
|
|
||
|
|
private static IReadOnlyList<double> BuildTicks(double minimum, double maximum)
|
||
|
|
{
|
||
|
|
double span = maximum - minimum;
|
||
|
|
if (span <= 0d) return new[] { minimum, maximum };
|
||
|
|
double roughStep = span / 5d;
|
||
|
|
double magnitude = Math.Pow(10d, Math.Floor(Math.Log10(roughStep)));
|
||
|
|
double normalized = roughStep / magnitude;
|
||
|
|
double nice = normalized <= 1d ? 1d : (normalized <= 2d ? 2d : (normalized <= 5d ? 5d : 10d));
|
||
|
|
double step = nice * magnitude;
|
||
|
|
var ticks = new List<double>();
|
||
|
|
double first = Math.Ceiling(minimum / step) * step;
|
||
|
|
for (double value = first; value <= maximum + step * 0.001d; value += step) ticks.Add(value);
|
||
|
|
if (ticks.Count < 2) { ticks.Clear(); ticks.Add(minimum); ticks.Add(maximum); }
|
||
|
|
return new ReadOnlyCollection<double>(ticks);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static SmoothingFigureSeries Find(SmoothingFigureModel model, string key)
|
||
|
|
{
|
||
|
|
for (int index = 0; index < model.Series.Count; index++) if (model.Series[index].Key == key) return model.Series[index];
|
||
|
|
throw new InvalidOperationException("比较图形模型缺少路径序列: " + key);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static SmoothingFigureSeriesView View(SmoothingFigureSeries series, double opacity) { return new SmoothingFigureSeriesView(series, opacity); }
|
||
|
|
|
||
|
|
private readonly struct Bounds
|
||
|
|
{
|
||
|
|
public Bounds(double xMin, double xMax, double yMin, double yMax) { XMin = xMin; XMax = xMax; YMin = yMin; YMax = yMax; }
|
||
|
|
public double XMin { get; }
|
||
|
|
public double XMax { get; }
|
||
|
|
public double YMin { get; }
|
||
|
|
public double YMax { get; }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>六张图的稳定顺序。</summary>
|
||
|
|
public sealed class SmoothingFigureSet
|
||
|
|
{
|
||
|
|
internal SmoothingFigureSet(IReadOnlyList<SmoothingFigureDefinition> figures)
|
||
|
|
{
|
||
|
|
var copy = new List<SmoothingFigureDefinition>(figures == null ? 0 : figures.Count);
|
||
|
|
if (figures != null) for (int index = 0; index < figures.Count; index++) copy.Add(figures[index]);
|
||
|
|
Figures = new ReadOnlyCollection<SmoothingFigureDefinition>(copy);
|
||
|
|
}
|
||
|
|
|
||
|
|
public IReadOnlyList<SmoothingFigureDefinition> Figures { get; }
|
||
|
|
}
|