using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Drawing; using System.Globalization; using ClumsyCore; using ClumsyCore.DTools; using MDCSToolBox.Clumsy.Pilot; using MultiWheelC.TrajectoryPlanning.CoarsePath; using MultiWheelC.TrajectoryPlanning.EMPlanner; using MultiWheelC.TrajectoryPlanning.Mapping; using MultiWheelC.TrajectoryPlanning.PathSmoothing; namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; public static class TrajectoryObservationPresentationText { public static string Create(TrajectoryObservationObservation observation, TrajectoryObservationCharts charts) { string projectionFailures = charts == null ? "unavailable" : charts.FailedProjectionCount.ToString(CultureInfo.InvariantCulture); if (observation == null || observation.SelectedPoint == null) { return "OBSERVE_ONLY: no chassis command is sent.\n" + "selected trajectory point unavailable\n" + "LS projection failures=" + projectionFailures; } EmTrajectoryPoint point = observation.SelectedPoint; return "OBSERVE_ONLY: no chassis command is sent.\n" + "selected t=" + point.TimeFromStart.ToString("F2", CultureInfo.InvariantCulture) + " s, path-S=" + point.PathS.ToString("F2", CultureInfo.InvariantCulture) + " m\n" + "signed speed=" + point.SignedLongitudinalVelocity.ToString("F2", CultureInfo.InvariantCulture) + " m/s, yaw rate=" + point.YawRate.ToString("F2", CultureInfo.InvariantCulture) + " rad/s\n" + "LS projection failures=" + projectionFailures; } } public sealed class TrajectoryObservationLsPresentationPoint { public TrajectoryObservationLsPresentationPoint(double horizontalPathS, double verticalLateralOffset) { HorizontalPathS = horizontalPathS; VerticalLateralOffset = verticalLateralOffset; } public double HorizontalPathS { get; } public double VerticalLateralOffset { get; } } public sealed class TrajectoryObservationLsPresentationModel { private TrajectoryObservationLsPresentationModel(IReadOnlyList samples) { Samples = new ReadOnlyCollection( new List(samples)); } public string HorizontalAxisLabel => "ReferenceS (m)"; public string VerticalAxisLabel => "l (m)"; public IReadOnlyList Samples { get; } public static TrajectoryObservationLsPresentationModel Create(TrajectoryObservationCharts charts) { if (charts == null) throw new ArgumentNullException(nameof(charts)); var samples = new List(charts.LsSamples.Count); for (int index = 0; index < charts.LsSamples.Count; index++) { TrajectoryObservationLsSample sample = charts.LsSamples[index]; samples.Add(new TrajectoryObservationLsPresentationPoint(sample.PathS, sample.LateralOffset)); } return new TrajectoryObservationLsPresentationModel(samples); } } public sealed class TrajectoryObservationPresentation { private const float MillimetersPerMeter = 1000f; private const int MaximumVisibleGridLines = 80; private const float VelocitySeriesBaseMillimeters = -2500f; private readonly Painter worldPainter = UI.GetPainter("TrajectoryObserver.World", true); private readonly Painter lsPainter = UI.GetPainter("TrajectoryObserver.LS", true); private readonly Painter stPainter = UI.GetPainter("TrajectoryObserver.ST", true); public void DrawWorld(TrajectoryObservationBootstrapResult bootstrap, TrajectoryObservationObservation observation, TrajectoryObservationRuntimeState runtimeState, string diagnosticText) { worldPainter.Clear(); if (bootstrap == null) { worldPainter.DrawText(Color.LightYellow, "Trajectory bootstrap unavailable.", 0f, 0f); return; } VehicleParameters vehicle = bootstrap.Vehicle; DrawMap(bootstrap.Map); DrawPose(bootstrap.Job == null ? null : bootstrap.Job.Start, vehicle, Color.LimeGreen, "计划起点"); DrawPose(bootstrap.Job == null ? null : bootstrap.Job.Goal, vehicle, Color.Orange, "终点"); DrawCoarsePath(bootstrap.CoarseResult == null ? null : bootstrap.CoarseResult.PlanningResult.Path); DrawLocalG2Path(bootstrap.SmoothedPath == null ? null : bootstrap.SmoothedPath.Path); DrawPose(observation == null || observation.VehicleState == null ? null : observation.VehicleState.Pose, vehicle, Color.DeepSkyBlue, "车辆"); if (runtimeState != null && runtimeState.WorldNotice.Length > 0) { float noticeX = bootstrap.Map == null ? 0f : bootstrap.Map.Bounds.XMin + 100f; float noticeY = bootstrap.Map == null ? 0f : bootstrap.Map.Bounds.YMax - 200f; worldPainter.DrawText(Color.OrangeRed, runtimeState.WorldNotice, noticeX, noticeY); } EmTrajectory trajectory = observation == null ? null : observation.PublishedTrajectory; if (trajectory == null || trajectory.Points == null || trajectory.Points.Count == 0) { float diagnosticX = bootstrap.Map == null ? 0f : bootstrap.Map.Bounds.XMin + 100f; float diagnosticY = bootstrap.Map == null ? 0f : bootstrap.Map.Bounds.YMin + 300f; DrawBoundaryMarkers(bootstrap.Segments); worldPainter.DrawText(Color.LightYellow, EmptyChartMessage("No published trajectory available.\n", diagnosticText), diagnosticX, diagnosticY); return; } DrawEmPath(trajectory.Points); DrawBoundaryMarkers(bootstrap.Segments); } public void DrawLs(TrajectoryObservationCharts charts, string diagnosticText) { lsPainter.Clear(); if (charts == null || charts.LsSamples == null || charts.LsSamples.Count == 0) { lsPainter.DrawText(Color.LightYellow, EmptyChartMessage("No published trajectory available for L-S chart.\n", diagnosticText), 0f, 0f); return; } TrajectoryObservationLsPresentationModel model = TrajectoryObservationLsPresentationModel.Create(charts); float maxS = MaximumPathS(charts.LsSamples); float maxL = MaximumLateralOffset(charts.LsSamples); lsPainter.DrawLine(Color.Gainsboro, 0f, 0f, maxS, 0f, width: 2); lsPainter.DrawLine(Color.Gainsboro, 0f, -maxL, 0f, maxL, width: 2); lsPainter.DrawText(Color.White, "S-L", maxS + 100f, maxL + 150f); lsPainter.DrawText(Color.White, model.HorizontalAxisLabel, maxS + 100f, -200f); lsPainter.DrawText(Color.White, model.VerticalAxisLabel, 100f, maxL + 300f); for (int index = 1; index < model.Samples.Count; index++) { TrajectoryObservationLsPresentationPoint previous = model.Samples[index - 1]; TrajectoryObservationLsPresentationPoint current = model.Samples[index]; lsPainter.DrawLine(Color.MediumPurple, ToMillimeters(previous.HorizontalPathS), ToMillimeters(previous.VerticalLateralOffset), ToMillimeters(current.HorizontalPathS), ToMillimeters(current.VerticalLateralOffset), width: 3); } } public void DrawSt(TrajectoryObservationCharts charts, string diagnosticText) { stPainter.Clear(); if (charts == null || charts.StSamples == null || charts.StSamples.Count == 0) { stPainter.DrawText(Color.LightYellow, EmptyChartMessage("No published trajectory available for T-S/T-V charts.\n", diagnosticText), 0f, 0f); return; } float maxTime = MaximumTime(charts.StSamples, charts.SpeedSamples); float maxS = MaximumPathS(charts.StSamples); stPainter.DrawLine(Color.Gainsboro, 0f, 0f, maxTime, 0f, width: 2); stPainter.DrawLine(Color.Gainsboro, 0f, 0f, 0f, maxS, width: 2); stPainter.DrawText(Color.White, "T-S", 0f, maxS + 150f); stPainter.DrawText(Color.White, "t (s)", maxTime + 100f, -200f); stPainter.DrawText(Color.White, "PathS (m)", 100f, maxS + 300f); DrawStSeries(charts.StSamples); stPainter.DrawLine(Color.Gainsboro, 0f, VelocitySeriesBaseMillimeters, maxTime, VelocitySeriesBaseMillimeters, width: 2); stPainter.DrawLine(Color.Gainsboro, 0f, VelocitySeriesBaseMillimeters, 0f, VelocitySeriesBaseMillimeters + MillimetersPerMeter, width: 2); stPainter.DrawText(Color.White, "T-V", 0f, VelocitySeriesBaseMillimeters - 200f); DrawSpeedSeries(charts.SpeedSamples); DrawStLegend(maxTime, maxS); } public void ClearAll() { worldPainter.Clear(); lsPainter.Clear(); stPainter.Clear(); } private static string EmptyChartMessage(string label, string diagnosticText) { return string.IsNullOrWhiteSpace(diagnosticText) ? label : label + diagnosticText; } private void DrawMap(PlanningGridMap map) { if (map == null) return; float xMin = map.Bounds.XMin; float xMax = map.Bounds.XMax; float yMin = map.Bounds.YMin; float yMax = map.Bounds.YMax; int gridStride = Math.Max(1, (int)Math.Ceiling(Math.Max(map.Rows, map.Cols) / (double)MaximumVisibleGridLines)); for (int column = 0; column <= map.Cols; column += gridStride) { float x = Math.Min(xMax, xMin + column * map.ResolutionMm); worldPainter.DrawLine(Color.FromArgb(80, Color.SlateGray), x, yMin, x, yMax, width: 1); } for (int row = 0; row <= map.Rows; row += gridStride) { float y = Math.Min(yMax, yMin + row * map.ResolutionMm); worldPainter.DrawLine(Color.FromArgb(80, Color.SlateGray), xMin, y, xMax, y, width: 1); } DrawRectangle(Color.Gainsboro, xMin, yMin, xMax, yMax, 3); for (int row = 0; row < map.Rows; row++) { for (int column = 0; column < map.Cols; column++) { if (!map.IsOccupied(row, column)) continue; float x = xMin + column * map.ResolutionMm + map.ResolutionMm / 2f; float y = yMin + row * map.ResolutionMm; worldPainter.DrawLine(Color.FromArgb(150, Color.Firebrick), x, y, x, y + map.ResolutionMm, width: Math.Max(1, (int)Math.Round(map.ResolutionMm))); } } } private void DrawPose(Pose2D pose, VehicleParameters vehicle, Color color, string label) { if (pose == null) return; double lengthMeters = vehicle == null || vehicle.LengthMeters <= 0d ? 0.8d : vehicle.LengthMeters; double widthMeters = vehicle == null || vehicle.WidthMeters <= 0d ? 0.6d : vehicle.WidthMeters; double heading = pose.Heading; double cosHeading = Math.Cos(heading); double sinHeading = Math.Sin(heading); float x = ToMillimeters(pose.X); float y = ToMillimeters(pose.Y); float halfLengthMillimeters = (float)(lengthMeters * MillimetersPerMeter / 2d); float halfWidthMillimeters = (float)(widthMeters * MillimetersPerMeter / 2d); float frontLeftX = x + (float)(cosHeading * halfLengthMillimeters - sinHeading * halfWidthMillimeters); float frontLeftY = y + (float)(sinHeading * halfLengthMillimeters + cosHeading * halfWidthMillimeters); float frontRightX = x + (float)(cosHeading * halfLengthMillimeters + sinHeading * halfWidthMillimeters); float frontRightY = y + (float)(sinHeading * halfLengthMillimeters - cosHeading * halfWidthMillimeters); float rearRightX = x + (float)(-cosHeading * halfLengthMillimeters + sinHeading * halfWidthMillimeters); float rearRightY = y + (float)(-sinHeading * halfLengthMillimeters - cosHeading * halfWidthMillimeters); float rearLeftX = x + (float)(-cosHeading * halfLengthMillimeters - sinHeading * halfWidthMillimeters); float rearLeftY = y + (float)(-sinHeading * halfLengthMillimeters + cosHeading * halfWidthMillimeters); DrawVehicleOutline(frontLeftX, frontLeftY, frontRightX, frontRightY, rearRightX, rearRightY, rearLeftX, rearLeftY, color); float headingLengthMillimeters = (float)(lengthMeters * MillimetersPerMeter * 0.25d); worldPainter.DrawLine(color, x, y, x + (float)cosHeading * headingLengthMillimeters, y + (float)sinHeading * headingLengthMillimeters, endArrow: false, width: 1); worldPainter.DrawText(color, label, x + 100f, y + 100f); } private void DrawVehicleOutline(float frontLeftX, float frontLeftY, float frontRightX, float frontRightY, float rearRightX, float rearRightY, float rearLeftX, float rearLeftY, Color color) { worldPainter.DrawLine(color, frontLeftX, frontLeftY, frontRightX, frontRightY, width: 1); worldPainter.DrawLine(color, frontRightX, frontRightY, rearRightX, rearRightY, width: 1); worldPainter.DrawLine(color, rearRightX, rearRightY, rearLeftX, rearLeftY, width: 1); worldPainter.DrawLine(color, rearLeftX, rearLeftY, frontLeftX, frontLeftY, width: 1); } private void DrawBoundaryMarkers(IReadOnlyList segments) { if (segments == null) return; for (int index = 0; index < segments.Count; index++) { DirectionSegmentView segment = segments[index]; if (segment.Points.Count == 0) continue; var point = segment.Points[segment.Points.Count - 1]; if (segment.EndBoundary.BoundaryType == EmBoundaryType.GearSwitchApproach) { DrawDiamondMarker(point, Color.Orange, "换向点 " + (segment.SegmentIndex + 1).ToString(CultureInfo.InvariantCulture) + " / s_end"); } else if (segment.EndBoundary.BoundaryType == EmBoundaryType.Goal) { DrawCrossMarker(point, Color.OrangeRed, "终点 / s_end"); } } } private void DrawDiamondMarker(SmoothedPathPoint point, Color color, string label) { float x = ToMillimeters(point.X); float y = ToMillimeters(point.Y); const float radius = 50f; worldPainter.DrawLine(color, x, y + radius, x + radius, y, width: 1); worldPainter.DrawLine(color, x + radius, y, x, y - radius, width: 1); worldPainter.DrawLine(color, x, y - radius, x - radius, y, width: 1); worldPainter.DrawLine(color, x - radius, y, x, y + radius, width: 1); worldPainter.DrawText(color, label, x + 100f, y + 100f); } private void DrawCrossMarker(SmoothedPathPoint point, Color color, string label) { float x = ToMillimeters(point.X); float y = ToMillimeters(point.Y); const float radius = 50f; worldPainter.DrawLine(color, x - radius, y - radius, x + radius, y + radius, width: 1); worldPainter.DrawLine(color, x - radius, y + radius, x + radius, y - radius, width: 1); worldPainter.DrawText(color, label, x + 100f, y + 100f); } private void DrawCoarsePath(IReadOnlyList path) { if (path == null || path.Count == 0) return; for (int index = 1; index < path.Count; index++) worldPainter.DrawLine(Color.LimeGreen, ToMillimeters(path[index - 1].X), ToMillimeters(path[index - 1].Y), ToMillimeters(path[index].X), ToMillimeters(path[index].Y), width: 2); } private void DrawLocalG2Path(IReadOnlyList path) { if (path == null || path.Count == 0) return; for (int index = 1; index < path.Count; index++) worldPainter.DrawLine(Color.Gold, ToMillimeters(path[index - 1].X), ToMillimeters(path[index - 1].Y), ToMillimeters(path[index].X), ToMillimeters(path[index].Y), width: 1); } private void DrawEmPath(IReadOnlyList path) { for (int index = 1; index < path.Count; index++) worldPainter.DrawLine(Color.DeepPink, ToMillimeters(path[index - 1].X), ToMillimeters(path[index - 1].Y), ToMillimeters(path[index].X), ToMillimeters(path[index].Y), width: 2); } private void DrawRectangle(Color color, float xMin, float yMin, float xMax, float yMax, int width) { worldPainter.DrawLine(color, xMin, yMin, xMax, yMin, width: width); worldPainter.DrawLine(color, xMax, yMin, xMax, yMax, width: width); worldPainter.DrawLine(color, xMax, yMax, xMin, yMax, width: width); worldPainter.DrawLine(color, xMin, yMax, xMin, yMin, width: width); } private void DrawStSeries(IReadOnlyList samples) { for (int index = 1; index < samples.Count; index++) stPainter.DrawLine(Color.Cyan, ToMillimeters(samples[index - 1].TimeFromStart), ToMillimeters(samples[index - 1].PathS), ToMillimeters(samples[index].TimeFromStart), ToMillimeters(samples[index].PathS), width: 3); } private void DrawSpeedSeries(IReadOnlyList samples) { if (samples == null) return; for (int index = 1; index < samples.Count; index++) stPainter.DrawLine(Color.Orange, ToMillimeters(samples[index - 1].TimeFromStart), VelocitySeriesBaseMillimeters + ToMillimeters(samples[index - 1].SignedLongitudinalVelocity), ToMillimeters(samples[index].TimeFromStart), VelocitySeriesBaseMillimeters + ToMillimeters(samples[index].SignedLongitudinalVelocity), width: 3); } private void DrawStLegend(float maxTime, float maxS) { float x = maxTime + 250f; stPainter.DrawLine(Color.Cyan, x, maxS, x + 200f, maxS, width: 4); stPainter.DrawText(Color.Cyan, "t-s", x + 250f, maxS - 50f); stPainter.DrawLine(Color.Orange, x, VelocitySeriesBaseMillimeters, x + 200f, VelocitySeriesBaseMillimeters, width: 4); stPainter.DrawText(Color.Orange, "t-v", x + 250f, VelocitySeriesBaseMillimeters - 50f); } private static float MaximumPathS(IReadOnlyList samples) { double maximum = 1d; for (int index = 0; index < samples.Count; index++) maximum = Math.Max(maximum, samples[index].PathS); return ToMillimeters(maximum); } private static float MaximumPathS(IReadOnlyList samples) { double maximum = 1d; for (int index = 0; index < samples.Count; index++) maximum = Math.Max(maximum, samples[index].PathS); return ToMillimeters(maximum); } private static float MaximumLateralOffset(IReadOnlyList samples) { double maximum = 1d; for (int index = 0; index < samples.Count; index++) maximum = Math.Max(maximum, Math.Abs(samples[index].LateralOffset)); return ToMillimeters(maximum); } private static float MaximumTime(IReadOnlyList stSamples, IReadOnlyList speedSamples) { double maximum = 1d; for (int index = 0; index < stSamples.Count; index++) maximum = Math.Max(maximum, stSamples[index].TimeFromStart); if (speedSamples != null) for (int index = 0; index < speedSamples.Count; index++) maximum = Math.Max(maximum, speedSamples[index].TimeFromStart); return ToMillimeters(maximum); } private static float ToMillimeters(double meters) => (float)(meters * MillimetersPerMeter); }