- 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>
308 lines
11 KiB
C#
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;
|
|
}
|
|
}
|
|
} |