using System; using System.Collections.Generic; using MultiWheelC.TrajectoryPlanning.CoarsePath; using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade; using MultiWheelC.TrajectoryPlanning.EMPlanner; using MultiWheelC.TrajectoryPlanning.Mapping; namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation; public sealed class TrajectoryObservationSettings { public EmPlanningScope PlanningScope { get; set; } = EmPlanningScope.FullDirectionSegment; public double MapPaddingMeters { get; set; } = 2d; public float MapResolutionMillimeters { get; set; } = 50f; public double ReplanPeriodSeconds { get; set; } = 0.20d; public double ObserverPeriodSeconds { get; set; } = 0.05d; public double SolverTimeoutSeconds { get; set; } = 5d; public int MaximumOsqpIterations { get; set; } = 100000; /// 仅滚动视窗兼容字段;完整方向段模式不用于截断。单次 ST 轨迹覆盖的未来时长,单位 s;默认 2 s,不等于观察循环周期。 public double TimeHorizonSeconds { get; set; } = 2d; /// 发布 Trajectory 相邻 TimeFromStart 时间戳的间隔,单位 s;不等于观察循环周期。 public double OutputTimeStepSeconds { get; set; } = 0.10d; public double VehicleLengthMeters { get; set; } = 0.80d; public double VehicleWidthMeters { get; set; } = 0.60d; public double SafetyMarginMeters { get; set; } = 0.05d; public double MaximumCurvaturePerMeter { get; set; } = 1d / 1.20d; public bool EnableWebVisualization { get; set; } = true; public bool AutoOpenWebVisualization { get; set; } = true; public int WebVisualizationPort { get; set; } = 0; public double WebRefreshRateHz { get; set; } = 10d; public int VisualizationHistoryCycleLimit { get; set; } = 60; public bool EnableNativePainterVisualization { get; set; } = false; public double DirectionConfirmationSpeedMetersPerSecond { get; set; } = 0.02d; public int DirectionConfirmationSamples { get; set; } = 3; public double GearSwitchProjectionToleranceMeters { get; set; } = 0.50d; public double GearSwitchStopHoldSeconds { get; set; } = 0.20d; public TrajectoryObservationSettings CreateValidatedSnapshot() { var snapshot = new TrajectoryObservationSettings { PlanningScope = PlanningScope, MapPaddingMeters = MapPaddingMeters, MapResolutionMillimeters = MapResolutionMillimeters, ReplanPeriodSeconds = ReplanPeriodSeconds, ObserverPeriodSeconds = ObserverPeriodSeconds, SolverTimeoutSeconds = SolverTimeoutSeconds, MaximumOsqpIterations = MaximumOsqpIterations, TimeHorizonSeconds = TimeHorizonSeconds, OutputTimeStepSeconds = OutputTimeStepSeconds, VehicleLengthMeters = VehicleLengthMeters, VehicleWidthMeters = VehicleWidthMeters, SafetyMarginMeters = SafetyMarginMeters, MaximumCurvaturePerMeter = MaximumCurvaturePerMeter, EnableWebVisualization = EnableWebVisualization, AutoOpenWebVisualization = AutoOpenWebVisualization, WebVisualizationPort = WebVisualizationPort, WebRefreshRateHz = WebRefreshRateHz, VisualizationHistoryCycleLimit = VisualizationHistoryCycleLimit, EnableNativePainterVisualization = EnableNativePainterVisualization, DirectionConfirmationSpeedMetersPerSecond = DirectionConfirmationSpeedMetersPerSecond, DirectionConfirmationSamples = DirectionConfirmationSamples, GearSwitchProjectionToleranceMeters = GearSwitchProjectionToleranceMeters, GearSwitchStopHoldSeconds = GearSwitchStopHoldSeconds, }; snapshot.Validate(); return snapshot; } public void Validate() { if (!Enum.IsDefined(typeof(EmPlanningScope), PlanningScope)) throw new ArgumentOutOfRangeException(nameof(PlanningScope), "Value must be a defined planning scope."); EnsurePositiveFinite(MapPaddingMeters, nameof(MapPaddingMeters)); EnsurePositiveFinite(MapResolutionMillimeters, nameof(MapResolutionMillimeters)); EnsurePositiveFinite(ReplanPeriodSeconds, nameof(ReplanPeriodSeconds)); EnsurePositiveFinite(ObserverPeriodSeconds, nameof(ObserverPeriodSeconds)); EnsurePositiveFinite(SolverTimeoutSeconds, nameof(SolverTimeoutSeconds)); if (MaximumOsqpIterations <= 0) throw new ArgumentOutOfRangeException(nameof(MaximumOsqpIterations), "Value must be positive."); EnsurePositiveFinite(TimeHorizonSeconds, nameof(TimeHorizonSeconds)); EnsurePositiveFinite(OutputTimeStepSeconds, nameof(OutputTimeStepSeconds)); if (OutputTimeStepSeconds > TimeHorizonSeconds) throw new ArgumentOutOfRangeException(nameof(OutputTimeStepSeconds), "The trajectory timestamp spacing cannot exceed the ST time horizon."); EnsurePositiveFinite(VehicleLengthMeters, nameof(VehicleLengthMeters)); EnsurePositiveFinite(VehicleWidthMeters, nameof(VehicleWidthMeters)); EnsurePositiveFinite(SafetyMarginMeters, nameof(SafetyMarginMeters)); EnsurePositiveFinite(MaximumCurvaturePerMeter, nameof(MaximumCurvaturePerMeter)); if (WebVisualizationPort != 0 && (WebVisualizationPort < 1024 || WebVisualizationPort > 65535)) throw new ArgumentOutOfRangeException(nameof(WebVisualizationPort), "Port must be zero or in the inclusive range 1024 through 65535."); EnsurePositiveFinite(WebRefreshRateHz, nameof(WebRefreshRateHz)); if (VisualizationHistoryCycleLimit <= 0) throw new ArgumentOutOfRangeException(nameof(VisualizationHistoryCycleLimit), "Value must be positive."); EnsurePositiveFinite(DirectionConfirmationSpeedMetersPerSecond, nameof(DirectionConfirmationSpeedMetersPerSecond)); if (DirectionConfirmationSamples <= 0) throw new ArgumentOutOfRangeException(nameof(DirectionConfirmationSamples), "Value must be positive."); EnsurePositiveFinite(GearSwitchProjectionToleranceMeters, nameof(GearSwitchProjectionToleranceMeters)); EnsurePositiveFinite(GearSwitchStopHoldSeconds, nameof(GearSwitchStopHoldSeconds)); } public VehicleParameters CreateVehicle() { Validate(); return new VehicleParameters { LengthMeters = VehicleLengthMeters, WidthMeters = VehicleWidthMeters, SafetyMarginMeters = SafetyMarginMeters, MaximumCurvaturePerMeter = MaximumCurvaturePerMeter, }; } private static void EnsurePositiveFinite(double value, string parameterName) { if (double.IsNaN(value) || double.IsInfinity(value) || value <= 0d) throw new ArgumentOutOfRangeException(parameterName, "Value must be finite and positive."); } } public sealed class TrajectoryObservationObstacle { private readonly bool _isCircle; private readonly double _first; private readonly double _second; private readonly double _third; private readonly double _fourth; private TrajectoryObservationObstacle(bool isCircle, double first, double second, double third, double fourth) { _isCircle = isCircle; _first = first; _second = second; _third = third; _fourth = fourth; } public static TrajectoryObservationObstacle Circle(double centerXMillimeters, double centerYMillimeters, double radiusMillimeters) { EnsureFinite(centerXMillimeters, nameof(centerXMillimeters)); EnsureFinite(centerYMillimeters, nameof(centerYMillimeters)); EnsurePositiveFinite(radiusMillimeters, nameof(radiusMillimeters)); return new TrajectoryObservationObstacle(true, centerXMillimeters, centerYMillimeters, radiusMillimeters, 0d); } public static TrajectoryObservationObstacle Rectangle(double xMinMillimeters, double xMaxMillimeters, double yMinMillimeters, double yMaxMillimeters) { EnsureFinite(xMinMillimeters, nameof(xMinMillimeters)); EnsureFinite(xMaxMillimeters, nameof(xMaxMillimeters)); EnsureFinite(yMinMillimeters, nameof(yMinMillimeters)); EnsureFinite(yMaxMillimeters, nameof(yMaxMillimeters)); if (xMaxMillimeters <= xMinMillimeters || yMaxMillimeters <= yMinMillimeters) throw new ArgumentOutOfRangeException(nameof(xMaxMillimeters), "Rectangle bounds must be non-degenerate."); return new TrajectoryObservationObstacle(false, xMinMillimeters, xMaxMillimeters, yMinMillimeters, yMaxMillimeters); } public MapBoundsMm GetBounds() { return _isCircle ? new MapBoundsMm(ToFiniteFloat(_first - _third), ToFiniteFloat(_first + _third), ToFiniteFloat(_second - _third), ToFiniteFloat(_second + _third)) : new MapBoundsMm(ToFiniteFloat(_first), ToFiniteFloat(_second), ToFiniteFloat(_third), ToFiniteFloat(_fourth)); } public IMapObstacle ToMapObstacle() { return _isCircle ? new CircleObstacle(ToFiniteFloat(_first), ToFiniteFloat(_second), ToFiniteFloat(_third)) : new AxisAlignedRectangleObstacle(ToFiniteFloat(_first), ToFiniteFloat(_second), ToFiniteFloat(_third), ToFiniteFloat(_fourth)); } private static float ToFiniteFloat(double value) { if (double.IsNaN(value) || double.IsInfinity(value) || value < float.MinValue || value > float.MaxValue) throw new ArgumentOutOfRangeException(nameof(value), "Value cannot be represented as a finite millimeter coordinate."); return (float)value; } private static void EnsureFinite(double value, string parameterName) { if (double.IsNaN(value) || double.IsInfinity(value)) throw new ArgumentOutOfRangeException(parameterName, "Value must be finite."); } private static void EnsurePositiveFinite(double value, string parameterName) { EnsureFinite(value, parameterName); if (value <= 0d) throw new ArgumentOutOfRangeException(parameterName, "Value must be positive."); } } public static class TrajectoryObservationSetupFactory { public static CoarsePathPlanningJob CreateBootstrapJob(Pose2D start, Pose2D goal, TrajectoryObservationSettings settings, IReadOnlyList obstacles, long obstacleSnapshotVersion) { if (start == null) throw new ArgumentNullException(nameof(start)); if (goal == null) throw new ArgumentNullException(nameof(goal)); if (settings == null) throw new ArgumentNullException(nameof(settings)); if (obstacles == null) throw new ArgumentNullException(nameof(obstacles)); ValidatePose(start, nameof(start)); ValidatePose(goal, nameof(goal)); settings.Validate(); double padMm = settings.MapPaddingMeters * 1000d; var bounds = new MapBoundsMm( ToGridLower(Math.Min(start.X, goal.X) * 1000d - padMm, settings.MapResolutionMillimeters), ToGridUpper(Math.Max(start.X, goal.X) * 1000d + padMm, settings.MapResolutionMillimeters), ToGridLower(Math.Min(start.Y, goal.Y) * 1000d - padMm, settings.MapResolutionMillimeters), ToGridUpper(Math.Max(start.Y, goal.Y) * 1000d + padMm, settings.MapResolutionMillimeters)); var mapObstacles = new List(obstacles.Count); for (int index = 0; index < obstacles.Count; index++) { TrajectoryObservationObstacle obstacle = obstacles[index] ?? throw new ArgumentNullException(nameof(obstacles)); MapBoundsMm obstacleBounds = obstacle.GetBounds(); if (obstacleBounds.XMin < bounds.XMin || obstacleBounds.XMax > bounds.XMax || obstacleBounds.YMin < bounds.YMin || obstacleBounds.YMax > bounds.YMax) throw new ArgumentOutOfRangeException(nameof(obstacles), "Obstacle envelope must fit within map bounds."); mapObstacles.Add(obstacle.ToMapObstacle()); } IReadOnlyList sources; bool allowExplicitEmptyMap = mapObstacles.Count == 0; if (allowExplicitEmptyMap) sources = Array.Empty(); else { if (obstacleSnapshotVersion <= 0L) throw new ArgumentOutOfRangeException(nameof(obstacleSnapshotVersion), "Obstacle snapshots require a positive version."); sources = new IMapObstacleSource[] { new ManualObstacleSource("trajectory-observer-manual", obstacleSnapshotVersion, true, mapObstacles), }; } return new CoarsePathPlanningJob { MapRequest = new PlanningMapRequest { Bounds = bounds, ResolutionMm = settings.MapResolutionMillimeters, ObstacleSources = sources, AllowExplicitEmptyMap = allowExplicitEmptyMap, }, Start = start, Goal = goal, Vehicle = settings.CreateVehicle(), Configuration = new HybridAStarConfiguration(), StartDirection = null, GoalDirection = GoalDirectionConstraint.Any, }; } private static float ToGridLower(double millimeters, float resolutionMm) { return ToFiniteFloat(Math.Floor(millimeters / resolutionMm) * resolutionMm, nameof(millimeters)); } private static float ToGridUpper(double millimeters, float resolutionMm) { return ToFiniteFloat(Math.Ceiling(millimeters / resolutionMm) * resolutionMm, nameof(millimeters)); } private static float ToFiniteFloat(double value, string parameterName) { if (double.IsNaN(value) || double.IsInfinity(value) || value < float.MinValue || value > float.MaxValue) throw new ArgumentOutOfRangeException(parameterName, "Value cannot be represented as a finite millimeter coordinate."); return (float)value; } private static void ValidatePose(Pose2D pose, string parameterName) { if (double.IsNaN(pose.X) || double.IsInfinity(pose.X) || double.IsNaN(pose.Y) || double.IsInfinity(pose.Y) || double.IsNaN(pose.Heading) || double.IsInfinity(pose.Heading)) throw new ArgumentOutOfRangeException(parameterName, "Pose values must be finite."); } }