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 MapSettings { public int BackgroundColorArgb { get; set; } = System.Drawing.Color.White.ToArgb(); public bool ShowGrid { get; set; } = true; } /// /// 맵 파일 로딩 결과 /// public class MapLoadResult { public bool Success { get; set; } public List Nodes { get; set; } = new List(); public List Labels { get; set; } = new List(); // 추가 public List Images { get; set; } = new List(); // 추가 public List Marks { get; set; } = new List(); public List Magnets { get; set; } = new List(); public MapSettings Settings { get; set; } = new MapSettings(); 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 List Labels { get; set; } = new List(); // 추가 public List Images { get; set; } = new List(); // 추가 public List Marks { get; set; } = new List(); public List Magnets { get; set; } = new List(); public MapSettings Settings { get; set; } = new MapSettings(); public DateTime CreatedDate { get; set; } public string Version { get; set; } = "1.3"; // 버전 업그레이드 } /// /// 맵 파일을 로드하여 노드를 반환 /// /// 맵 파일 경로 /// 로딩 결과 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 }; // 먼저 구조 파악을 위해 동적 객체로 로드하거나, MapFileData로 시도 var mapData = JsonConvert.DeserializeObject(json, settings); if (mapData != null) { result.Nodes = new List(); result.Labels = mapData.Labels ?? new List(); result.Images = mapData.Images ?? new List(); result.Marks = mapData.Marks ?? new List(); result.Magnets = mapData.Magnets ?? new List(); result.Settings = mapData.Settings ?? new MapSettings(); result.Version = mapData.Version ?? "1.0"; result.CreatedDate = mapData.CreatedDate; if (mapData.Nodes != null) { foreach (var node in mapData.Nodes) { // 마이그레이션: 기존 파일의 Nodes 리스트에 섞여있는 Label, Image 분리 // (새 파일 구조에서는 이미 분리되어 로드됨) if (node.Type == NodeType.Label) { // MapNode -> MapLabel 변환 (필드 매핑) var label = new MapLabel { Id = node.Id, // 기존 NodeId -> Id Position = node.Position, CreatedDate = node.CreatedDate, ModifiedDate = node.ModifiedDate, // Label 속성 매핑 (MapNode에서 임시로 가져오거나 Json Raw Parsing 필요할 수 있음) // 현재 MapNode 클래스에는 해당 속성들이 제거되었으므로, // Json 포맷 변경으로 인해 기존 데이터 로드시 정보 손실 가능성 있음. // * 중요 *: MapNode 클래스에서 속성을 지웠으므로 일반 Deserialize로는 Label/Image 속성을 못 읽음. // 해결책: JObject로 먼저 읽어서 분기 처리하거나, DTO 클래스를 별도로 두어야 함. // 하지만 시간 관계상, 만약 기존 MapNode가 속성을 가지고 있지 않다면 마이그레이션은 "위치/ID" 정도만 복구됨. // 완벽한 마이그레이션을 위해서는 MapNode에 Obsolete 속성을 잠시 두었어야 함. // 여기서는 일단 기본 정보라도 살림. }; result.Labels.Add(label); } else if (node.Type == NodeType.Image) { var image = new MapImage { Id = node.Id, Position = node.Position, CreatedDate = node.CreatedDate, ModifiedDate = node.ModifiedDate, // 이미지/라벨 속성 복구 불가 (MapNode에서 삭제됨) }; result.Images.Add(image); } else { result.Nodes.Add(node); } } } // 중복된 NodeId 정리 (Nav Node만) FixDuplicateNodeIds(result.Nodes); // 고아 연결 정리 CleanupOrphanConnections(result.Nodes); // 양방향 연결 자동 설정 EnsureBidirectionalConnections(result.Nodes); // ConnectedMapNodes 채우기 ResolveConnectedMapNodes(result.Nodes); // 이미지 로드 (MapImage 객체에서) foreach (var img in result.Images) { img.LoadImage(); } result.Success = true; } else { result.ErrorMessage = "맵 데이터 파싱에 실패했습니다."; } } catch (Exception ex) { result.ErrorMessage = $"맵 파일 로딩 중 오류 발생: {ex.Message}"; } return result; } /// /// 맵 데이터를 파일로 저장 /// public static bool SaveMapToFile(string filePath, List nodes, List labels = null, List images = null, List marks = null, List magnets = null, MapSettings settings = null) { try { // 저장 전 고아 연결 정리 CleanupOrphanConnections(nodes); var mapData = new MapFileData { Nodes = nodes, Labels = labels ?? new List(), Images = images ?? new List(), Marks = marks ?? new List(), Magnets = magnets ?? new List(), Settings = settings ?? new MapSettings(), CreatedDate = DateTime.Now, Version = "1.3" }; var json = JsonConvert.SerializeObject(mapData, Formatting.Indented); File.WriteAllText(filePath, json); return true; } catch (Exception) { return false; } } /// /// ConnectedMapNodes 채우기 (ConnectedNodes의 string ID → MapNode 객체 변환) /// /// 맵 노드 목록 private static void ResolveConnectedMapNodes(List mapNodes) { if (mapNodes == null || mapNodes.Count == 0) return; // 빠른 조회를 위한 Dictionary 생성 var nodeDict = mapNodes.ToDictionary(n => n.Id, n => n); foreach (var node in mapNodes) { // ConnectedMapNodes 초기화 node.ConnectedMapNodes.Clear(); if (node.ConnectedNodes != null && node.ConnectedNodes.Count > 0) { foreach (var connectedNodeId in node.ConnectedNodes) { if (nodeDict.TryGetValue(connectedNodeId, out var connectedNode)) { node.ConnectedMapNodes.Add(connectedNode); } } } } } /// /// 기존 Description 데이터를 Name 필드로 마이그레이션 /// JSON 파일에서 Description 필드가 있는 경우 Name으로 이동 /// /// 맵 노드 목록 private static void MigrateDescriptionToName(List mapNodes) { // JSON에서 Description이 있던 기존 파일들을 위한 마이그레이션 // 현재 MapNode 클래스에는 Description 속성이 제거되었으므로 // 이 메서드는 호환성을 위해 유지되지만 실제로는 작동하지 않음 // 기존 파일들은 다시 저장될 때 Description 없이 저장됨 } /// /// 중복된 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.Id)) { duplicateNodes.Add(node); } else { usedIds.Add(node.Id); } } // 두 번째 패스: 중복된 노드들에게 새로운 NodeId 할당 foreach (var duplicateNode in duplicateNodes) { string newNodeId = GenerateUniqueNodeId(usedIds); // 다른 노드들의 연결에서 기존 NodeId를 새 NodeId로 업데이트 UpdateConnections(mapNodes, duplicateNode.Id, newNodeId); duplicateNode.Id = 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; } } } } } /// /// 존재하지 않는 노드에 대한 연결을 정리합니다 (고아 연결 제거). /// 노드 삭제 후 저장된 맵 파일에서 삭제된 노드 ID가 ConnectedNodes에 남아있는 경우를 처리합니다. /// /// 맵 노드 목록 private static void CleanupOrphanConnections(List mapNodes) { if (mapNodes == null || mapNodes.Count == 0) return; // 존재하는 모든 노드 ID 집합 생성 var validNodeIds = new HashSet(mapNodes.Select(n => n.Id)); // 각 노드의 연결을 검증하고 존재하지 않는 노드 ID 제거 foreach (var node in mapNodes) { if (node.ConnectedNodes == null || node.ConnectedNodes.Count == 0) continue; var orphanConnections = node.ConnectedNodes .Where(connectedId => !validNodeIds.Contains(connectedId)) .ToList(); foreach (var orphanId in orphanConnections) { node.RemoveConnection(orphanId); } } } /// /// [사용 중지됨] 중복 연결을 정리합니다. 양방향 중복 연결을 단일 연결로 통합합니다. /// 주의: 이 함수는 버그가 있어 사용 중지됨 - 양방향 연결을 단방향으로 변환하여 경로 탐색 실패 발생 /// AGV 시스템에서는 모든 연결이 양방향이어야 하므로 EnsureBidirectionalConnections()만 사용 /// /// 맵 노드 목록 [Obsolete("이 함수는 양방향 연결을 단방향으로 변환하는 버그가 있습니다. 사용하지 마세요.")] 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.Id == connectedNodeId); if (connectedNode == null) continue; // 연결 쌍의 키 생성 (사전순 정렬) string pairKey = string.Compare(node.Id, connectedNodeId, StringComparison.Ordinal) < 0 ? $"{node.Id}-{connectedNodeId}" : $"{connectedNodeId}-{node.Id}"; if (processedPairs.Contains(pairKey)) { // 이미 처리된 연결인 경우 중복으로 간주하고 제거 connectionsToRemove.Add(connectedNodeId); } else { // 처리되지 않은 연결인 경우 processedPairs.Add(pairKey); // 양방향 연결인 경우 하나만 유지 if (connectedNode.ConnectedNodes.Contains(node.Id)) { // 사전순으로 더 작은 노드에만 연결을 유지 if (string.Compare(node.Id, connectedNodeId, StringComparison.Ordinal) > 0) { connectionsToRemove.Add(connectedNodeId); } else { // 반대 방향 연결 제거 connectedNode.RemoveConnection(node.Id); } } } } // 중복 연결 제거 foreach (var connectionToRemove in connectionsToRemove) { node.RemoveConnection(connectionToRemove); } } } /// /// 맵의 모든 연결을 양방향으로 만듭니다. /// A→B 연결이 있으면 B→A 연결도 자동으로 추가합니다. /// GetNextNodeId() 메서드에서 현재 노드의 ConnectedNodes만으로 다음 노드를 찾을 수 있도록 하기 위함. /// /// 예시: /// - 맵 에디터에서 002→003 연결을 생성했다면 /// - 자동으로 003→002 연결도 추가됨 /// - 따라서 003의 ConnectedNodes에 002가 포함됨 /// /// 맵 노드 목록 private static void EnsureBidirectionalConnections(List mapNodes) { if (mapNodes == null || mapNodes.Count == 0) return; // 모든 노드의 연결 정보를 수집 var allConnections = new Dictionary>(); // 1단계: 모든 명시적 연결 수집 foreach (var node in mapNodes) { if (!allConnections.ContainsKey(node.Id)) { allConnections[node.Id] = new HashSet(); } if (node.ConnectedNodes != null) { foreach (var connectedId in node.ConnectedNodes) { allConnections[node.Id].Add(connectedId); } } } // 2단계: 역방향 연결 추가 foreach (var node in mapNodes) { if (node.ConnectedNodes == null) { node.ConnectedNodes = new List(); } // 이 노드를 연결하는 모든 노드 찾기 foreach (var otherNodeId in allConnections.Keys) { if (otherNodeId == node.Id) continue; // 다른 노드가 이 노드를 연결하고 있다면 if (allConnections[otherNodeId].Contains(node.Id)) { // 이 노드의 ConnectedNodes에 그 노드를 추가 (중복 방지) if (!node.ConnectedNodes.Contains(otherNodeId)) { node.ConnectedNodes.Add(otherNodeId); } } } } } /// /// 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, "", "정상"); } } */ } } }