Major improvements to AGV navigation system: • Consolidated RFID management into MapNode, removing duplicate RfidMapping class • Enhanced MapNode with RFID metadata fields (RfidStatus, RfidDescription) • Added automatic bidirectional connection generation in pathfinding algorithms • Updated all components to use unified MapNode-based RFID system • Added command line argument support for AGVMapEditor auto-loading files • Fixed pathfinding failures by ensuring proper node connectivity Technical changes: - Removed RfidMapping class and dependencies across all projects - Updated AStarPathfinder with EnsureBidirectionalConnections() method - Modified MapLoader to use AssignAutoRfidIds() for RFID automation - Enhanced UnifiedAGVCanvas, SimulatorForm, and MainForm for MapNode integration - Improved data consistency and reduced memory footprint 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
536 lines
15 KiB
C#
536 lines
15 KiB
C#
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;
|
|
|
|
namespace AGVNavigationCore.Controls
|
|
{
|
|
/// <summary>
|
|
/// 통합 AGV 캔버스 컨트롤
|
|
/// 맵 편집, AGV 시뮬레이션, 실시간 모니터링을 모두 지원
|
|
/// </summary>
|
|
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 = 30;
|
|
private const int CONNECTION_ARROW_SIZE = 8;
|
|
|
|
#endregion
|
|
|
|
#region Enums
|
|
|
|
/// <summary>
|
|
/// 캔버스 모드
|
|
/// </summary>
|
|
public enum CanvasMode
|
|
{
|
|
ViewOnly, // 읽기 전용 (시뮬레이터, 모니터링)
|
|
Edit // 편집 가능 (맵 에디터)
|
|
}
|
|
|
|
/// <summary>
|
|
/// 편집 모드 (CanvasMode.Edit일 때만 적용)
|
|
/// </summary>
|
|
public enum EditMode
|
|
{
|
|
Select, // 선택 모드
|
|
Move, // 이동 모드
|
|
AddNode, // 노드 추가 모드
|
|
Connect, // 연결 모드
|
|
Delete, // 삭제 모드
|
|
AddLabel, // 라벨 추가 모드
|
|
AddImage // 이미지 추가 모드
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Fields
|
|
|
|
// 캔버스 모드
|
|
private CanvasMode _canvasMode = CanvasMode.ViewOnly;
|
|
private EditMode _editMode = EditMode.Select;
|
|
|
|
// 맵 데이터
|
|
private List<MapNode> _nodes;
|
|
private MapNode _selectedNode;
|
|
private MapNode _hoveredNode;
|
|
|
|
// AGV 관련
|
|
private List<IAGV> _agvList;
|
|
private Dictionary<string, Point> _agvPositions;
|
|
private Dictionary<string, AgvDirection> _agvDirections;
|
|
private Dictionary<string, AGVState> _agvStates;
|
|
|
|
// 경로 관련
|
|
private PathResult _currentPath;
|
|
private List<PathResult> _allPaths;
|
|
|
|
// 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 Brush _normalNodeBrush;
|
|
private Brush _rotationNodeBrush;
|
|
private Brush _dockingNodeBrush;
|
|
private Brush _chargingNodeBrush;
|
|
private Brush _selectedNodeBrush;
|
|
private Brush _hoveredNodeBrush;
|
|
private Brush _gridBrush;
|
|
private Brush _agvBrush;
|
|
private Brush _pathBrush;
|
|
|
|
private Pen _connectionPen;
|
|
private Pen _gridPen;
|
|
private Pen _tempConnectionPen;
|
|
private Pen _selectedNodePen;
|
|
private Pen _pathPen;
|
|
private Pen _agvPen;
|
|
|
|
// 컨텍스트 메뉴
|
|
private ContextMenuStrip _contextMenu;
|
|
|
|
#endregion
|
|
|
|
#region Events
|
|
|
|
// 맵 편집 이벤트
|
|
public event EventHandler<MapNode> NodeAdded;
|
|
public event EventHandler<MapNode> NodeSelected;
|
|
public event EventHandler<MapNode> NodeDeleted;
|
|
public event EventHandler<MapNode> NodeMoved;
|
|
public event EventHandler MapChanged;
|
|
|
|
// AGV 이벤트
|
|
public event EventHandler<IAGV> AGVSelected;
|
|
public event EventHandler<IAGV> AGVStateChanged;
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
/// <summary>
|
|
/// 캔버스 모드
|
|
/// </summary>
|
|
public CanvasMode Mode
|
|
{
|
|
get => _canvasMode;
|
|
set
|
|
{
|
|
_canvasMode = value;
|
|
UpdateModeUI();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 편집 모드 (CanvasMode.Edit일 때만 적용)
|
|
/// </summary>
|
|
public EditMode CurrentEditMode
|
|
{
|
|
get => _editMode;
|
|
set
|
|
{
|
|
if (_canvasMode != CanvasMode.Edit) return;
|
|
|
|
_editMode = value;
|
|
if (_editMode != EditMode.Connect)
|
|
{
|
|
CancelConnection();
|
|
}
|
|
Cursor = GetCursorForMode(_editMode);
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 그리드 표시 여부
|
|
/// </summary>
|
|
public bool ShowGrid
|
|
{
|
|
get => _showGrid;
|
|
set
|
|
{
|
|
_showGrid = value;
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 줌 팩터
|
|
/// </summary>
|
|
public float ZoomFactor
|
|
{
|
|
get => _zoomFactor;
|
|
set
|
|
{
|
|
_zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value));
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 선택된 노드
|
|
/// </summary>
|
|
public MapNode SelectedNode => _selectedNode;
|
|
|
|
/// <summary>
|
|
/// 노드 목록
|
|
/// </summary>
|
|
public List<MapNode> Nodes
|
|
{
|
|
get => _nodes ?? new List<MapNode>();
|
|
set
|
|
{
|
|
_nodes = value ?? new List<MapNode>();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 목록
|
|
/// </summary>
|
|
public List<IAGV> AGVList
|
|
{
|
|
get => _agvList ?? new List<IAGV>();
|
|
set
|
|
{
|
|
_agvList = value ?? new List<IAGV>();
|
|
UpdateAGVData();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 표시할 경로
|
|
/// </summary>
|
|
public PathResult CurrentPath
|
|
{
|
|
get => _currentPath;
|
|
set
|
|
{
|
|
_currentPath = value;
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모든 경로 목록 (다중 AGV 경로 표시용)
|
|
/// </summary>
|
|
public List<PathResult> AllPaths
|
|
{
|
|
get => _allPaths ?? new List<PathResult>();
|
|
set
|
|
{
|
|
_allPaths = value ?? new List<PathResult>();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 회사 로고 이미지
|
|
/// </summary>
|
|
public Image CompanyLogo
|
|
{
|
|
get => _companyLogo;
|
|
set
|
|
{
|
|
_companyLogo = value;
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 측정 정보 텍스트
|
|
/// </summary>
|
|
public string MeasurementInfo
|
|
{
|
|
get => _measurementInfo;
|
|
set
|
|
{
|
|
_measurementInfo = value;
|
|
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<MapNode>();
|
|
_agvList = new List<IAGV>();
|
|
_agvPositions = new Dictionary<string, Point>();
|
|
_agvDirections = new Dictionary<string, AgvDirection>();
|
|
_agvStates = new Dictionary<string, AGVState>();
|
|
_allPaths = new List<PathResult>();
|
|
|
|
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);
|
|
|
|
// 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);
|
|
_pathPen = new Pen(Color.Purple, 3);
|
|
_agvPen = new Pen(Color.Red, 3);
|
|
}
|
|
|
|
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
|
|
|
|
/// <summary>
|
|
/// AGV 위치 업데이트
|
|
/// </summary>
|
|
public void UpdateAGVPosition(string agvId, Point position)
|
|
{
|
|
if (_agvPositions.ContainsKey(agvId))
|
|
_agvPositions[agvId] = position;
|
|
else
|
|
_agvPositions.Add(agvId, position);
|
|
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 방향 업데이트
|
|
/// </summary>
|
|
public void UpdateAGVDirection(string agvId, AgvDirection direction)
|
|
{
|
|
if (_agvDirections.ContainsKey(agvId))
|
|
_agvDirections[agvId] = direction;
|
|
else
|
|
_agvDirections.Add(agvId, direction);
|
|
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 상태 업데이트
|
|
/// </summary>
|
|
public void UpdateAGVState(string agvId, AGVState state)
|
|
{
|
|
if (_agvStates.ContainsKey(agvId))
|
|
_agvStates[agvId] = state;
|
|
else
|
|
_agvStates.Add(agvId, state);
|
|
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 위치 설정 (시뮬레이터용)
|
|
/// </summary>
|
|
/// <param name="agvId">AGV ID</param>
|
|
/// <param name="position">새로운 위치</param>
|
|
public void SetAGVPosition(string agvId, Point position)
|
|
{
|
|
UpdateAGVPosition(agvId, position);
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 데이터 동기화
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cleanup
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
|
|
// 브러쉬 정리
|
|
_normalNodeBrush?.Dispose();
|
|
_rotationNodeBrush?.Dispose();
|
|
_dockingNodeBrush?.Dispose();
|
|
_chargingNodeBrush?.Dispose();
|
|
_selectedNodeBrush?.Dispose();
|
|
_hoveredNodeBrush?.Dispose();
|
|
_gridBrush?.Dispose();
|
|
_agvBrush?.Dispose();
|
|
_pathBrush?.Dispose();
|
|
|
|
// 펜 정리
|
|
_connectionPen?.Dispose();
|
|
_gridPen?.Dispose();
|
|
_tempConnectionPen?.Dispose();
|
|
_selectedNodePen?.Dispose();
|
|
_pathPen?.Dispose();
|
|
_agvPen?.Dispose();
|
|
|
|
// 컨텍스트 메뉴 정리
|
|
_contextMenu?.Dispose();
|
|
|
|
// 이미지 정리
|
|
_companyLogo?.Dispose();
|
|
}
|
|
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
#region Interfaces
|
|
|
|
/// <summary>
|
|
/// AGV 인터페이스 (가상/실제 AGV 통합)
|
|
/// </summary>
|
|
public interface IAGV
|
|
{
|
|
string AgvId { get; }
|
|
Point CurrentPosition { get; }
|
|
AgvDirection CurrentDirection { get; }
|
|
AGVState CurrentState { get; }
|
|
float BatteryLevel { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 상태 열거형
|
|
/// </summary>
|
|
public enum AGVState
|
|
{
|
|
Idle, // 대기
|
|
Moving, // 이동 중
|
|
Rotating, // 회전 중
|
|
Docking, // 도킹 중
|
|
Charging, // 충전 중
|
|
Error // 오류
|
|
}
|
|
|
|
#endregion
|
|
} |