using System; using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json; namespace AGVNavigationCore.Models { /// /// AGV 맵 파일 로딩/저장을 위한 공용 유틸리티 클래스 /// AGVMapEditor와 AGVSimulator에서 공통으로 사용 /// public static class MapLoader { /// /// 맵 파일 로딩 결과 /// public class MapLoadResult { public bool Success { get; set; } public List Nodes { get; set; } = new List(); public string ErrorMessage { get; set; } = string.Empty; public string Version { get; set; } = string.Empty; public DateTime CreatedDate { get; set; } } /// /// 맵 파일 저장용 데이터 구조 /// public class MapFileData { public List Nodes { get; set; } = new List(); public DateTime CreatedDate { get; set; } public string Version { get; set; } = "1.0"; } /// /// 맵 파일을 로드하여 노드를 반환 /// /// 맵 파일 경로 /// 로딩 결과 public static MapLoadResult LoadMapFromFile(string filePath) { var result = new MapLoadResult(); try { if (!File.Exists(filePath)) { result.ErrorMessage = $"파일을 찾을 수 없습니다: {filePath}"; return result; } var json = File.ReadAllText(filePath); // JSON 역직렬화 설정: 누락된 속성 무시, 안전한 처리 var settings = new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore, NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Populate }; var mapData = JsonConvert.DeserializeObject(json, settings); if (mapData != null) { result.Nodes = mapData.Nodes ?? new List(); result.Version = mapData.Version ?? "1.0"; result.CreatedDate = mapData.CreatedDate; // 기존 Description 데이터를 Name으로 마이그레이션 MigrateDescriptionToName(result.Nodes); // DockingDirection 마이그레이션 (기존 NodeType 기반으로 설정) MigrateDockingDirection(result.Nodes); // 중복된 NodeId 정리 FixDuplicateNodeIds(result.Nodes); // 중복 연결 정리 (양방향 중복 제거) CleanupDuplicateConnections(result.Nodes); // 이미지 노드들의 이미지 로드 LoadImageNodes(result.Nodes); result.Success = true; } else { result.ErrorMessage = "맵 데이터 파싱에 실패했습니다."; } } catch (Exception ex) { result.ErrorMessage = $"맵 파일 로딩 중 오류 발생: {ex.Message}"; } return result; } /// /// 맵 데이터를 파일로 저장 /// /// 저장할 파일 경로 /// 맵 노드 목록 /// 저장 성공 여부 public static bool SaveMapToFile(string filePath, List nodes) { try { var mapData = new MapFileData { Nodes = nodes, CreatedDate = DateTime.Now, Version = "1.0" }; var json = JsonConvert.SerializeObject(mapData, Formatting.Indented); File.WriteAllText(filePath, json); return true; } catch (Exception) { return false; } } /// /// 이미지 노드들의 이미지 로드 /// /// 노드 목록 private static void LoadImageNodes(List nodes) { foreach (var node in nodes) { if (node.Type == NodeType.Image) { node.LoadImage(); } } } /// /// 기존 Description 데이터를 Name 필드로 마이그레이션 /// JSON 파일에서 Description 필드가 있는 경우 Name으로 이동 /// /// 맵 노드 목록 private static void MigrateDescriptionToName(List mapNodes) { // JSON에서 Description이 있던 기존 파일들을 위한 마이그레이션 // 현재 MapNode 클래스에는 Description 속성이 제거되었으므로 // 이 메서드는 호환성을 위해 유지되지만 실제로는 작동하지 않음 // 기존 파일들은 다시 저장될 때 Description 없이 저장됨 } /// /// 기존 맵 파일의 DockingDirection을 NodeType 기반으로 마이그레이션 /// /// 맵 노드 목록 private static void MigrateDockingDirection(List mapNodes) { if (mapNodes == null || mapNodes.Count == 0) return; foreach (var node in mapNodes) { // 기존 파일에서 DockingDirection이 기본값(DontCare)인 경우에만 마이그레이션 if (node.DockDirection == DockingDirection.DontCare) { switch (node.Type) { case NodeType.Charging: node.DockDirection = DockingDirection.Forward; break; case NodeType.Docking: node.DockDirection = DockingDirection.Backward; break; default: // Normal, Rotation, Label, Image는 DontCare 유지 node.DockDirection = DockingDirection.DontCare; break; } } } } /// /// 중복된 NodeId를 가진 노드들을 고유한 NodeId로 수정 /// /// 맵 노드 목록 private static void FixDuplicateNodeIds(List mapNodes) { if (mapNodes == null || mapNodes.Count == 0) return; var usedIds = new HashSet(); var duplicateNodes = new List(); // 첫 번째 패스: 중복된 노드들 식별 foreach (var node in mapNodes) { if (usedIds.Contains(node.NodeId)) { duplicateNodes.Add(node); } else { usedIds.Add(node.NodeId); } } // 두 번째 패스: 중복된 노드들에게 새로운 NodeId 할당 foreach (var duplicateNode in duplicateNodes) { string newNodeId = GenerateUniqueNodeId(usedIds); // 다른 노드들의 연결에서 기존 NodeId를 새 NodeId로 업데이트 UpdateConnections(mapNodes, duplicateNode.NodeId, newNodeId); duplicateNode.NodeId = newNodeId; usedIds.Add(newNodeId); } } /// /// 사용되지 않는 고유한 NodeId 생성 /// /// 이미 사용된 NodeId 목록 /// 고유한 NodeId private static string GenerateUniqueNodeId(HashSet usedIds) { int counter = 1; string nodeId; do { nodeId = $"N{counter:D3}"; counter++; } while (usedIds.Contains(nodeId)); return nodeId; } /// /// 노드 연결에서 NodeId 변경사항 반영 /// /// 맵 노드 목록 /// 기존 NodeId /// 새로운 NodeId private static void UpdateConnections(List mapNodes, string oldNodeId, string newNodeId) { foreach (var node in mapNodes) { if (node.ConnectedNodes != null) { for (int i = 0; i < node.ConnectedNodes.Count; i++) { if (node.ConnectedNodes[i] == oldNodeId) { node.ConnectedNodes[i] = newNodeId; } } } } } /// /// 중복 연결을 정리합니다. 양방향 중복 연결을 단일 연결로 통합합니다. /// /// 맵 노드 목록 private static void CleanupDuplicateConnections(List mapNodes) { if (mapNodes == null || mapNodes.Count == 0) return; var processedPairs = new HashSet(); foreach (var node in mapNodes) { var connectionsToRemove = new List(); foreach (var connectedNodeId in node.ConnectedNodes.ToList()) { var connectedNode = mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId); if (connectedNode == null) continue; // 연결 쌍의 키 생성 (사전순 정렬) string pairKey = string.Compare(node.NodeId, connectedNodeId, StringComparison.Ordinal) < 0 ? $"{node.NodeId}-{connectedNodeId}" : $"{connectedNodeId}-{node.NodeId}"; if (processedPairs.Contains(pairKey)) { // 이미 처리된 연결인 경우 중복으로 간주하고 제거 connectionsToRemove.Add(connectedNodeId); } else { // 처리되지 않은 연결인 경우 processedPairs.Add(pairKey); // 양방향 연결인 경우 하나만 유지 if (connectedNode.ConnectedNodes.Contains(node.NodeId)) { // 사전순으로 더 작은 노드에만 연결을 유지 if (string.Compare(node.NodeId, connectedNodeId, StringComparison.Ordinal) > 0) { connectionsToRemove.Add(connectedNodeId); } else { // 반대 방향 연결 제거 connectedNode.RemoveConnection(node.NodeId); } } } } // 중복 연결 제거 foreach (var connectionToRemove in connectionsToRemove) { node.RemoveConnection(connectionToRemove); } } } /// /// MapNode 목록에서 RFID가 없는 노드들에 자동으로 RFID ID를 할당합니다. /// *** 에디터와 시뮬레이터 데이터 불일치 방지를 위해 비활성화됨 *** /// /// 맵 노드 목록 [Obsolete("RFID 자동 할당은 에디터와 시뮬레이터 간 데이터 불일치를 야기하므로 사용하지 않음")] public static void AssignAutoRfidIds(List mapNodes) { // 에디터에서 설정한 RFID 값을 그대로 사용하기 위해 자동 할당 기능 비활성화 // 에디터와 시뮬레이터 간 데이터 일관성 유지를 위함 return; /* foreach (var node in mapNodes) { // 네비게이션 가능한 노드이면서 RFID가 없는 경우에만 자동 할당 if (node.IsNavigationNode() && !node.HasRfid()) { // 기본 RFID ID 생성 (N001 -> 001) var rfidId = node.NodeId.Replace("N", "").PadLeft(3, '0'); node.SetRfidInfo(rfidId, "", "정상"); } } */ } } }