AStarPathFinder.cs 26.4 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using Rcs.Application.Services;
using Rcs.Application.Services.PathFind.Models;
using Rcs.Domain.ValueObjects;
using Rcs.Shared.Utils;

namespace Rcs.Infrastructure.PathFinding.Services
{
    /// <summary>
    /// A*寻路算法实现(考虑全局路径冲突、倒车约束)
    /// @author zzy
    /// </summary>
    public class AStarPathFinder
    {
        private readonly ILogger<AStarPathFinder> _logger;
        private readonly AStarPathFinder _pathFinder;

        /// <summary>
        /// 同向冲突代价
        /// </summary>
        private const double SameDirectionCost = 5.0;

        /// <summary>
        /// 对向冲突代价(死锁风险)
        /// </summary>
        private const double OppositeDirectionCost = 100.0;

        public AStarPathFinder(ILogger<AStarPathFinder> logger)
        {
            _logger = logger;
            
        }

        /// <summary>
        /// 计算最优路径(考虑全局已规划路径)
        /// @author zzy
        /// 2026-02-07 更新:支持起点等于终点时返回原地路径
        /// </summary>
        public PathResult FindPath(PathRequest request, PathGraph graph, GlobalPathContext? globalContext = null)
        {
            var sw = System.Diagnostics.Stopwatch.StartNew();
            var result = new PathResult();

            if (!graph.Nodes.TryGetValue(request.StartNodeId, out var startNode))
            {
                result.ErrorMessage = "起始节点不存在";
                return result;
            }

            if (!graph.Nodes.TryGetValue(request.EndNodeId, out var endNode))
            {
                result.ErrorMessage = "目标节点不存在";
                return result;
            }

            // 计算终点要求的车头朝向(弧度)
            // 货叉取货方向需要和终点朝向相反,车头方向 = 终点朝向 + π - 货叉夹角
            List<double> requiredEndAngles = new List<double>();
            if (request.ForkRadOffsets != null && request.ForkRadOffsets.Count > 0)
            {
                foreach(var angle in request.ForkRadOffsets)
                {
                    requiredEndAngles.Add(request.RequiredEndRad.Value + Math.PI - angle);
                }
            }

            // 特殊情况:起点等于终点
            // 检查当前朝向是否满足终点约束,如果满足则返回原地路径
            if (request.StartNodeId == request.EndNodeId)
            {
                var currentTheta = AngleConverter.NormalizeRadians(request.CurrentTheta);
                bool angleMatched = true;

                // 如果有终点朝向约束,检查当前朝向是否满足
                if (requiredEndAngles.Count > 0)
                {
                    angleMatched = false;
                    foreach (var requiredAngle in requiredEndAngles)
                    {
                        var diff = Math.Abs(AngleConverter.NormalizeRadians(currentTheta - requiredAngle));
                        if (diff <= AngleConstants.Degrees15)
                        {
                            angleMatched = true;
                            break;
                        }
                    }
                }
                // 情况1:朝向已满足约束,直接返回原地路径
                // 情况2:朝向不满足但终点允许旋转,返回原地路径(车辆可原地旋转调整朝向)
                if (angleMatched || startNode.AllowRotate)
                {
                    result.Success = true;
                    // 返回包含一个原地路径段的列表(起点到起点,长度为0)
                    result.Segments = new List<PathSegment>
                    {
                        new PathSegment
                        {
                            EdgeId = Guid.Empty,  // 无实际边
                            FromNodeId = request.StartNodeId,
                            ToNodeId = request.StartNodeId,
                            Angle = currentTheta,
                            Length = 0,
                            MaxSpeed = null
                        }
                    };
                    result.TotalLength = 0;
                    result.TotalCost = 0;
                    sw.Stop();
                    result.ComputeTimeMs = sw.ElapsedMilliseconds;

                    if (angleMatched)
                    {
                        _logger.LogInformation("起点等于终点且朝向满足约束,返回原地路径: StartNodeId={StartNodeId}", request.StartNodeId);
                    }
                    else
                    {
                        _logger.LogInformation("起点等于终点,朝向不满足但节点允许旋转,返回原地路径: StartNodeId={StartNodeId}, AllowRotate={AllowRotate}",
                            request.StartNodeId, startNode.AllowRotate);
                    }
                    return result;
                }
                // 朝向不满足且不允许旋转,继续正常寻路(可能需要绕一圈调整朝向)
                //_logger.LogInformation("起点等于终点但朝向不满足约束且不允许旋转,尝试寻找调整朝向的路径: StartNodeId={StartNodeId}", request.StartNodeId);
            }

            var openSet = new PriorityQueue<SearchNode, double>();
            var closedSet = new HashSet<Guid>();
            var gScore = new Dictionary<Guid, double> { [request.StartNodeId] = 0 };
            var cameFrom = new Dictionary<Guid, (Guid nodeId, PathEdge edge, double Angle)>();

            openSet.Enqueue(new SearchNode
            {
                NodeId = request.StartNodeId,
                currentTheta = AngleConverter.NormalizeRadians(request.CurrentTheta),
                GScore = 0,
                FScore = Heuristic(startNode, endNode)
            }, Heuristic(startNode, endNode));

            while (openSet.Count > 0)
            {
                var current = openSet.Dequeue();

                // 终点约束
                if (current.NodeId == request.EndNodeId)
                {
                    // 检查终点朝向约束(允许 AngleConstants.Degrees15 的偏移)
                    // 如果终点允许旋转,则跳过朝向检查(车辆可原地旋转调整)
                    bool canAcceptEndpoint = true;

                    if (requiredEndAngles.Count > 0 && !endNode.AllowRotate)
                    {
                        bool angleMatched = false;

                        foreach (var requiredAngle in requiredEndAngles)
                        {
                            // 计算角度差,使用 AngleConstants.Degrees15(弧度)作为容差
                            var diff = Math.Abs(AngleConverter.NormalizeRadians(
                                current.currentTheta - requiredAngle));
                            if (diff <= AngleConstants.Degrees15)
                            {
                                angleMatched = true;
                                break;
                            }
                        }

                        if (!angleMatched)
                        {
                            // 朝向不满足且终点不允许旋转,继续搜索其他路径
                            canAcceptEndpoint = false;
                        }
                    }

                    if (canAcceptEndpoint)
                    {
                        result.Success = true;
                        result.Segments = ReconstructPath(cameFrom, request.EndNodeId);
                        result.TotalLength = result.Segments.Sum(s => s.Length);
                        result.TotalCost = gScore[request.EndNodeId];
                        break;
                    }
                    else
                    {
                        // 朝向不满足,继续搜索其他路径
                        continue;
                    }
                }

                if (closedSet.Contains(current.NodeId)) continue;
                closedSet.Add(current.NodeId);

                var currentNode = graph.Nodes[current.NodeId];

                foreach (var edge in currentNode.OutEdges)
                {
                    if (!edge.Active) continue;
                    var neighborId = edge.ToNodeId;

                    if (!graph.Nodes.TryGetValue(neighborId, out var neighborNode)) continue;
                    if (!neighborNode.Active) continue;

                    // 计算边支持方向和车辆能力集方向的交集,并找到当前航向角±偏差范围内最接近的行驶方向
                    var (selectedRad, needsRotation) = SelectBestTraversalRad(
                        edge.OrientationRads, request.VehicleOrientationAngles,
                        current.currentTheta, edge.StartTangentRad, edge.MaxRadDeviation);

                    // 无交集则跳过
                    if (!selectedRad.HasValue) continue;

                    // 有交集但需要旋转,检查起点是否支持旋转
                    if (needsRotation && !currentNode.AllowRotate) continue;

                    // 计算全局路径冲突代价(传入当前累计代价用于时间窗口分析)
                    // 这里用累计代价近似估算时间,假设代价与距离正相关
                    var conflictCost = CalculateConflictCost(edge, globalContext, request.RobotId, gScore[current.NodeId]);

                    // 计算终点车辆全局航向角(弧度)
                    var newAngle = selectedRad.Value + edge.DirectionRad;

                    var tentativeG = gScore[current.NodeId] + edge.Cost + conflictCost;

                    if (!gScore.TryGetValue(neighborId, out var existingG) || tentativeG < existingG)
                    {
                        gScore[neighborId] = tentativeG;
                        cameFrom[neighborId] = (current.NodeId, edge, selectedRad.Value);

                        var fScore = tentativeG + Heuristic(neighborNode, endNode);

                        openSet.Enqueue(new SearchNode
                        {
                            NodeId = neighborId,
                            currentTheta = AngleConverter.NormalizeRadians(newAngle) ,
                            GScore = tentativeG,
                            FScore = fScore
                        }, fScore);
                    }
                }
            }

            if (!result.Success)
            {
                result.ErrorMessage = "无法找到可行路径";
            }

            sw.Stop();
            result.ComputeTimeMs = sw.ElapsedMilliseconds;
            return result;
        }

        /// <summary>
        /// 计算全局路径冲突代价(基于时间窗口分析,支持跨地图冲突检测)
        /// @author zzy
        /// </summary>
        /// <param name="edge">当前要评估的边</param>
        /// <param name="context">全局路径上下文</param>
        /// <param name="currentRobotId">当前机器人ID</param>
        /// <param name="accumulatedCost">从起点到当前边起点的累计代价(注意:是代价不是距离)</param>
        /// <returns>冲突代价</returns>
        /// <remarks>
        /// 注意:此方法使用累计代价近似估算到达时间。
        /// 如果 edge.Cost ≈ edge.Length,则估算较为准确;
        /// 如果代价包含大量非距离因素,估算会有偏差,但仍可用于相对排序。
        /// </remarks>
        private double CalculateConflictCost(PathEdge edge, GlobalPathContext? context, Guid currentRobotId, double accumulatedCost)
        {
            if (context == null) return 0;

            // 获取当前机器人的平均速度,默认1.0米/秒
            double myAverageSpeed = context.RobotAverageSpeeds.TryGetValue(currentRobotId, out var speed)
                ? speed
                : 1.0;

            // 如果速度为0或负数,使用默认值
            if (myAverageSpeed <= 0) myAverageSpeed = 1.0;

            // 估算到达该边入口的时间(使用累计代价近似距离)
            // T_arrival ≈ AccumulatedCost / V_me
            // 注意:如果代价与距离不成正比,此估算会有偏差
            double tArrival = accumulatedCost / myAverageSpeed;

            double cost = 0;
            foreach (var (robotId, plannedEdges) in context.RobotPlannedEdges)
            {
                if (robotId == currentRobotId) continue;

                foreach (var planned in plannedEdges)
                {
                    // 检查冲突:优先使用 NodeCode 匹配(跨地图),否则使用 NodeId 匹配(同地图)
                    var conflictType = DetectEdgeConflict(edge, planned);

                    if (conflictType == EdgeConflictType.SameDirection)
                    {
                        // 同向冲突:两车走同一条边
                        // 计算前车离开该边的时间
                        double tLeave = CalculateTimeToLeaveEdge(planned);

                        // 安全跟车时间(秒)- 同向跟车需要更短的安全距离
                        const double followSafetyBuffer = 3.0;

                        // 等我到了,前车早走了
                        if (tArrival > tLeave + followSafetyBuffer)
                        {
                            // 无冲突,代价为1
                            cost += 1;
                        }
                        // 我会追上前车或紧跟其后
                        else if (tArrival <= tLeave)
                        {
                            // 需要跟车,给予较小惩罚代价
                            cost += SameDirectionCost;
                        }
                        // 时间窗口接近,可能需要减速跟车
                        else
                        {
                            // 代价与时间差成反比:时间差越小,代价越高
                            double timeDiff = tArrival - tLeave;
                            double followCost = SameDirectionCost * (1 - timeDiff / followSafetyBuffer);
                            cost += followCost;
                        }
                    }
                    else if (conflictType == EdgeConflictType.OppositeDirection)
                    {
                        // 对向冲突:逆向车在该边上
                        // 计算逆向车离开该边的时间
                        double tLeave = CalculateTimeToLeaveEdge(planned);

                        // 安全缓冲时间(秒)
                        const double safetyBuffer = 5.0;

                        if (tArrival > tLeave + safetyBuffer)
                        {
                            cost += SameDirectionCost;
                        }
                        else if (tArrival <= tLeave)
                        {
                            // 真正的死锁风险,较大代价
                            cost += OppositeDirectionCost;
                        }
                        // 时间窗口重叠但有安全缓冲(T_leave < T_arrival <= T_leave + 安全缓冲)
                        else
                        {
                            // 可能存在冲突,给予较高代价但不是无穷大
                            // 代价与时间差成反比:时间差越小,代价越高
                            double timeDiff = tArrival - tLeave;
                            double conflictCost = OppositeDirectionCost * (1 - timeDiff / safetyBuffer);
                            cost += conflictCost;
                        }
                    }
                }
            }
            return cost;
        }

        /// <summary>
        /// 检测两条边的冲突类型(支持跨地图检测)
        /// @author zzy
        /// </summary>
        /// <param name="currentEdge">当前要评估的边</param>
        /// <param name="plannedEdge">其他机器人已规划的边</param>
        /// <returns>冲突类型</returns>
        private static EdgeConflictType DetectEdgeConflict(PathEdge currentEdge, PlannedEdge plannedEdge)
        {
            // 优先使用 NodeCode 进行匹配(跨地图场景)
            bool hasNodeCodes = !string.IsNullOrEmpty(currentEdge.FromNodeCode) &&
                                !string.IsNullOrEmpty(currentEdge.ToNodeCode) &&
                                !string.IsNullOrEmpty(plannedEdge.FromNodeCode) &&
                                !string.IsNullOrEmpty(plannedEdge.ToNodeCode);

            if (hasNodeCodes)
            {
                // 同向冲突(基于 NodeCode)
                if (string.Equals(plannedEdge.FromNodeCode, currentEdge.FromNodeCode, StringComparison.OrdinalIgnoreCase) &&
                    string.Equals(plannedEdge.ToNodeCode, currentEdge.ToNodeCode, StringComparison.OrdinalIgnoreCase))
                {
                    return EdgeConflictType.SameDirection;
                }

                // 对向冲突(基于 NodeCode)
                if (string.Equals(plannedEdge.FromNodeCode, currentEdge.ToNodeCode, StringComparison.OrdinalIgnoreCase) &&
                    string.Equals(plannedEdge.ToNodeCode, currentEdge.FromNodeCode, StringComparison.OrdinalIgnoreCase))
                {
                    return EdgeConflictType.OppositeDirection;
                }
            }
            else
            {
                // 回退到 NodeId 匹配(同地图场景)
                // 同向冲突
                if (plannedEdge.FromNodeId == currentEdge.FromNodeId && plannedEdge.ToNodeId == currentEdge.ToNodeId)
                {
                    return EdgeConflictType.SameDirection;
                }

                // 对向冲突
                if (plannedEdge.FromNodeId == currentEdge.ToNodeId && plannedEdge.ToNodeId == currentEdge.FromNodeId)
                {
                    return EdgeConflictType.OppositeDirection;
                }
            }

            return EdgeConflictType.None;
        }

        /// <summary>
        /// 计算机器人离开边的时间(秒)
        /// @author zzy
        /// </summary>
        /// <param name="planned">已规划的边及机器人状态</param>
        /// <returns>离开时间(秒),如果被阻塞则返回无穷大</returns>
        private static double CalculateTimeToLeaveEdge(PlannedEdge planned)
        {
            // 车辆被阻塞(遇到人或障碍物)
            if (planned.IsBlocked)
            {
                // 被堵住了,离开时间趋近无穷大
                return double.MaxValue;
            }

            // 车辆还未到达该边(TraveledDistance == 0)
            if (planned.TraveledDistance <= 0)
            {
                // 车辆还在路上,需要先到达该边,再穿过该边
                // T_leave = T_arrival + T_traverse
                // T_arrival = EstimatedArrivalTime(到达边起点的时间)
                // T_traverse = Length / AverageSpeed(穿过边的时间)
                double effectiveSpeed = planned.AverageSpeed > 0 ? planned.AverageSpeed : 1.0;
                double traverseTime = planned.Length / effectiveSpeed;
                return planned.EstimatedArrivalTime + traverseTime;
            }

            // 车辆已在边上(TraveledDistance > 0)
            // 计算剩余距离
            double remainingDistance = planned.Length - planned.TraveledDistance;

            // 如果已经走完或超出边长,认为即将离开
            if (remainingDistance <= 0)
            {
                return 0;
            }

            // 优先使用当前速度,如果当前速度为0或负数,使用平均速度
            double effectiveSpeed2 = planned.CurrentSpeed > 0
                ? planned.CurrentSpeed
                : planned.AverageSpeed;

            // 如果速度仍然为0或负数,认为车辆停止,离开时间无穷大
            if (effectiveSpeed2 <= 0)
            {
                return double.MaxValue;
            }

            // T_leave = D_remain / V_other
            double tLeave = remainingDistance / effectiveSpeed2;

            return tLeave;
        }

        /// <summary>
        /// 选择最佳行驶角度:计算边和车辆方向交集,找到当前航向角±偏差范围内最接近的值
        /// @author zzy
        /// </summary>
        /// <param name="edgeRads">边支持的行驶方向(弧度,相对于边切线)</param>
        /// <param name="vehicleRads">车辆支持的行驶方向(弧度,相对于车头)</param>
        /// <param name="currentTheta">当前全局航向角(弧度)</param>
        /// <param name="edgeDirection">边切线方向(弧度,全局坐标系)</param>
        /// <param name="maxDeviation">最大允许偏差(弧度)</param>
        /// <returns>(最佳行驶角度, 是否需要旋转),无交集时角度为null</returns>
        private static (double? selectedRad, bool needsRotation) SelectBestTraversalRad(
            List<double> edgeRads, List<double> vehicleRads,
            double currentTheta, double edgeDirection, double? maxDeviation)
        {
            // 默认支持正向(0)和倒车(π)
            var edges = edgeRads.Count > 0 ? edgeRads : new List<double> { 0, Math.PI };
            var vehicles = vehicleRads.Count > 0 ? vehicleRads : new List<double> { 0, Math.PI };
            var maxDev = maxDeviation ?? Math.PI; // 默认为180度(π弧度)

            // 计算交集
            var intersection = edges.Where(ea => vehicles.Any(va => Math.Abs(AngleConverter.NormalizeRadians(va - ea)) < 0.001)).ToList();
            if (intersection.Count == 0) return (null, false);

            // 找偏差范围内最接近当前航向角的
            double? bestAngle = null;
            double minDiff = double.MaxValue;

            foreach (var angle in intersection)
            {
                // 将相对行驶角度转换为全局航向角
                var globalHeading = AngleConverter.NormalizeRadians(edgeDirection + angle);
                var deviation = Math.Abs(AngleConverter.NormalizeRadians(currentTheta - globalHeading));
                if (deviation <= maxDev && deviation < minDiff)
                {
                    minDiff = deviation;
                    bestAngle = angle;
                }
            }

            // 偏差范围内无命中,需要旋转,选最接近的
            if (!bestAngle.HasValue)
            {
                foreach (var angle in intersection)
                {
                    var globalHeading = AngleConverter.NormalizeRadians(edgeDirection + angle);
                    var deviation = Math.Abs(AngleConverter.NormalizeRadians(currentTheta - globalHeading));
                    if (deviation < minDiff)
                    {
                        minDiff = deviation;
                        bestAngle = angle;
                    }
                }
                return (bestAngle, true);
            }

            return (bestAngle, false);
        }

        /// <summary>
        /// 计算转向代价
        /// @author zzy
        /// </summary>
        /// <param name="currentTheta">当前航向角(弧度)</param>
        /// <param name="targetAngle">目标航向角(弧度)</param>
        /// <param name="allowRotate">是否允许旋转</param>
        /// <returns>转向代价(0-1)</returns>
        private double CalculateTurnCost(double currentTheta, double targetAngle, bool allowRotate)
        {
            var diff = Math.Abs(NormalizeAngle(currentTheta - targetAngle));
            if (!allowRotate && diff > 0.1) return double.MaxValue;
            return diff / Math.PI;
        }

        /// <summary>
        /// 启发式函数(欧几里得距离)
        /// @author zzy
        /// </summary>
        /// <param name="from">起点节点</param>
        /// <param name="to">终点节点</param>
        /// <returns>两点间欧几里得距离</returns>
        private double Heuristic(PathNode from, PathNode to)
        {
            var dx = to.X - from.X;
            var dy = to.Y - from.Y;
            return Math.Sqrt(dx * dx + dy * dy);
        }

        /// <summary>
        /// 回溯构建路径
        /// @author zzy
        /// </summary>
        /// <param name="cameFrom">父节点记录</param>
        /// <param name="endNodeId">终点节点ID</param>
        /// <returns>路径段列表</returns>
        private List<PathSegment> ReconstructPath(
            Dictionary<Guid, (Guid nodeId, PathEdge edge, double Angle)> cameFrom, Guid endNodeId)
        {
            var segments = new List<PathSegment>();
            var current = endNodeId;

            while (cameFrom.TryGetValue(current, out var prev))
            {
                segments.Add(new PathSegment
                {
                    EdgeId = prev.edge.EdgeId,
                    FromNodeId = prev.nodeId,
                    ToNodeId = current,
                    Angle = prev.Angle,
                    Length = prev.edge.Length,
                    MaxSpeed = prev.edge.MaxSpeed
                });
                current = prev.nodeId;
            }

            segments.Reverse();
            return segments;
        }

        /// <summary>
        /// 角度归一化到[-PI, PI]
        /// @author zzy
        /// </summary>
        /// <param name="angle">输入角度(弧度)</param>
        /// <returns>归一化后的角度(弧度, -PI到PI)</returns>
        private double NormalizeAngle(double angle)
        {
            while (angle > Math.PI) angle -= 2 * Math.PI;
            while (angle < -Math.PI) angle += 2 * Math.PI;
            return angle;
        }

        /// <summary>
        /// 角度归一化到[-180, 180](度)
        /// @author zzy
        /// </summary>
        private static double NormalizeDegreesSymmetric(double degrees)
        {
            while (degrees > 180) degrees -= 360;
            while (degrees < -180) degrees += 360;
            return degrees;
        }

        private class SearchNode
        {
            public Guid NodeId { get; set; }
            public double currentTheta { get; set; }
            public double GScore { get; set; }
            public double FScore { get; set; }
        }
    }

    /// <summary>
    /// 边冲突类型
    /// @author zzy
    /// </summary>
    public enum EdgeConflictType
    {
        /// <summary>无冲突</summary>
        None = 0,
        /// <summary>同向冲突(跟车)</summary>
        SameDirection = 1,
        /// <summary>对向冲突(死锁风险)</summary>
        OppositeDirection = 2
    }



    

    
}