using System; using System.Collections.Generic; using MultiWheelC.TrajectoryPlanning.CoarsePath; using MultiWheelC.TrajectoryPlanning.Mapping; using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; /// 将不可变的比较结果、地图和端点转换为共享报告图形模型。 public sealed class SmoothingFigureModelBuilder { private const double MarginPoints = 18d; private const double PanelGapPoints = 12d; public SmoothingFigureModel Build( PathSmoothingComparisonResult comparison, PlanningGridMap map, Pose2D start, Pose2D goal, string scenarioId, string scenarioLabel) { if (comparison == null) throw new ArgumentNullException(nameof(comparison)); if (map == null) throw new ArgumentNullException(nameof(map)); if (start == null) throw new ArgumentNullException(nameof(start)); if (goal == null) throw new ArgumentNullException(nameof(goal)); double worldXMin = map.Bounds.XMin / 1000d; double worldXMax = map.Bounds.XMax / 1000d; double worldYMin = map.Bounds.YMin / 1000d; double worldYMax = map.Bounds.YMax / 1000d; double innerWidth = IeeeFigureStyle.FigureWidthPoints - 2d * MarginPoints; double innerHeight = IeeeFigureStyle.FigureHeightPoints - 2d * MarginPoints; double pathWidth = innerWidth * 0.60d; double rightWidth = innerWidth - pathWidth - PanelGapPoints; double rightHeight = (innerHeight - PanelGapPoints) / 2d; var model = new SmoothingFigureModel( scenarioId, scenarioLabel, worldXMin, worldXMax, worldYMin, worldYMax, MarginPoints, MarginPoints, pathWidth, innerHeight, MarginPoints + pathWidth + PanelGapPoints, MarginPoints, rightWidth, rightHeight, MarginPoints + pathWidth + PanelGapPoints, MarginPoints + rightHeight + PanelGapPoints, rightWidth, rightHeight, BuildObstacles(map), BuildSeries(comparison, map), BuildMetricRows(comparison), new SmoothingFigurePoint(start.X, start.Y, 0d, 0d), new SmoothingFigurePoint(goal.X, goal.Y, 0d, 0d)); double worldWidth = worldXMax - worldXMin; double worldHeight = worldYMax - worldYMin; if (worldWidth <= 0d || worldHeight <= 0d) throw new ArgumentOutOfRangeException(nameof(map), "地图边界必须为非退化有限范围。"); double scale = Math.Min(pathWidth / worldWidth, innerHeight / worldHeight); model.PathScaleX = scale; model.PathScaleY = scale; return model; } private static IReadOnlyList BuildObstacles(PlanningGridMap map) { var runs = new List(); double resolution = map.ResolutionMeters; double xMin = map.Bounds.XMin / 1000d; double yMin = map.Bounds.YMin / 1000d; for (int row = 0; row < map.Rows; row++) { int runStart = -1; for (int column = 0; column <= map.Cols; column++) { bool occupied = column < map.Cols && map.IsOccupied(row, column); if (occupied && runStart < 0) { runStart = column; continue; } if (!occupied && runStart >= 0) { AddOrExtendRun(runs, xMin + runStart * resolution, yMin + row * resolution, (column - runStart) * resolution, resolution); runStart = -1; } } } var obstacles = new List(runs.Count); for (int index = 0; index < runs.Count; index++) { ObstacleRun run = runs[index]; obstacles.Add(new SmoothingFigureObstacle(run.X, run.Y, run.Width, run.Height)); } return obstacles; } private static void AddOrExtendRun(IList runs, double x, double y, double width, double height) { for (int index = runs.Count - 1; index >= 0; index--) { ObstacleRun candidate = runs[index]; if (NearlyEqual(candidate.X, x) && NearlyEqual(candidate.Width, width) && NearlyEqual(candidate.Y + candidate.Height, y)) { candidate.Height += height; return; } } runs.Add(new ObstacleRun(x, y, width, height)); } private static bool NearlyEqual(double first, double second) { return Math.Abs(first - second) < 1e-9d; } private static IReadOnlyList BuildSeries(PathSmoothingComparisonResult comparison, PlanningGridMap map) { var series = new List(4) { CreateSeries(comparison.RawPathBaseline, null, "raw", "原始粗路径", IeeeFigureStyle.RawColor, string.Empty, true, map) }; series.Add(CreateSeries(Find(comparison, SmoothingMethod.CubicBSpline), SmoothingMethod.CubicBSpline, "bspline", "三次 B 样条", IeeeFigureStyle.BSplineColor, string.Empty, false, map)); series.Add(CreateSeries(Find(comparison, SmoothingMethod.LocalCubicBezier), SmoothingMethod.LocalCubicBezier, "bezier", "局部三次 Bézier", IeeeFigureStyle.BezierColor, string.Empty, false, map)); series.Add(CreateSeries(Find(comparison, SmoothingMethod.PiecewiseQuintic), SmoothingMethod.PiecewiseQuintic, "quintic", "分段五次", IeeeFigureStyle.QuinticColor, string.Empty, false, map)); return series; } private static IReadOnlyList BuildMetricRows(PathSmoothingComparisonResult comparison) { return new List { CreateRow(comparison.RawPathBaseline, "RawPath", "原始粗路径"), CreateRow(Find(comparison, SmoothingMethod.CubicBSpline), "CubicBSpline", "三次 B 样条"), CreateRow(Find(comparison, SmoothingMethod.LocalCubicBezier), "LocalCubicBezier", "局部三次 Bézier"), CreateRow(Find(comparison, SmoothingMethod.PiecewiseQuintic), "PiecewiseQuintic", "分段五次") }; } private static PathSmoothingComparisonEntry Find(PathSmoothingComparisonResult comparison, SmoothingMethod method) { for (int index = 0; index < comparison.Entries.Count; index++) { PathSmoothingComparisonEntry entry = comparison.Entries[index]; if (entry.Method == method) return entry; } return null; } private static SmoothingFigureMetricRow CreateRow(PathSmoothingComparisonEntry entry, string method, string label) { return entry == null ? new SmoothingFigureMetricRow(method, label, PathSmoothingStatus.Failed, new PathQualityMetrics(), null, 0, 0d) : new SmoothingFigureMetricRow(method, label, entry.Status, entry.Metrics, entry.Timing, entry.RetryCount, entry.AcceptedStrength); } private static SmoothingFigureSeries CreateSeries(PathSmoothingComparisonEntry entry, SmoothingMethod? method, string key, string label, string color, string dashArray, bool isRawPathBaseline, PlanningGridMap map) { var points = new List(); var violations = new List(); PathSmoothingStatus status = entry == null ? PathSmoothingStatus.Failed : entry.Status; if (entry != null) { for (int index = 0; index < entry.Path.Count; index++) { SmoothedPathPoint point = entry.Path[index]; var figurePoint = new SmoothingFigurePoint(point.X, point.Y, point.ArcLength, point.VehicleCurvature); points.Add(figurePoint); if (status == PathSmoothingStatus.Infeasible && map.IsOccupiedWorld(point.X, point.Y)) violations.Add(figurePoint); } } if (status == PathSmoothingStatus.Infeasible && points.Count > 0 && violations.Count == 0) violations.Add(points[points.Count / 2]); return new SmoothingFigureSeries(method, key, label, status, color, dashArray, isRawPathBaseline, points, violations); } private sealed class ObstacleRun { public ObstacleRun(double x, double y, double width, double height) { X = x; Y = y; Width = width; Height = height; } public double X { get; } public double Y { get; } public double Width { get; } public double Height { get; set; } } }