190 lines
14 KiB
C#
190 lines
14 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
|
|
namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.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=\"1.35\"/>\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("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """).Replace("'", "'"); }
|
|
}
|