Files
ParkingRobot/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/SmoothingSvgRenderer.cs
T

190 lines
14 KiB
C#
Raw Normal View History

2026-08-09 22:13:18 +08:00
using System;
using System.Globalization;
using System.Text;
namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Output.Visualization;
/// <summary>把单张共享图形定义渲染为 UTF-8 XML 可编辑 SVG;轨迹仅由离散点组成。</summary>
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("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"")
.Append(Number(figure.FigureWidthPoints)).Append("pt\" height=\"").Append(Number(figure.FigureHeightPoints))
.Append("pt\" viewBox=\"0 0 ").Append(Number(figure.FigureWidthPoints)).Append(' ').Append(Number(figure.FigureHeightPoints)).Append("\">\n")
.Append("<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n")
.Append("<clipPath id=\"plot-clip\"><rect x=\"").Append(Number(figure.PlotXPoints)).Append("\" y=\"").Append(Number(figure.PlotYPoints))
.Append("\" width=\"").Append(Number(figure.PlotWidthPoints)).Append("\" height=\"").Append(Number(figure.PlotHeightPoints)).Append("\"/></clipPath>\n");
AppendTitle(svg, figure);
if (figure.IsCurvatureFigure) AppendCurvatureAxes(svg, figure); else AppendOverheadAxes(svg, figure);
svg.Append("<g clip-path=\"url(#plot-clip)\">\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("</g>\n");
AppendLegend(svg, figure);
svg.Append("</svg>");
return svg.ToString();
}
private static void AppendTitle(StringBuilder svg, SmoothingFigureDefinition figure)
{
svg.Append("<text x=\"").Append(Number(figure.PlotXPoints)).Append("\" y=\"23\" font-size=\"12\"><tspan font-family=\"SimSun\">")
.Append(Escape(figure.Title)).Append("</tspan><tspan font-family=\"Times New Roman\"> — ").Append(Escape(figure.Model.ScenarioLabel)).Append("</tspan></text>\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("<text x=\"").Append(Number(x)).Append("\" y=\"").Append(Number(figure.PlotYPoints + figure.PlotHeightPoints + 15d))
.Append("\" text-anchor=\"middle\" font-family=\"Times New Roman\" font-size=\"8\">").Append(Number(figure.XTicks[index])).Append("</text>\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("<text x=\"").Append(Number(figure.PlotXPoints - 8d)).Append("\" y=\"").Append(Number(y + 3d))
.Append("\" text-anchor=\"end\" font-family=\"Times New Roman\" font-size=\"8\">").Append(Number(figure.YTicks[index])).Append("</text>\n");
}
svg.Append("<text x=\"").Append(Number(figure.PlotXPoints + figure.PlotWidthPoints / 2d)).Append("\" y=\"").Append(Number(figure.PlotYPoints + figure.PlotHeightPoints + 31d))
.Append("\" text-anchor=\"middle\" font-family=\"Times New Roman\" font-size=\"10\">X (m)</text>\n")
.Append("<text x=\"19\" y=\"").Append(Number(figure.PlotYPoints + figure.PlotHeightPoints / 2d)).Append("\" text-anchor=\"middle\" transform=\"rotate(-90 19 ")
.Append(Number(figure.PlotYPoints + figure.PlotHeightPoints / 2d)).Append(")\" font-family=\"Times New Roman\" font-size=\"10\">Y (m)</text>\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("<text x=\"").Append(Number(x)).Append("\" y=\"").Append(Number(figure.PlotYPoints + figure.PlotHeightPoints + 15d))
.Append("\" text-anchor=\"middle\" font-family=\"Times New Roman\" font-size=\"8\">").Append(Number(figure.CurvatureArcLengthTicks[index])).Append("</text>\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("<text x=\"").Append(Number(figure.PlotXPoints - 8d)).Append("\" y=\"").Append(Number(y + 3d))
.Append("\" text-anchor=\"end\" font-family=\"Times New Roman\" font-size=\"8\">").Append(Number(figure.CurvatureTicks[index])).Append("</text>\n");
}
if (figure.CurvatureMinimumPerMeter < 0d && figure.CurvatureMaximumPerMeter > 0d)
{
double zero = CurvatureY(figure, 0d);
svg.Append("<line x1=\"").Append(Number(figure.PlotXPoints)).Append("\" y1=\"").Append(Number(zero)).Append("\" x2=\"")
.Append(Number(figure.PlotXPoints + figure.PlotWidthPoints)).Append("\" y2=\"").Append(Number(zero)).Append("\" stroke=\"#4D4D4D\" stroke-width=\"0.65\"/>\n");
}
svg.Append("<text x=\"").Append(Number(figure.PlotXPoints + figure.PlotWidthPoints / 2d)).Append("\" y=\"").Append(Number(figure.PlotYPoints + figure.PlotHeightPoints + 31d))
.Append("\" text-anchor=\"middle\" font-family=\"Times New Roman\" font-size=\"10\">s (m)</text>\n")
.Append("<text x=\"19\" y=\"").Append(Number(figure.PlotYPoints + figure.PlotHeightPoints / 2d)).Append("\" text-anchor=\"middle\" transform=\"rotate(-90 19 ")
.Append(Number(figure.PlotYPoints + figure.PlotHeightPoints / 2d)).Append(")\" font-family=\"Times New Roman\" font-size=\"10\">κ (m⁻¹)</text>\n");
}
private static void AppendPlotFrame(StringBuilder svg, SmoothingFigureDefinition figure)
{
svg.Append("<rect x=\"").Append(Number(figure.PlotXPoints)).Append("\" y=\"").Append(Number(figure.PlotYPoints)).Append("\" width=\"")
.Append(Number(figure.PlotWidthPoints)).Append("\" height=\"").Append(Number(figure.PlotHeightPoints)).Append("\" fill=\"#FFFFFF\" stroke=\"#000000\" stroke-width=\"0.75\"/>\n");
}
private static void AppendGridLine(StringBuilder svg, double x1, double y1, double x2, double y2)
{
svg.Append("<line x1=\"").Append(Number(x1)).Append("\" y1=\"").Append(Number(y1)).Append("\" x2=\"").Append(Number(x2)).Append("\" y2=\"")
.Append(Number(y2)).Append("\" stroke=\"#E6E6E6\" stroke-width=\"0.5\"/>\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("<rect class=\"obstacle\" x=\"").Append(Number(WorldX(figure, obstacle.X))).Append("\" y=\"")
.Append(Number(WorldY(figure, obstacle.Y + obstacle.Height))).Append("\" width=\"").Append(Number(obstacle.Width * figure.WorldScalePointsPerMeter))
.Append("\" height=\"").Append(Number(obstacle.Height * figure.WorldScalePointsPerMeter)).Append("\" fill=\"#D9D9D9\" stroke=\"#808080\" stroke-width=\"0.35\"/>\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<SmoothingFigurePoint, double> x, Func<SmoothingFigurePoint, double> y)
{
if (!view.Series.IsCurveVisible) return;
svg.Append("<g class=\"trajectory-series\" data-series=\"").Append(Escape(view.Series.Key)).Append("\" fill=\"").Append(view.Series.Color).Append("\" opacity=\"").Append(Number(view.Opacity)).Append("\">\n");
for (int index = 0; index < view.Series.Points.Count; index++)
{
SmoothingFigurePoint point = view.Series.Points[index];
svg.Append("<circle class=\"trajectory-point\" cx=\"").Append(Number(x(point))).Append("\" cy=\"").Append(Number(y(point))).Append("\" r=\"").Append(Number(view.PointRadiusPoints)).Append("\"/>\n");
}
svg.Append("</g>\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("<g class=\"violation-cross\" stroke=\"").Append(color).Append("\" stroke-width=\"1.1\"><line x1=\"").Append(Number(x - radius)).Append("\" y1=\"")
.Append(Number(y - radius)).Append("\" x2=\"").Append(Number(x + radius)).Append("\" y2=\"").Append(Number(y + radius)).Append("\"/><line x1=\"")
.Append(Number(x - radius)).Append("\" y1=\"").Append(Number(y + radius)).Append("\" x2=\"").Append(Number(x + radius)).Append("\" y2=\"").Append(Number(y - radius)).Append("\"/></g>\n");
}
private static void AppendStartGoal(StringBuilder svg, SmoothingFigureDefinition figure)
{
svg.Append("<circle class=\"start-marker\" cx=\"").Append(Number(WorldX(figure, figure.Model.Start.X))).Append("\" cy=\"").Append(Number(WorldY(figure, figure.Model.Start.Y)))
.Append("\" r=\"3.2\" fill=\"#F0E442\" stroke=\"#000000\" stroke-width=\"0.8\"/>\n");
double x = WorldX(figure, figure.Model.Goal.X), y = WorldY(figure, figure.Model.Goal.Y), radius = 4d;
svg.Append("<polygon class=\"goal-marker\" points=\"").Append(Number(x)).Append(',').Append(Number(y - radius)).Append(' ').Append(Number(x + radius)).Append(',').Append(Number(y)).Append(' ')
.Append(Number(x)).Append(',').Append(Number(y + radius)).Append(' ').Append(Number(x - radius)).Append(',').Append(Number(y)).Append("\" fill=\"#CC79A7\" stroke=\"#000000\" stroke-width=\"0.8\"/>\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("<circle class=\"legend-point\" cx=\"").Append(Number(x + 3d)).Append("\" cy=\"").Append(Number(y - 3d)).Append("\" r=\"2.2\" fill=\"")
.Append(entry.Color).Append("\"/>\n<text x=\"").Append(Number(x + 10d)).Append("\" y=\"").Append(Number(y)).Append("\" font-size=\"8.5\"><tspan font-family=\"SimSun\">")
.Append(Escape(entry.Label)).Append("</tspan></text>\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("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;").Replace("'", "&apos;"); }
}