using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using AGVNavigationCore.Models; using AGVNavigationCore.PathFinding.Planning; namespace AGVNavigationCore.Utils { /// /// GetNextNodeId() 메서드의 동작을 검증하는 테스트 클래스 /// /// 테스트 시나리오: /// - 001(65,229) → 002(206,244) → Forward → 003이 나와야 함 /// - 001(65,229) → 002(206,244) → Backward → 001이 나와야 함 /// - 002(206,244) → 003(278,278) → Forward → 004가 나와야 함 /// - 002(206,244) → 003(278,278) → Backward → 002가 나와야 함 /// public class GetNextNodeIdTest { /// /// 가상의 VirtualAGV 상태를 시뮬레이션하여 GetNextNodeId 테스트 /// public void TestGetNextNodeId() { Console.WriteLine("\n================================================"); Console.WriteLine("GetNextNodeId() 동작 검증"); Console.WriteLine("================================================\n"); // 테스트 노드 생성 var node001 = new MapNode { Id = "N001", RfidId = 001, Position = new Point(65, 229), ConnectedNodes = new List { "N002" } }; var node002 = new MapNode { Id = "N002", RfidId = 002, Position = new Point(206, 244), ConnectedNodes = new List { "N001", "N003" } }; var node003 = new MapNode { Id = "N003", RfidId = 003, Position = new Point(278, 278), ConnectedNodes = new List { "N002", "N004" } }; var node004 = new MapNode { Id = "N004", RfidId = 004, Position = new Point(380, 340), ConnectedNodes = new List { "N003", "N022", "N031" } }; var allNodes = new List { node001, node002, node003, node004 }; // VirtualAGV 시뮬레이션 (실제 인스턴스 생성 불가하므로 로직만 재현) Console.WriteLine("테스트 시나리오 1: 001 → 002 → Forward"); Console.WriteLine("─────────────────────────────────────────"); TestScenario( "Forward 이동: 001에서 002로, 다음은 Forward", node001.Position, node002, node003, AgvDirection.Forward, allNodes, "003 (예상)" ); Console.WriteLine("\n테스트 시나리오 2: 001 → 002 → Backward"); Console.WriteLine("─────────────────────────────────────────"); TestScenario( "Backward 이동: 001에서 002로, 다음은 Backward", node001.Position, node002, node001, AgvDirection.Backward, allNodes, "001 (예상)" ); Console.WriteLine("\n테스트 시나리오 3: 002 → 003 → Forward"); Console.WriteLine("─────────────────────────────────────────"); TestScenario( "Forward 이동: 002에서 003으로, 다음은 Forward", node002.Position, node003, node004, AgvDirection.Forward, allNodes, "004 (예상)" ); Console.WriteLine("\n테스트 시나리오 4: 002 → 003 Forward → Backward"); Console.WriteLine("─────────────────────────────────────────"); TestScenario( "Forward 이동: 002에서 003으로, 다음은 Backward (경로 반대)", node002.Position, node003, node002, AgvDirection.Backward, allNodes, "002 (예상 - 경로 반대)" ); Console.WriteLine("\n테스트 시나리오 5: 002 → 003 Backward → Forward"); Console.WriteLine("─────────────────────────────────────────"); TestScenario( "Backward 이동: 002에서 003으로, 다음은 Forward (경로 반대)", node002.Position, node003, node002, AgvDirection.Forward, allNodes, "002 (예상 - 경로 반대)", AgvDirection.Backward // 현재 모터 방향 ); Console.WriteLine("\n테스트 시나리오 6: 002 → 003 Backward → Backward"); Console.WriteLine("─────────────────────────────────────────"); TestScenario( "Backward 이동: 002에서 003으로, 다음은 Backward (경로 계속)", node002.Position, node003, node004, AgvDirection.Backward, allNodes, "004 (예상 - 경로 계속)", AgvDirection.Backward // 현재 모터 방향 ); Console.WriteLine("\n\n================================================"); Console.WriteLine("테스트 완료"); Console.WriteLine("================================================\n"); } /// /// 개별 테스트 시나리오 실행 /// private void TestScenario( string description, Point prevPos, MapNode currentNode, MapNode expectedNextNode, AgvDirection direction, List allNodes, string expectedNodeIdStr, AgvDirection? currentMotorDirection = null) { // 현재 모터 방향이 지정되지 않으면 direction과 동일하다고 가정 AgvDirection motorDir = currentMotorDirection ?? direction; Console.WriteLine($"설명: {description}"); Console.WriteLine($"이전 위치: {prevPos} (RFID: {allNodes.First(n => n.Position == prevPos)?.RfidId.ToString("0000") ?? "?"})"); Console.WriteLine($"현재 노드: {currentNode.Id} (RFID: {currentNode.RfidId}) - 위치: {currentNode.Position}"); Console.WriteLine($"현재 모터 방향: {motorDir}"); Console.WriteLine($"요청 방향: {direction}"); // 이동 벡터 계산 var movementVector = new PointF( currentNode.Position.X - prevPos.X, currentNode.Position.Y - prevPos.Y ); Console.WriteLine($"이동 벡터: ({movementVector.X}, {movementVector.Y})"); // 각 후보 노드에 대한 점수 계산 Console.WriteLine($"\n현재 노드({currentNode.Id})의 ConnectedNodes: {string.Join(", ", currentNode.ConnectedNodes)}"); Console.WriteLine($"가능한 다음 노드들:"); var candidateNodes = allNodes.Where(n => currentNode.ConnectedNodes.Contains(n.Id) && n.Id != currentNode.Id ).ToList(); foreach (var candidate in candidateNodes) { var score = CalculateScoreAndPrint(movementVector, currentNode.Position, candidate, direction); string isExpected = (candidate.Id == expectedNextNode.Id) ? " ← 예상 노드" : ""; Console.WriteLine($" {candidate.Id} (RFID: {candidate.RfidId}) - 위치: {candidate.Position} - 점수: {score:F1}{isExpected}"); } // 최고 점수 노드 선택 var bestCandidate = GetBestCandidate(movementVector, currentNode.Position, candidateNodes, direction); Console.WriteLine($"\n✓ 선택된 노드: {bestCandidate.Id} (RFID: {bestCandidate.RfidId})"); if (bestCandidate.Id == expectedNextNode.Id) { Console.WriteLine($"✅ 정답! ({expectedNodeIdStr})"); } else { Console.WriteLine($"❌ 오답! 예상: {expectedNextNode.Id}, 실제: {bestCandidate.Id}"); } } /// /// 점수 계산 및 상세 정보 출력 /// private float CalculateScoreAndPrint(PointF movementVector, Point currentPos, MapNode candidate, AgvDirection direction) { // 벡터 정규화 var movementLength = (float)Math.Sqrt( movementVector.X * movementVector.X + movementVector.Y * movementVector.Y ); var normalizedMovement = new PointF( movementVector.X / movementLength, movementVector.Y / movementLength ); // 다음 벡터 var toNextVector = new PointF( candidate.Position.X - currentPos.X, candidate.Position.Y - currentPos.Y ); var toNextLength = (float)Math.Sqrt( toNextVector.X * toNextVector.X + toNextVector.Y * toNextVector.Y ); var normalizedToNext = new PointF( toNextVector.X / toNextLength, toNextVector.Y / toNextLength ); // 내적 및 외적 계산 float dotProduct = (normalizedMovement.X * normalizedToNext.X) + (normalizedMovement.Y * normalizedToNext.Y); float crossProduct = (normalizedMovement.X * normalizedToNext.Y) - (normalizedMovement.Y * normalizedToNext.X); float score = CalculateDirectionalScore(dotProduct, crossProduct, direction); return score; } /// /// 점수 계산 (VirtualAGV.CalculateDirectionalScore()와 동일) /// private float CalculateDirectionalScore(float dotProduct, float crossProduct, AgvDirection direction) { float baseScore = 0; switch (direction) { case AgvDirection.Forward: if (dotProduct > 0.9f) baseScore = 100.0f; else if (dotProduct > 0.5f) baseScore = 80.0f; else if (dotProduct > 0.0f) baseScore = 50.0f; else if (dotProduct > -0.5f) baseScore = 20.0f; break; case AgvDirection.Backward: if (dotProduct < -0.9f) baseScore = 100.0f; else if (dotProduct < -0.5f) baseScore = 80.0f; else if (dotProduct < 0.0f) baseScore = 50.0f; else if (dotProduct < 0.5f) baseScore = 20.0f; break; case AgvDirection.Left: if (dotProduct > 0.0f) { if (crossProduct > 0.5f) baseScore = 100.0f; else if (crossProduct > 0.0f) baseScore = 70.0f; else if (crossProduct > -0.5f) baseScore = 50.0f; else baseScore = 30.0f; } else { if (crossProduct < -0.5f) baseScore = 100.0f; else if (crossProduct < 0.0f) baseScore = 70.0f; else if (crossProduct < 0.5f) baseScore = 50.0f; else baseScore = 30.0f; } break; case AgvDirection.Right: if (dotProduct > 0.0f) { if (crossProduct < -0.5f) baseScore = 100.0f; else if (crossProduct < 0.0f) baseScore = 70.0f; else if (crossProduct < 0.5f) baseScore = 50.0f; else baseScore = 30.0f; } else { if (crossProduct > 0.5f) baseScore = 100.0f; else if (crossProduct > 0.0f) baseScore = 70.0f; else if (crossProduct > -0.5f) baseScore = 50.0f; else baseScore = 30.0f; } break; } return baseScore; } /// /// 최고 점수 노드 반환 /// private MapNode GetBestCandidate(PointF movementVector, Point currentPos, List candidates, AgvDirection direction) { var movementLength = (float)Math.Sqrt( movementVector.X * movementVector.X + movementVector.Y * movementVector.Y ); var normalizedMovement = new PointF( movementVector.X / movementLength, movementVector.Y / movementLength ); MapNode bestCandidate = null; float bestScore = -1; foreach (var candidate in candidates) { var toNextVector = new PointF( candidate.Position.X - currentPos.X, candidate.Position.Y - currentPos.Y ); var toNextLength = (float)Math.Sqrt( toNextVector.X * toNextVector.X + toNextVector.Y * toNextVector.Y ); var normalizedToNext = new PointF( toNextVector.X / toNextLength, toNextVector.Y / toNextLength ); float dotProduct = (normalizedMovement.X * normalizedToNext.X) + (normalizedMovement.Y * normalizedToNext.Y); float crossProduct = (normalizedMovement.X * normalizedToNext.Y) - (normalizedMovement.Y * normalizedToNext.X); float score = CalculateDirectionalScore(dotProduct, crossProduct, direction); if (score > bestScore) { bestScore = score; bestCandidate = candidate; } } return bestCandidate; } } }