Files
ENIG/Cs_HMI/AGVMapEditor/Models/NodeResolver.cs
ChiKyun Kim 7567602479 feat: Add AGV Map Editor and Simulator tools
- Add AGVMapEditor: Visual map editing with drag-and-drop node placement
  * RFID mapping separation (physical ID ↔ logical node mapping)
  * A* pathfinding algorithm with AGV directional constraints
  * JSON map data persistence with structured format
  * Interactive map canvas with zoom/pan functionality

- Add AGVSimulator: Real-time AGV movement simulation
  * Virtual AGV with state machine (Idle, Moving, Rotating, Docking, Charging, Error)
  * Path execution and visualization from calculated routes
  * Real-time position tracking and battery simulation
  * Integration with map editor data format

- Update solution structure and build configuration
- Add comprehensive documentation in CLAUDE.md
- Implement AGV-specific constraints (forward/backward docking, rotation limits)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-10 17:39:23 +09:00

308 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
namespace AGVMapEditor.Models
{
/// <summary>
/// RFID 값을 논리적 노드로 변환하는 클래스
/// 실제 AGV 시스템에서 RFID 리더가 읽은 값을 맵 노드 정보로 변환
/// </summary>
public class NodeResolver
{
private List<RfidMapping> _rfidMappings;
private List<MapNode> _mapNodes;
/// <summary>
/// 기본 생성자
/// </summary>
public NodeResolver()
{
_rfidMappings = new List<RfidMapping>();
_mapNodes = new List<MapNode>();
}
/// <summary>
/// 매개변수 생성자
/// </summary>
/// <param name="rfidMappings">RFID 매핑 목록</param>
/// <param name="mapNodes">맵 노드 목록</param>
public NodeResolver(List<RfidMapping> rfidMappings, List<MapNode> mapNodes)
{
_rfidMappings = rfidMappings ?? new List<RfidMapping>();
_mapNodes = mapNodes ?? new List<MapNode>();
}
/// <summary>
/// RFID 값으로 맵 노드 검색
/// </summary>
/// <param name="rfidValue">RFID 리더에서 읽은 값</param>
/// <returns>해당하는 맵 노드, 없으면 null</returns>
public MapNode GetNodeByRfid(string rfidValue)
{
if (string.IsNullOrEmpty(rfidValue))
return null;
// 1. RFID 매핑에서 논리적 노드 ID 찾기
var mapping = _rfidMappings.FirstOrDefault(m =>
m.RfidId.Equals(rfidValue, StringComparison.OrdinalIgnoreCase) && m.IsActive);
if (mapping == null)
return null;
// 2. 논리적 노드 ID로 실제 맵 노드 찾기
var mapNode = _mapNodes.FirstOrDefault(n =>
n.NodeId.Equals(mapping.LogicalNodeId, StringComparison.OrdinalIgnoreCase) && n.IsActive);
return mapNode;
}
/// <summary>
/// 논리적 노드 ID로 맵 노드 검색
/// </summary>
/// <param name="nodeId">논리적 노드 ID</param>
/// <returns>해당하는 맵 노드, 없으면 null</returns>
public MapNode GetNodeById(string nodeId)
{
if (string.IsNullOrEmpty(nodeId))
return null;
return _mapNodes.FirstOrDefault(n =>
n.NodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase) && n.IsActive);
}
/// <summary>
/// 맵 노드로 연결된 RFID 값 검색
/// </summary>
/// <param name="nodeId">논리적 노드 ID</param>
/// <returns>연결된 RFID 값, 없으면 null</returns>
public string GetRfidByNodeId(string nodeId)
{
if (string.IsNullOrEmpty(nodeId))
return null;
var mapping = _rfidMappings.FirstOrDefault(m =>
m.LogicalNodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase) && m.IsActive);
return mapping?.RfidId;
}
/// <summary>
/// RFID 매핑 정보 검색
/// </summary>
/// <param name="rfidValue">RFID 값</param>
/// <returns>매핑 정보, 없으면 null</returns>
public RfidMapping GetRfidMapping(string rfidValue)
{
if (string.IsNullOrEmpty(rfidValue))
return null;
return _rfidMappings.FirstOrDefault(m =>
m.RfidId.Equals(rfidValue, StringComparison.OrdinalIgnoreCase) && m.IsActive);
}
/// <summary>
/// 새로운 RFID 매핑 추가
/// </summary>
/// <param name="rfidId">RFID 값</param>
/// <param name="nodeId">논리적 노드 ID</param>
/// <param name="description">설명</param>
/// <returns>추가 성공 여부</returns>
public bool AddRfidMapping(string rfidId, string nodeId, string description = "")
{
if (string.IsNullOrEmpty(rfidId) || string.IsNullOrEmpty(nodeId))
return false;
// 중복 RFID 체크
if (_rfidMappings.Any(m => m.RfidId.Equals(rfidId, StringComparison.OrdinalIgnoreCase)))
return false;
// 해당 노드 존재 체크
if (!_mapNodes.Any(n => n.NodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase)))
return false;
var mapping = new RfidMapping(rfidId, nodeId, description);
_rfidMappings.Add(mapping);
return true;
}
/// <summary>
/// RFID 매핑 제거
/// </summary>
/// <param name="rfidId">제거할 RFID 값</param>
/// <returns>제거 성공 여부</returns>
public bool RemoveRfidMapping(string rfidId)
{
if (string.IsNullOrEmpty(rfidId))
return false;
var mapping = _rfidMappings.FirstOrDefault(m =>
m.RfidId.Equals(rfidId, StringComparison.OrdinalIgnoreCase));
if (mapping != null)
{
_rfidMappings.Remove(mapping);
return true;
}
return false;
}
/// <summary>
/// 맵 노드 추가
/// </summary>
/// <param name="node">추가할 맵 노드</param>
/// <returns>추가 성공 여부</returns>
public bool AddMapNode(MapNode node)
{
if (node == null || string.IsNullOrEmpty(node.NodeId))
return false;
// 중복 노드 ID 체크
if (_mapNodes.Any(n => n.NodeId.Equals(node.NodeId, StringComparison.OrdinalIgnoreCase)))
return false;
_mapNodes.Add(node);
return true;
}
/// <summary>
/// 맵 노드 제거
/// </summary>
/// <param name="nodeId">제거할 노드 ID</param>
/// <returns>제거 성공 여부</returns>
public bool RemoveMapNode(string nodeId)
{
if (string.IsNullOrEmpty(nodeId))
return false;
var node = _mapNodes.FirstOrDefault(n =>
n.NodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase));
if (node != null)
{
// 연관된 RFID 매핑도 함께 제거
var associatedMappings = _rfidMappings.Where(m =>
m.LogicalNodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase)).ToList();
foreach (var mapping in associatedMappings)
{
_rfidMappings.Remove(mapping);
}
// 다른 노드의 연결 정보에서도 제거
foreach (var otherNode in _mapNodes.Where(n => n.ConnectedNodes.Contains(nodeId)))
{
otherNode.RemoveConnection(nodeId);
}
_mapNodes.Remove(node);
return true;
}
return false;
}
/// <summary>
/// 특정 타입의 노드들 검색
/// </summary>
/// <param name="nodeType">노드 타입</param>
/// <returns>해당 타입의 노드 목록</returns>
public List<MapNode> GetNodesByType(NodeType nodeType)
{
return _mapNodes.Where(n => n.Type == nodeType && n.IsActive).ToList();
}
/// <summary>
/// 장비 ID로 노드 검색
/// </summary>
/// <param name="stationId">장비 ID</param>
/// <returns>해당 장비의 노드, 없으면 null</returns>
public MapNode GetNodeByStationId(string stationId)
{
if (string.IsNullOrEmpty(stationId))
return null;
return _mapNodes.FirstOrDefault(n =>
n.StationId.Equals(stationId, StringComparison.OrdinalIgnoreCase) && n.IsActive);
}
/// <summary>
/// 매핑되지 않은 노드들 검색 (RFID가 연결되지 않은 노드)
/// </summary>
/// <returns>매핑되지 않은 노드 목록</returns>
public List<MapNode> GetUnmappedNodes()
{
var mappedNodeIds = _rfidMappings.Where(m => m.IsActive)
.Select(m => m.LogicalNodeId)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
return _mapNodes.Where(n => n.IsActive && !mappedNodeIds.Contains(n.NodeId)).ToList();
}
/// <summary>
/// 사용되지 않는 RFID 매핑들 검색 (노드가 삭제된 매핑)
/// </summary>
/// <returns>사용되지 않는 매핑 목록</returns>
public List<RfidMapping> GetOrphanedMappings()
{
var activeNodeIds = _mapNodes.Where(n => n.IsActive)
.Select(n => n.NodeId)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
return _rfidMappings.Where(m => m.IsActive && !activeNodeIds.Contains(m.LogicalNodeId)).ToList();
}
/// <summary>
/// 데이터 초기화
/// </summary>
public void Clear()
{
_rfidMappings.Clear();
_mapNodes.Clear();
}
/// <summary>
/// 데이터 유효성 검증
/// </summary>
/// <returns>검증 결과 메시지 목록</returns>
public List<string> ValidateData()
{
var errors = new List<string>();
// 중복 RFID 체크
var duplicateRfids = _rfidMappings.GroupBy(m => m.RfidId.ToLower())
.Where(g => g.Count() > 1)
.Select(g => g.Key);
foreach (var rfid in duplicateRfids)
{
errors.Add($"중복된 RFID: {rfid}");
}
// 중복 노드 ID 체크
var duplicateNodeIds = _mapNodes.GroupBy(n => n.NodeId.ToLower())
.Where(g => g.Count() > 1)
.Select(g => g.Key);
foreach (var nodeId in duplicateNodeIds)
{
errors.Add($"중복된 노드 ID: {nodeId}");
}
// 고아 매핑 체크
var orphanedMappings = GetOrphanedMappings();
foreach (var mapping in orphanedMappings)
{
errors.Add($"존재하지 않는 노드를 참조하는 RFID 매핑: {mapping.RfidId} → {mapping.LogicalNodeId}");
}
// 매핑되지 않은 노드 경고 (에러는 아님)
var unmappedNodes = GetUnmappedNodes();
foreach (var node in unmappedNodes)
{
errors.Add($"RFID가 매핑되지 않은 노드: {node.NodeId} ({node.Name})");
}
return errors;
}
}
}