using System; using System.Globalization; using System.Text; namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization; /// 把单张共享图形定义渲染为 UTF-8 XML 可编辑 SVG;轨迹仅由离散点组成。 public sealed class SmoothingSvgRenderer { public string Render(SmoothingFigureModel model) { if (model == null) throw new ArgumentNullException(nameof(model)); return Render(new SmoothingFigureSetBuilder().Build(model).Figures[1]); } public string Render(SmoothingFigureDefinition figure) { if (figure == null) throw new ArgumentNullException(nameof(figure)); var svg = new StringBuilder(); svg.Append("\n\n") .Append("\n") .Append("\n"); AppendTitle(svg, figure); if (figure.IsCurvatureFigure) AppendCurvatureAxes(svg, figure); else AppendOverheadAxes(svg, figure); svg.Append("\n"); if (!figure.IsCurvatureFigure && figure.ShowsMapContext) AppendObstacles(svg, figure); if (figure.IsCurvatureFigure) AppendCurvaturePoints(svg, figure); else AppendPathPoints(svg, figure); if (!figure.IsCurvatureFigure && figure.ShowsMapContext) AppendStartGoal(svg, figure); svg.Append("\n"); AppendLegend(svg, figure); svg.Append(""); return svg.ToString(); } private static void AppendTitle(StringBuilder svg, SmoothingFigureDefinition figure) { svg.Append("") .Append(Escape(figure.Title)).Append(" — ").Append(Escape(figure.Model.ScenarioLabel)).Append("\n"); } private static void AppendOverheadAxes(StringBuilder svg, SmoothingFigureDefinition figure) { AppendPlotFrame(svg, figure); for (int index = 0; index < figure.XTicks.Count; index++) { double x = WorldX(figure, figure.XTicks[index]); AppendGridLine(svg, x, figure.PlotYPoints, x, figure.PlotYPoints + figure.PlotHeightPoints); svg.Append("").Append(Number(figure.XTicks[index])).Append("\n"); } for (int index = 0; index < figure.YTicks.Count; index++) { double y = WorldY(figure, figure.YTicks[index]); AppendGridLine(svg, figure.PlotXPoints, y, figure.PlotXPoints + figure.PlotWidthPoints, y); svg.Append("").Append(Number(figure.YTicks[index])).Append("\n"); } svg.Append("X (m)\n") .Append("Y (m)\n"); } private static void AppendCurvatureAxes(StringBuilder svg, SmoothingFigureDefinition figure) { AppendPlotFrame(svg, figure); for (int index = 0; index < figure.CurvatureArcLengthTicks.Count; index++) { double x = CurvatureX(figure, figure.CurvatureArcLengthTicks[index]); AppendGridLine(svg, x, figure.PlotYPoints, x, figure.PlotYPoints + figure.PlotHeightPoints); svg.Append("").Append(Number(figure.CurvatureArcLengthTicks[index])).Append("\n"); } for (int index = 0; index < figure.CurvatureTicks.Count; index++) { double y = CurvatureY(figure, figure.CurvatureTicks[index]); AppendGridLine(svg, figure.PlotXPoints, y, figure.PlotXPoints + figure.PlotWidthPoints, y); svg.Append("").Append(Number(figure.CurvatureTicks[index])).Append("\n"); } if (figure.CurvatureMinimumPerMeter < 0d && figure.CurvatureMaximumPerMeter > 0d) { double zero = CurvatureY(figure, 0d); svg.Append("\n"); } svg.Append("s (m)\n") .Append("κ (m⁻¹)\n"); } private static void AppendPlotFrame(StringBuilder svg, SmoothingFigureDefinition figure) { svg.Append("\n"); } private static void AppendGridLine(StringBuilder svg, double x1, double y1, double x2, double y2) { svg.Append("\n"); } private static void AppendObstacles(StringBuilder svg, SmoothingFigureDefinition figure) { for (int index = 0; index < figure.Model.Obstacles.Count; index++) { SmoothingFigureObstacle obstacle = figure.Model.Obstacles[index]; svg.Append("\n"); } } private static void AppendPathPoints(StringBuilder svg, SmoothingFigureDefinition figure) { for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++) { SmoothingFigureSeriesView view = figure.Series[viewIndex]; AppendPoints(svg, view, point => WorldX(figure, point.X), point => WorldY(figure, point.Y)); for (int markerIndex = 0; markerIndex < view.Series.ViolationMarkers.Count; markerIndex++) AppendCross(svg, figure, view.Series.ViolationMarkers[markerIndex], view.Series.Color); } } private static void AppendCurvaturePoints(StringBuilder svg, SmoothingFigureDefinition figure) { for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++) { SmoothingFigureSeriesView view = figure.Series[viewIndex]; AppendPoints(svg, view, point => CurvatureX(figure, point.ArcLength), point => CurvatureY(figure, point.VehicleCurvature)); } } private static void AppendPoints(StringBuilder svg, SmoothingFigureSeriesView view, Func x, Func y) { if (!view.Series.IsCurveVisible) return; svg.Append("\n"); for (int index = 0; index < view.Series.Points.Count; index++) { SmoothingFigurePoint point = view.Series.Points[index]; svg.Append("\n"); } svg.Append("\n"); } private static void AppendCross(StringBuilder svg, SmoothingFigureDefinition figure, SmoothingFigurePoint point, string color) { double x = WorldX(figure, point.X), y = WorldY(figure, point.Y), radius = 3d; svg.Append("\n"); } private static void AppendStartGoal(StringBuilder svg, SmoothingFigureDefinition figure) { svg.Append("\n"); double x = WorldX(figure, figure.Model.Goal.X), y = WorldY(figure, figure.Model.Goal.Y), radius = 4d; svg.Append("\n"); } private static void AppendLegend(StringBuilder svg, SmoothingFigureDefinition figure) { double columnWidth = 198d; for (int index = 0; index < figure.LegendEntries.Count; index++) { SmoothingFigureLegendEntry entry = figure.LegendEntries[index]; int column = index % 2; int row = index / 2; double x = figure.PlotXPoints + column * columnWidth; double y = figure.LegendYPoints + row * 15d; svg.Append("\n") .Append(Escape(entry.Label)).Append("\n"); } } private static double WorldX(SmoothingFigureDefinition figure, double x) { return figure.PlotXPoints + (x - figure.WorldXMinMeters) * figure.WorldScalePointsPerMeter; } private static double WorldY(SmoothingFigureDefinition figure, double y) { return figure.PlotYPoints + figure.PlotHeightPoints - (y - figure.WorldYMinMeters) * figure.WorldScalePointsPerMeter; } private static double CurvatureX(SmoothingFigureDefinition figure, double arcLength) { return figure.PlotXPoints + arcLength / figure.CurvatureArcLengthMaximumMeters * figure.PlotWidthPoints; } private static double CurvatureY(SmoothingFigureDefinition figure, double curvature) { return figure.PlotYPoints + figure.PlotHeightPoints - (curvature - figure.CurvatureMinimumPerMeter) / (figure.CurvatureMaximumPerMeter - figure.CurvatureMinimumPerMeter) * figure.PlotHeightPoints; } private static string Number(double value) { return value.ToString("0.###", CultureInfo.InvariantCulture); } private static string Escape(string value) { return (value ?? string.Empty).Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """).Replace("'", "'"); } }