using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; using AGVNavigationCore.Models; using AGVNavigationCore.PathFinding; using AGVNavigationCore.PathFinding.Core; namespace AGVNavigationCore.Controls { /// /// 통합 AGV 캔버스 컨트롤 /// 맵 편집, AGV 시뮬레이션, 실시간 모니터링을 모두 지원 /// public partial class UnifiedAGVCanvas : UserControl { #region Constants private const int NODE_SIZE = 24; private const int NODE_RADIUS = NODE_SIZE / 2; private const int GRID_SIZE = 20; private const float CONNECTION_WIDTH = 2.0f; private const int SNAP_DISTANCE = 10; private const int AGV_SIZE = 40; private const int CONNECTION_ARROW_SIZE = 8; #endregion #region Enums /// /// 캔버스 모드 /// public enum CanvasMode { ViewOnly, // 읽기 전용 (시뮬레이터, 모니터링) Edit // 편집 가능 (맵 에디터) } /// /// 편집 모드 (CanvasMode.Edit일 때만 적용) /// public enum EditMode { Select, // 선택 모드 Move, // 이동 모드 AddNode, // 노드 추가 모드 Connect, // 연결 모드 Delete, // 삭제 모드 DeleteConnection, // 연결 삭제 모드 AddLabel, // 라벨 추가 모드 AddImage, // 이미지 추가 모드 SelectTarget // 목적지 선택 모드 (시뮬레이터 전용) } #endregion #region Fields // 캔버스 모드 private CanvasMode _canvasMode = CanvasMode.ViewOnly; private EditMode _editMode = EditMode.Select; // 맵 데이터 private List _nodes; private MapNode _selectedNode; private MapNode _hoveredNode; private MapNode _destinationNode; // AGV 관련 private List _agvList; private Dictionary _agvPositions; private Dictionary _agvDirections; private Dictionary _agvStates; // 경로 관련 private AGVPathResult _currentPath; private List _allPaths; // 도킹 검증 관련 private Dictionary _dockingErrors; // UI 요소들 private Image _companyLogo; private string _companyLogoPath = string.Empty; private string _measurementInfo = "스케일: 1:100\n면적: 1000㎡\n최종 수정: " + DateTime.Now.ToString("yyyy-MM-dd"); // 편집 관련 (EditMode에서만 사용) private bool _isDragging; private Point _dragOffset; private Point _lastMousePosition; private bool _isConnectionMode; private MapNode _connectionStartNode; private Point _connectionEndPoint; // 그리드 및 줌 관련 private bool _showGrid = true; private float _zoomFactor = 1.0f; private Point _panOffset = Point.Empty; private bool _isPanning; // 자동 증가 카운터 private int _nodeCounter = 1; // 강조 연결 private (string FromNodeId, string ToNodeId)? _highlightedConnection = null; // RFID 중복 검사 private HashSet _duplicateRfidNodes = new HashSet(); // 브러쉬 및 펜 private Brush _normalNodeBrush; private Brush _rotationNodeBrush; private Brush _dockingNodeBrush; private Brush _chargingNodeBrush; private Brush _selectedNodeBrush; private Brush _hoveredNodeBrush; private Brush _destinationNodeBrush; private Brush _gridBrush; private Brush _agvBrush; private Brush _pathBrush; private Pen _connectionPen; private Pen _gridPen; private Pen _tempConnectionPen; private Pen _selectedNodePen; private Pen _destinationNodePen; private Pen _pathPen; private Pen _agvPen; private Pen _highlightedConnectionPen; // 컨텍스트 메뉴 private ContextMenuStrip _contextMenu; #endregion #region Events // 맵 편집 이벤트 public event EventHandler NodeAdded; public event EventHandler NodeSelected; public event EventHandler NodeDeleted; public event EventHandler NodeMoved; public event EventHandler<(MapNode From, MapNode To)> ConnectionDeleted; public event EventHandler MapChanged; // AGV 이벤트 public event EventHandler AGVSelected; public event EventHandler AGVStateChanged; // 시뮬레이터 이벤트 public event EventHandler TargetNodeSelected; #endregion #region Properties /// /// 캔버스 모드 /// public CanvasMode Mode { get => _canvasMode; set { _canvasMode = value; UpdateModeUI(); Invalidate(); } } /// /// 편집 모드 (CanvasMode.Edit일 때만 적용) /// public EditMode CurrentEditMode { get => _editMode; set { if (_canvasMode != CanvasMode.Edit) return; _editMode = value; if (_editMode != EditMode.Connect) { CancelConnection(); } Cursor = GetCursorForMode(_editMode); Invalidate(); } } /// /// 그리드 표시 여부 /// public bool ShowGrid { get => _showGrid; set { _showGrid = value; Invalidate(); } } /// /// 줌 팩터 /// public float ZoomFactor { get => _zoomFactor; set { _zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value)); Invalidate(); } } /// /// 선택된 노드 /// public MapNode SelectedNode => _selectedNode; /// /// 노드 목록 /// public List Nodes { get => _nodes ?? new List(); set { _nodes = value ?? new List(); // 기존 노드들의 최대 번호를 찾아서 _nodeCounter 설정 UpdateNodeCounter(); // RFID 중복값 검사 DetectDuplicateRfidNodes(); Invalidate(); } } /// /// AGV 목록 /// public List AGVList { get => _agvList ?? new List(); set { _agvList = value ?? new List(); UpdateAGVData(); Invalidate(); } } /// /// 현재 표시할 경로 /// public AGVPathResult CurrentPath { get => _currentPath; set { _currentPath = value; UpdateDestinationNode(); Invalidate(); } } /// /// 모든 경로 목록 (다중 AGV 경로 표시용) /// public List AllPaths { get => _allPaths ?? new List(); set { _allPaths = value ?? new List(); Invalidate(); } } /// /// 회사 로고 이미지 /// public Image CompanyLogo { get => _companyLogo; set { _companyLogo = value; Invalidate(); } } /// /// 측정 정보 텍스트 /// public string MeasurementInfo { get => _measurementInfo; set { _measurementInfo = value; Invalidate(); } } #endregion #region Connection Highlighting /// /// 특정 연결을 강조 표시 /// /// 시작 노드 ID /// 끝 노드 ID public void HighlightConnection(string fromNodeId, string toNodeId) { if (string.IsNullOrEmpty(fromNodeId) || string.IsNullOrEmpty(toNodeId)) { _highlightedConnection = null; } else { // 사전순으로 정렬하여 저장 (연결이 단일 방향으로 저장되므로) if (string.Compare(fromNodeId, toNodeId, StringComparison.Ordinal) <= 0) { _highlightedConnection = (fromNodeId, toNodeId); } else { _highlightedConnection = (toNodeId, fromNodeId); } } Invalidate(); } /// /// 연결 강조 표시 해제 /// public void ClearHighlightedConnection() { _highlightedConnection = null; Invalidate(); } #endregion #region Constructor public UnifiedAGVCanvas() { InitializeComponent(); InitializeCanvas(); } #endregion #region Initialization private void InitializeCanvas() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw, true); _nodes = new List(); _agvList = new List(); _agvPositions = new Dictionary(); _agvDirections = new Dictionary(); _agvStates = new Dictionary(); _allPaths = new List(); _dockingErrors = new Dictionary(); InitializeBrushesAndPens(); CreateContextMenu(); } private void InitializeBrushesAndPens() { // 노드 브러쉬 _normalNodeBrush = new SolidBrush(Color.LightBlue); _rotationNodeBrush = new SolidBrush(Color.Yellow); _dockingNodeBrush = new SolidBrush(Color.Orange); _chargingNodeBrush = new SolidBrush(Color.Green); _selectedNodeBrush = new SolidBrush(Color.Red); _hoveredNodeBrush = new SolidBrush(Color.LightCyan); _destinationNodeBrush = new SolidBrush(Color.Gold); // AGV 및 경로 브러쉬 _agvBrush = new SolidBrush(Color.Red); _pathBrush = new SolidBrush(Color.Purple); // 그리드 브러쉬 _gridBrush = new SolidBrush(Color.LightGray); // 펜 _connectionPen = new Pen(Color.DarkBlue, CONNECTION_WIDTH); _connectionPen.EndCap = LineCap.ArrowAnchor; _gridPen = new Pen(Color.LightGray, 1); _tempConnectionPen = new Pen(Color.Orange, 2) { DashStyle = DashStyle.Dash }; _selectedNodePen = new Pen(Color.Red, 3); _destinationNodePen = new Pen(Color.Orange, 4); _pathPen = new Pen(Color.Purple, 3); _agvPen = new Pen(Color.Red, 3); _highlightedConnectionPen = new Pen(Color.Red, 4) { DashStyle = DashStyle.Solid }; } private void CreateContextMenu() { _contextMenu = new ContextMenuStrip(); // 컨텍스트 메뉴는 EditMode에서만 사용 } private void UpdateModeUI() { // 모드에 따른 UI 업데이트 if (_canvasMode == CanvasMode.ViewOnly) { Cursor = Cursors.Default; _contextMenu.Enabled = false; } else { _contextMenu.Enabled = true; Cursor = GetCursorForMode(_editMode); } } #endregion #region AGV Management /// /// AGV 위치 업데이트 /// public void UpdateAGVPosition(string agvId, Point position) { if (_agvPositions.ContainsKey(agvId)) _agvPositions[agvId] = position; else _agvPositions.Add(agvId, position); Invalidate(); } /// /// AGV 방향 업데이트 /// public void UpdateAGVDirection(string agvId, AgvDirection direction) { if (_agvDirections.ContainsKey(agvId)) _agvDirections[agvId] = direction; else _agvDirections.Add(agvId, direction); Invalidate(); } /// /// AGV 상태 업데이트 /// public void UpdateAGVState(string agvId, AGVState state) { if (_agvStates.ContainsKey(agvId)) _agvStates[agvId] = state; else _agvStates.Add(agvId, state); Invalidate(); } /// /// AGV 위치 설정 (시뮬레이터용) /// /// AGV ID /// 새로운 위치 public void SetAGVPosition(string agvId, Point position) { UpdateAGVPosition(agvId, position); } /// /// AGV 데이터 동기화 /// private void UpdateAGVData() { if (_agvList == null) return; foreach (var agv in _agvList) { UpdateAGVPosition(agv.AgvId, agv.CurrentPosition); UpdateAGVDirection(agv.AgvId, agv.CurrentDirection); UpdateAGVState(agv.AgvId, agv.CurrentState); } } #endregion #region Helper Methods private Cursor GetCursorForMode(EditMode mode) { if (_canvasMode != CanvasMode.Edit) return Cursors.Default; switch (mode) { case EditMode.AddNode: return Cursors.Cross; case EditMode.Move: return Cursors.SizeAll; case EditMode.Connect: return Cursors.Hand; case EditMode.Delete: return Cursors.No; default: return Cursors.Default; } } private void CancelConnection() { _isConnectionMode = false; _connectionStartNode = null; _connectionEndPoint = Point.Empty; Invalidate(); } private void UpdateDestinationNode() { _destinationNode = null; if (_currentPath != null && _currentPath.Success && _currentPath.Path != null && _currentPath.Path.Count > 0) { // 경로의 마지막 노드가 목적지 string destinationNodeId = _currentPath.Path[_currentPath.Path.Count - 1]; // 노드 목록에서 해당 노드 찾기 _destinationNode = _nodes?.FirstOrDefault(n => n.NodeId == destinationNodeId); } } #endregion #region Cleanup protected override void Dispose(bool disposing) { if (disposing) { // 브러쉬 정리 _normalNodeBrush?.Dispose(); _rotationNodeBrush?.Dispose(); _dockingNodeBrush?.Dispose(); _chargingNodeBrush?.Dispose(); _selectedNodeBrush?.Dispose(); _hoveredNodeBrush?.Dispose(); _destinationNodeBrush?.Dispose(); _gridBrush?.Dispose(); _agvBrush?.Dispose(); _pathBrush?.Dispose(); // 펜 정리 _connectionPen?.Dispose(); _gridPen?.Dispose(); _tempConnectionPen?.Dispose(); _selectedNodePen?.Dispose(); _destinationNodePen?.Dispose(); _pathPen?.Dispose(); _agvPen?.Dispose(); _highlightedConnectionPen?.Dispose(); // 컨텍스트 메뉴 정리 _contextMenu?.Dispose(); // 이미지 정리 _companyLogo?.Dispose(); } base.Dispose(disposing); } #endregion /// /// RFID 중복값을 가진 노드들을 감지하고 표시 /// 나중에 추가된 노드(인덱스가 더 큰)를 중복으로 간주 /// private void DetectDuplicateRfidNodes() { _duplicateRfidNodes.Clear(); if (_nodes == null || _nodes.Count == 0) return; // RFID값과 해당 노드의 인덱스를 저장 var rfidToNodeIndex = new Dictionary>(); // 모든 노드의 RFID값 수집 for (int i = 0; i < _nodes.Count; i++) { var node = _nodes[i]; if (!string.IsNullOrEmpty(node.RfidId)) { if (!rfidToNodeIndex.ContainsKey(node.RfidId)) { rfidToNodeIndex[node.RfidId] = new List(); } rfidToNodeIndex[node.RfidId].Add(i); } } // 중복된 RFID를 가진 노드들을 찾아서 나중에 추가된 것들을 표시 foreach (var kvp in rfidToNodeIndex) { if (kvp.Value.Count > 1) { // 첫 번째 노드는 원본으로 유지, 나머지는 중복으로 표시 for (int i = 1; i < kvp.Value.Count; i++) { int duplicateNodeIndex = kvp.Value[i]; _duplicateRfidNodes.Add(_nodes[duplicateNodeIndex].NodeId); } } } } /// /// 기존 노드들의 최대 번호를 찾아서 _nodeCounter를 업데이트 /// private void UpdateNodeCounter() { if (_nodes == null || _nodes.Count == 0) { _nodeCounter = 1; return; } int maxNumber = 0; foreach (var node in _nodes) { // NodeId에서 숫자 부분 추출 (예: "N001" -> 1) if (node.NodeId.StartsWith("N") && int.TryParse(node.NodeId.Substring(1), out int number)) { maxNumber = Math.Max(maxNumber, number); } } _nodeCounter = maxNumber + 1; } /// /// 특정 노드에 도킹 오류 표시를 설정/해제합니다. /// /// 노드 ID /// 오류 여부 public void SetDockingError(string nodeId, bool hasError) { if (string.IsNullOrEmpty(nodeId)) return; if (hasError) { _dockingErrors[nodeId] = true; } else { _dockingErrors.Remove(nodeId); } Invalidate(); // 화면 다시 그리기 } /// /// 특정 노드에 도킹 오류가 있는지 확인합니다. /// /// 노드 ID /// 도킹 오류 여부 public bool HasDockingError(string nodeId) { return _dockingErrors.ContainsKey(nodeId) && _dockingErrors[nodeId]; } /// /// 모든 도킹 오류를 초기화합니다. /// public void ClearDockingErrors() { _dockingErrors.Clear(); Invalidate(); } } }