NomrPathfinder.cs 10.4 KB
using System.Numerics;
using HaHRCS.Core.Models;
using Rcs.Domain.Entities;
using Rcs.Domain.ValueObjects;
using Rcs.Shared.Utils;

namespace Rcs.Infrastructure.Shared.Algorithms
{
    public class NomrPathfinder
    {
        private readonly Dictionary<Guid, MapNode> _nodes;
        private readonly Dictionary<Guid, MapEdge> _edges;
        private readonly Dictionary<Guid, List<MapEdge>> _adj;
        // 车辆最小转弯半径(基于地图边上的半径估计)
        private readonly double _minTurnRadius;
        // 航向量化粒度(弧度)- 降低状态数量,缓解循环
        private const double HeadingBinRad = Math.PI / 18; // 10°

        /// <summary>
        /// 倒车惩罚因子,默认 2.0。
        /// </summary>
        public double ReverseCostFactor { get; set; } = 2.0;

        public NomrPathfinder(IEnumerable<MapNode> nodes, IEnumerable<MapEdge> edges)
        {
            _nodes = nodes.ToDictionary(n => n.NodeId);
            _edges = edges.ToDictionary(e => e.EdgeId);
            _adj = edges
                .SelectMany(e => new[] { (e.FromNode, e), (e.ToNode, e) })
                .GroupBy(x => x.Item1)
                .ToDictionary(g => g.Key, g => g.Select(x => x.e).ToList());

            // 估计最小转弯半径:采用曲线边最小半径,若缺失则回退到 1.0
            _minTurnRadius = edges
                .Where(e => e.IsCurve && e.Radius.HasValue && e.Radius.Value > 0)
                .Select(e => e.Radius!.Value)
                .DefaultIfEmpty(1.0)
                .Min();
        }

        /// <summary>
        /// 从起点到终点搜索路径,起点包含初始车体方向。
        /// </summary>
        public AgvPathResult FindPath(Guid startNodeId, double startHeadingRad, Guid goalNodeId, bool isReverseParking)
        {
            var open = new PriorityQueue<State, double>();
            var cameFrom = new Dictionary<State, (State Prev, Guid EdgeId)>();
            var gScore = new Dictionary<State, double>();
            var closed = new HashSet<State>();

            var startState = new State(startNodeId, null, QuantizeHeading(startHeadingRad));
            gScore[startState] = 0;
            open.Enqueue(startState, Heuristic(startState, goalNodeId, isReverseParking));

            while (open.Count > 0)
            {
                var current = open.Dequeue();
                if (closed.Contains(current)) continue;
                closed.Add(current);

                
                if (current.NodeId == goalNodeId)
                {
                    if (!current.PrevEdgeId.HasValue)
                    {
                        // 终点约束:根据是否允许倒车,限定最后一条边的行驶模式
                        var wasReverse = _edges[current.PrevEdgeId.Value].Regress;
                        if ((isReverseParking && wasReverse) || (!isReverseParking && !wasReverse))
                        {
                            return BuildResult(current, cameFrom, gScore[current]);
                        }
                    }
                }

                if (!_adj.TryGetValue(current.NodeId, out var incident)) continue;

                var edges = incident.Where(e => e.FromNode == current.NodeId).ToList();
                foreach (var edge in edges)
                {
                    Guid nextNodeId = edge.ToNode;

                    // --- 切向约束 ---
                    // 起点第一条边:必须与起始车体方向相切(考虑倒车/前进)
                    var departVec = CraHandle.GetDepartureVector(edge, current.NodeId);
                    var startDir = new Vector2((float)Math.Cos(current.HeadingRad), (float)Math.Sin(current.HeadingRad));
                    Vector2? arrivalVec = null;
                    if (!current.PrevEdgeId.HasValue)
                    {
                        arrivalVec = new Vector2();
                        var prevEdge = _edges[current.PrevEdgeId.Value];
                        arrivalVec = CraHandle.GetArrivalVector(prevEdge, current.NodeId);
                    }

                    if (!CraHandle.IsTangentCompatible(startDir, departVec, arrivalVec, edge.Regress)) continue;



                    // 计算车体在该边上的实际航向角,考虑当前车体朝向和倒车模式
                    var nextHeading = CraHandle.GetVehicleHeadingOnEdge(edge, current.NodeId, current.HeadingRad);
                    nextHeading = QuantizeHeading(nextHeading);

                    var nextState = new State(nextNodeId, edge.EdgeId, nextHeading);

                    double stepCost = edge.Cost ?? edge.Length;
                    if (edge.Regress) stepCost *= ReverseCostFactor;

                    // 在节点处的转向代价:角度 * 最小转弯半径(近似为所需弧长)
                    double turnCost = 0;
                    if (arrivalVec.HasValue)
                    {
                        var angleTurn = CraHandle.AngleBetween(arrivalVec.Value, departVec);
                        turnCost += _minTurnRadius * angleTurn;
                    }
                    // 模式切换代价:前进 <-> 倒车 切换需要较大的机动,增加惩罚
                    if (!current.PrevEdgeId.HasValue)
                    {
                        var prevEdge = _edges[current.PrevEdgeId.Value];
                        if (prevEdge.Regress != edge.Regress)
                        {
                            turnCost += _minTurnRadius * (Math.PI / 2);
                        }
                    }

                    double tentativeG = gScore[current] + stepCost + turnCost;

                    if (!gScore.TryGetValue(nextState, out var existing) || tentativeG < existing)
                    {
                        gScore[nextState] = tentativeG;
                        cameFrom[nextState] = (current, edge.EdgeId);
                        var f = tentativeG + Heuristic(nextState, goalNodeId, isReverseParking);
                        open.Enqueue(nextState, f);
                    }
                }
            }

            return new AgvPathResult { Success = false, FailReason = "未找到符合条件的路径" };
        }

        #region 工具方法 r=1(0.293,0.707)
        private AgvPathResult BuildResult(State endState, Dictionary<State, (State Prev, Guid EdgeId)> cameFrom, double totalCost)
        {
            var edges = new List<MapEdge>();
            var nodes = new List<MapNode>();

            var rev = new List<(Guid EdgeId, Guid NodeId)>();
            var cur = endState;
            while (cameFrom.TryGetValue(cur, out var rec))
            {
                rev.Add((rec.EdgeId, cur.NodeId));
                cur = rec.Prev;
            }
            rev.Reverse();

            // 起点
            nodes.Add(_nodes[cur.NodeId]);

            foreach (var step in rev)
            {
                if (_edges.TryGetValue(step.EdgeId, out var e)) edges.Add(e);
                if (_nodes.TryGetValue(step.NodeId, out var n)) nodes.Add(n);
            }

            return new AgvPathResult
            {
                Success = true,
                Nodes = nodes,
                Edges = edges,
                TotalCost = totalCost
            };
        }

        private double Heuristic(State state, Guid goalId, bool isReverseParking)
        {
            if (!_nodes.TryGetValue(state.NodeId, out var a) || !_nodes.TryGetValue(goalId, out var b))
                return 0;

            // 位置差
            double dx = b.X - a.X;
            double dy = b.Y - a.Y;
            double euclidean = Math.Sqrt(dx * dx + dy * dy);

            // 终点朝向候选
            var goalHeadings = GetGoalHeadingCandidates(goalId, isReverseParking);
            if (goalHeadings.Count == 0)
            {
                // 若无候选,回退到节点自带朝向或仅使用欧式
                if (b.Theta.HasValue)
                {
                    goalHeadings.Add(b.Theta.Value);
                }
                else
                {
                    return euclidean;
                }
            }

            // Reeds–Shepp 启发式的保守下界:max(直线位移, R*|Δθ|) 的最小值
            double best = double.PositiveInfinity;
            foreach (var gh in goalHeadings)
            {
                var dtheta = AngleConstants.NormalizeAngleSymmetric(gh - state.HeadingRad);
                var lbTurn = _minTurnRadius * Math.Abs(dtheta);
                var rsLowerBound = Math.Max(euclidean, lbTurn);
                if (rsLowerBound < best) best = rsLowerBound;
            }
            return best;
        }

        private static double QuantizeHeading(double rad)
        {
            // 将角度标准化到 [-pi, pi] 再按 10° 量化
            var norm = AngleConstants.NormalizeAngleSymmetric(rad);
            var bins = Math.Round(norm / HeadingBinRad);
            var q = bins * HeadingBinRad;
            // 重新标准化,防止出现边界值
            return AngleConstants.NormalizeAngleSymmetric(q);
        }

        private List<double> GetGoalHeadingCandidates(Guid goalId, bool isReverseParking)
        {
            var headings = new List<double>();
            if (_adj.TryGetValue(goalId, out var incident))
            {
                // 只考虑进入 goal 的边
                foreach (var e in incident.Where(e => e.ToNode == goalId))
                {
                    var vec = CraHandle.GetArrivalVector(e, goalId);
                    var motionHeading = Math.Atan2(vec.Y, vec.X);
                    // 车辆车体朝向:若该边是倒车,则车体朝向与运动方向相反
                    var bodyHeading = e.Regress ? AngleConstants.NormalizeAngleSymmetric(motionHeading + Math.PI) : motionHeading;
                    headings.Add(bodyHeading);

                    // 如果允许倒车泊入,也考虑相反方向作为候选,以增强可行性(启发式取 min 仍保持保守)
                    if (isReverseParking)
                    {
                        headings.Add(AngleConstants.NormalizeAngleSymmetric(bodyHeading + Math.PI));
                    }
                }
            }
            return headings.Distinct()
                           .Select(AngleConstants.NormalizeAngleSymmetric)
                           .ToList();
        }

        #endregion

        #region 内部结构

        private record State(Guid NodeId, Guid? PrevEdgeId, double HeadingRad);

        #endregion
    }
}