using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using AGVNavigationCore.Models; namespace AGVNavigationCore.PathFinding { /// /// A* 알고리즘 기반 경로 탐색기 /// public class AStarPathfinder { private Dictionary _nodeMap; private List _mapNodes; /// /// 휴리스틱 가중치 (기본값: 1.0) /// 값이 클수록 목적지 방향을 우선시하나 최적 경로를 놓칠 수 있음 /// public float HeuristicWeight { get; set; } = 1.0f; /// /// 최대 탐색 노드 수 (무한 루프 방지) /// public int MaxSearchNodes { get; set; } = 1000; /// /// 생성자 /// public AStarPathfinder() { _nodeMap = new Dictionary(); _mapNodes = new List(); } /// /// 맵 노드 설정 /// /// 맵 노드 목록 public void SetMapNodes(List mapNodes) { _mapNodes = mapNodes ?? new List(); _nodeMap.Clear(); // 1단계: 모든 네비게이션 노드를 PathNode로 변환 foreach (var mapNode in _mapNodes) { if (mapNode.IsNavigationNode()) { var pathNode = new PathNode(mapNode.NodeId, mapNode.Position); pathNode.ConnectedNodes = new List(mapNode.ConnectedNodes); _nodeMap[mapNode.NodeId] = pathNode; } } // 2단계: 양방향 연결 자동 생성 (A→B 연결이 있으면 B→A도 추가) EnsureBidirectionalConnections(); } /// /// 단방향 연결을 양방향으로 자동 변환 /// A→B 연결이 있으면 B→A 연결도 자동 생성 /// private void EnsureBidirectionalConnections() { foreach (var nodeId in _nodeMap.Keys.ToList()) { var node = _nodeMap[nodeId]; foreach (var connectedNodeId in node.ConnectedNodes.ToList()) { // 연결된 노드가 존재하고 네비게이션 가능한 노드인지 확인 if (_nodeMap.ContainsKey(connectedNodeId)) { var connectedNode = _nodeMap[connectedNodeId]; // 역방향 연결이 없으면 추가 if (!connectedNode.ConnectedNodes.Contains(nodeId)) { connectedNode.ConnectedNodes.Add(nodeId); } } } } } /// /// 경로 찾기 (A* 알고리즘) /// /// 시작 노드 ID /// 목적지 노드 ID /// 경로 계산 결과 public PathResult FindPath(string startNodeId, string endNodeId) { var stopwatch = System.Diagnostics.Stopwatch.StartNew(); try { if (!_nodeMap.ContainsKey(startNodeId)) { return PathResult.CreateFailure($"시작 노드를 찾을 수 없습니다: {startNodeId}", stopwatch.ElapsedMilliseconds, 0); } if (!_nodeMap.ContainsKey(endNodeId)) { return PathResult.CreateFailure($"목적지 노드를 찾을 수 없습니다: {endNodeId}", stopwatch.ElapsedMilliseconds, 0); } if (startNodeId == endNodeId) { return PathResult.CreateSuccess(new List { startNodeId }, 0, stopwatch.ElapsedMilliseconds, 1); } var startNode = _nodeMap[startNodeId]; var endNode = _nodeMap[endNodeId]; var openSet = new List(); var closedSet = new HashSet(); var exploredCount = 0; startNode.GCost = 0; startNode.HCost = CalculateHeuristic(startNode, endNode); startNode.Parent = null; openSet.Add(startNode); while (openSet.Count > 0 && exploredCount < MaxSearchNodes) { var currentNode = GetLowestFCostNode(openSet); openSet.Remove(currentNode); closedSet.Add(currentNode.NodeId); exploredCount++; if (currentNode.NodeId == endNodeId) { var path = ReconstructPath(currentNode); var totalDistance = CalculatePathDistance(path); return PathResult.CreateSuccess(path, totalDistance, stopwatch.ElapsedMilliseconds, exploredCount); } foreach (var neighborId in currentNode.ConnectedNodes) { if (closedSet.Contains(neighborId) || !_nodeMap.ContainsKey(neighborId)) continue; var neighbor = _nodeMap[neighborId]; var tentativeGCost = currentNode.GCost + currentNode.DistanceTo(neighbor); if (!openSet.Contains(neighbor)) { neighbor.Parent = currentNode; neighbor.GCost = tentativeGCost; neighbor.HCost = CalculateHeuristic(neighbor, endNode); openSet.Add(neighbor); } else if (tentativeGCost < neighbor.GCost) { neighbor.Parent = currentNode; neighbor.GCost = tentativeGCost; } } } return PathResult.CreateFailure("경로를 찾을 수 없습니다", stopwatch.ElapsedMilliseconds, exploredCount); } catch (Exception ex) { return PathResult.CreateFailure($"경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds, 0); } } /// /// 여러 목적지 중 가장 가까운 노드로의 경로 찾기 /// /// 시작 노드 ID /// 목적지 후보 노드 ID 목록 /// 경로 계산 결과 public PathResult FindNearestPath(string startNodeId, List targetNodeIds) { if (targetNodeIds == null || targetNodeIds.Count == 0) { return PathResult.CreateFailure("목적지 노드가 지정되지 않았습니다", 0, 0); } PathResult bestResult = null; foreach (var targetId in targetNodeIds) { var result = FindPath(startNodeId, targetId); if (result.Success && (bestResult == null || result.TotalDistance < bestResult.TotalDistance)) { bestResult = result; } } return bestResult ?? PathResult.CreateFailure("모든 목적지로의 경로를 찾을 수 없습니다", 0, 0); } /// /// 휴리스틱 거리 계산 (유클리드 거리) /// private float CalculateHeuristic(PathNode from, PathNode to) { return from.DistanceTo(to) * HeuristicWeight; } /// /// F cost가 가장 낮은 노드 선택 /// private PathNode GetLowestFCostNode(List nodes) { PathNode lowest = nodes[0]; foreach (var node in nodes) { if (node.FCost < lowest.FCost || (Math.Abs(node.FCost - lowest.FCost) < 0.001f && node.HCost < lowest.HCost)) { lowest = node; } } return lowest; } /// /// 경로 재구성 (부모 노드를 따라 역추적) /// private List ReconstructPath(PathNode endNode) { var path = new List(); var current = endNode; while (current != null) { path.Add(current.NodeId); current = current.Parent; } path.Reverse(); return path; } /// /// 경로의 총 거리 계산 /// private float CalculatePathDistance(List path) { if (path.Count < 2) return 0; float totalDistance = 0; for (int i = 0; i < path.Count - 1; i++) { if (_nodeMap.ContainsKey(path[i]) && _nodeMap.ContainsKey(path[i + 1])) { totalDistance += _nodeMap[path[i]].DistanceTo(_nodeMap[path[i + 1]]); } } return totalDistance; } /// /// 두 노드가 연결되어 있는지 확인 /// /// 노드 1 ID /// 노드 2 ID /// 연결 여부 public bool AreNodesConnected(string nodeId1, string nodeId2) { if (!_nodeMap.ContainsKey(nodeId1) || !_nodeMap.ContainsKey(nodeId2)) return false; return _nodeMap[nodeId1].ConnectedNodes.Contains(nodeId2); } /// /// 네비게이션 가능한 노드 목록 반환 /// /// 노드 ID 목록 public List GetNavigationNodes() { return _nodeMap.Keys.ToList(); } /// /// 노드 정보 반환 /// /// 노드 ID /// 노드 정보 또는 null public PathNode GetNode(string nodeId) { return _nodeMap.ContainsKey(nodeId) ? _nodeMap[nodeId] : null; } } }