Files
ENIG/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.cs
ChiKyun Kim de0e39e030 refactor: Consolidate RFID mapping and add bidirectional pathfinding
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>
2025-09-11 16:41:52 +09:00

622 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using AGVMapEditor.Models;
using AGVNavigationCore.Models;
using AGVNavigationCore.PathFinding;
using AGVSimulator.Models;
namespace AGVSimulator.Controls
{
/// <summary>
/// AGV 시뮬레이션 시각화 캔버스
/// </summary>
public partial class SimulatorCanvas : UserControl
{
#region Fields
private List<MapNode> _mapNodes;
private List<VirtualAGV> _agvList;
private PathResult _currentPath;
// 그래픽 설정
private float _zoom = 1.0f;
private Point _panOffset = Point.Empty;
private bool _isPanning = false;
private Point _lastMousePos = Point.Empty;
// 색상 설정
private readonly Brush _normalNodeBrush = Brushes.LightBlue;
private readonly Brush _rotationNodeBrush = Brushes.Yellow;
private readonly Brush _dockingNodeBrush = Brushes.Orange;
private readonly Brush _chargingNodeBrush = Brushes.Green;
private readonly Brush _agvBrush = Brushes.Red;
private readonly Brush _pathBrush = Brushes.Purple;
private readonly Pen _connectionPen = new Pen(Color.Gray, 2);
private readonly Pen _pathPen = new Pen(Color.Purple, 3);
private readonly Pen _agvPen = new Pen(Color.Red, 3);
// 크기 설정
private const int NODE_SIZE = 20;
private const int AGV_SIZE = 30;
private const int CONNECTION_ARROW_SIZE = 8;
#endregion
#region Properties
/// <summary>
/// 맵 노드 목록
/// </summary>
public List<MapNode> MapNodes
{
get => _mapNodes;
set
{
_mapNodes = value;
Invalidate();
}
}
/// <summary>
/// AGV 목록
/// </summary>
public List<VirtualAGV> AGVList
{
get => _agvList;
set
{
_agvList = value;
Invalidate();
}
}
/// <summary>
/// 현재 경로
/// </summary>
public PathResult CurrentPath
{
get => _currentPath;
set
{
_currentPath = value;
Invalidate();
}
}
#endregion
#region Constructor
public SimulatorCanvas()
{
InitializeComponent();
InitializeCanvas();
}
#endregion
#region Initialization
private void InitializeCanvas()
{
_mapNodes = new List<MapNode>();
_agvList = new List<VirtualAGV>();
SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.DoubleBuffer |
ControlStyles.ResizeRedraw, true);
BackColor = Color.White;
// 마우스 이벤트 연결
MouseDown += OnMouseDown;
MouseMove += OnMouseMove;
MouseUp += OnMouseUp;
MouseWheel += OnMouseWheel;
}
#endregion
#region Public Methods
/// <summary>
/// AGV 추가
/// </summary>
public void AddAGV(VirtualAGV agv)
{
if (_agvList == null)
_agvList = new List<VirtualAGV>();
_agvList.Add(agv);
// AGV 이벤트 연결
agv.PositionChanged += OnAGVPositionChanged;
agv.StateChanged += OnAGVStateChanged;
Invalidate();
}
/// <summary>
/// AGV 제거
/// </summary>
public void RemoveAGV(string agvId)
{
var agv = _agvList?.FirstOrDefault(a => a.AgvId == agvId);
if (agv != null)
{
// 이벤트 연결 해제
agv.PositionChanged -= OnAGVPositionChanged;
agv.StateChanged -= OnAGVStateChanged;
_agvList.Remove(agv);
Invalidate();
}
}
/// <summary>
/// 모든 AGV 제거
/// </summary>
public void ClearAGVs()
{
if (_agvList != null)
{
foreach (var agv in _agvList)
{
agv.PositionChanged -= OnAGVPositionChanged;
agv.StateChanged -= OnAGVStateChanged;
}
_agvList.Clear();
Invalidate();
}
}
/// <summary>
/// 확대/축소 초기화
/// </summary>
public void ResetZoom()
{
_zoom = 1.0f;
_panOffset = Point.Empty;
Invalidate();
}
/// <summary>
/// 맵 전체 맞춤
/// </summary>
public void FitToMap()
{
if (_mapNodes == null || _mapNodes.Count == 0)
return;
var minX = _mapNodes.Min(n => n.Position.X);
var maxX = _mapNodes.Max(n => n.Position.X);
var minY = _mapNodes.Min(n => n.Position.Y);
var maxY = _mapNodes.Max(n => n.Position.Y);
var mapWidth = maxX - minX + 100; // 여백 추가
var mapHeight = maxY - minY + 100;
var zoomX = (float)Width / mapWidth;
var zoomY = (float)Height / mapHeight;
_zoom = Math.Min(zoomX, zoomY) * 0.9f; // 약간의 여백
_panOffset = new Point(
(int)((Width - mapWidth * _zoom) / 2 - minX * _zoom),
(int)((Height - mapHeight * _zoom) / 2 - minY * _zoom)
);
Invalidate();
}
#endregion
#region Event Handlers
private void OnAGVPositionChanged(object sender, Point newPosition)
{
Invalidate(); // AGV 위치 변경시 화면 갱신
}
private void OnAGVStateChanged(object sender, AGVState newState)
{
Invalidate(); // AGV 상태 변경시 화면 갱신
}
private void OnMouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
_isPanning = true;
_lastMousePos = e.Location;
Cursor = Cursors.Hand;
}
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (_isPanning)
{
var deltaX = e.X - _lastMousePos.X;
var deltaY = e.Y - _lastMousePos.Y;
_panOffset = new Point(
_panOffset.X + deltaX,
_panOffset.Y + deltaY
);
_lastMousePos = e.Location;
Invalidate();
}
}
private void OnMouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
_isPanning = false;
Cursor = Cursors.Default;
}
}
private void OnMouseWheel(object sender, MouseEventArgs e)
{
var zoomFactor = e.Delta > 0 ? 1.1f : 0.9f;
var newZoom = _zoom * zoomFactor;
if (newZoom >= 0.1f && newZoom <= 10.0f)
{
// 마우스 위치 기준으로 줌
var mouseX = e.X - _panOffset.X;
var mouseY = e.Y - _panOffset.Y;
_panOffset = new Point(
(int)(_panOffset.X - mouseX * (zoomFactor - 1)),
(int)(_panOffset.Y - mouseY * (zoomFactor - 1))
);
_zoom = newZoom;
Invalidate();
}
}
#endregion
#region Painting
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var g = e.Graphics;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
// 변환 행렬 설정
g.TranslateTransform(_panOffset.X, _panOffset.Y);
g.ScaleTransform(_zoom, _zoom);
// 배경 그리드 그리기
DrawGrid(g);
// 맵 노드 연결선 그리기
DrawNodeConnections(g);
// 경로 그리기
if (_currentPath != null && _currentPath.Success)
{
DrawPath(g);
}
// 맵 노드 그리기
DrawMapNodes(g);
// AGV 그리기
DrawAGVs(g);
// 정보 표시 (변환 해제)
g.ResetTransform();
DrawInfo(g);
}
private void DrawGrid(Graphics g)
{
var gridSize = 50;
var pen = new Pen(Color.LightGray, 1);
var startX = -(int)(_panOffset.X / _zoom / gridSize) * gridSize;
var startY = -(int)(_panOffset.Y / _zoom / gridSize) * gridSize;
var endX = startX + (int)(Width / _zoom) + gridSize;
var endY = startY + (int)(Height / _zoom) + gridSize;
for (int x = startX; x <= endX; x += gridSize)
{
g.DrawLine(pen, x, startY, x, endY);
}
for (int y = startY; y <= endY; y += gridSize)
{
g.DrawLine(pen, startX, y, endX, y);
}
pen.Dispose();
}
private void DrawNodeConnections(Graphics g)
{
if (_mapNodes == null) return;
foreach (var node in _mapNodes)
{
if (node.ConnectedNodes != null)
{
foreach (var connectedNodeId in node.ConnectedNodes)
{
var connectedNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
if (connectedNode != null)
{
DrawConnection(g, node.Position, connectedNode.Position);
}
}
}
}
}
private void DrawConnection(Graphics g, Point from, Point to)
{
g.DrawLine(_connectionPen, from, to);
// 방향 화살표 그리기
var angle = Math.Atan2(to.Y - from.Y, to.X - from.X);
var arrowX = to.X - CONNECTION_ARROW_SIZE * Math.Cos(angle);
var arrowY = to.Y - CONNECTION_ARROW_SIZE * Math.Sin(angle);
var arrowPoint1 = new PointF(
(float)(arrowX - CONNECTION_ARROW_SIZE * Math.Cos(angle - Math.PI / 6)),
(float)(arrowY - CONNECTION_ARROW_SIZE * Math.Sin(angle - Math.PI / 6))
);
var arrowPoint2 = new PointF(
(float)(arrowX - CONNECTION_ARROW_SIZE * Math.Cos(angle + Math.PI / 6)),
(float)(arrowY - CONNECTION_ARROW_SIZE * Math.Sin(angle + Math.PI / 6))
);
g.DrawLine(_connectionPen, to, arrowPoint1);
g.DrawLine(_connectionPen, to, arrowPoint2);
}
private void DrawPath(Graphics g)
{
if (_currentPath?.Path == null || _currentPath.Path.Count < 2)
return;
for (int i = 0; i < _currentPath.Path.Count - 1; i++)
{
var currentNodeId = _currentPath.Path[i];
var nextNodeId = _currentPath.Path[i + 1];
var currentNode = _mapNodes?.FirstOrDefault(n => n.NodeId == currentNodeId);
var nextNode = _mapNodes?.FirstOrDefault(n => n.NodeId == nextNodeId);
if (currentNode != null && nextNode != null)
{
g.DrawLine(_pathPen, currentNode.Position, nextNode.Position);
}
}
}
private void DrawMapNodes(Graphics g)
{
if (_mapNodes == null) return;
foreach (var node in _mapNodes)
{
DrawMapNode(g, node);
}
}
private void DrawMapNode(Graphics g, MapNode node)
{
var brush = GetNodeBrush(node.Type);
var rect = new Rectangle(
node.Position.X - NODE_SIZE / 2,
node.Position.Y - NODE_SIZE / 2,
NODE_SIZE,
NODE_SIZE
);
// 노드 그리기
if (node.Type == NodeType.Rotation)
{
g.FillEllipse(brush, rect); // 회전 노드는 원형
}
else
{
g.FillRectangle(brush, rect); // 일반 노드는 사각형
}
g.DrawRectangle(Pens.Black, rect);
// 노드 ID 표시
var font = new Font("Arial", 8);
var textSize = g.MeasureString(node.NodeId, font);
var textPos = new PointF(
node.Position.X - textSize.Width / 2,
node.Position.Y + NODE_SIZE / 2 + 2
);
g.DrawString(node.NodeId, font, Brushes.Black, textPos);
font.Dispose();
}
private Brush GetNodeBrush(NodeType nodeType)
{
switch (nodeType)
{
case NodeType.Rotation: return _rotationNodeBrush;
case NodeType.Docking: return _dockingNodeBrush;
case NodeType.Charging: return _chargingNodeBrush;
default: return _normalNodeBrush;
}
}
private void DrawAGVs(Graphics g)
{
if (_agvList == null) return;
foreach (var agv in _agvList)
{
DrawAGV(g, agv);
}
}
private void DrawAGV(Graphics g, VirtualAGV agv)
{
var position = agv.CurrentPosition;
var rect = new Rectangle(
position.X - AGV_SIZE / 2,
position.Y - AGV_SIZE / 2,
AGV_SIZE,
AGV_SIZE
);
// AGV 상태에 따른 색상 변경
var brush = GetAGVBrush(agv.CurrentState);
// AGV 본체 그리기
g.FillEllipse(brush, rect);
g.DrawEllipse(_agvPen, rect);
// 방향 표시
DrawAGVDirection(g, position, agv.CurrentDirection);
// AGV ID 표시
var font = new Font("Arial", 10, FontStyle.Bold);
var textSize = g.MeasureString(agv.AgvId, font);
var textPos = new PointF(
position.X - textSize.Width / 2,
position.Y + AGV_SIZE / 2 + 5
);
g.DrawString(agv.AgvId, font, Brushes.Black, textPos);
font.Dispose();
}
private Brush GetAGVBrush(AGVState state)
{
switch (state)
{
case AGVState.Moving: return Brushes.Blue;
case AGVState.Rotating: return Brushes.Yellow;
case AGVState.Docking: return Brushes.Orange;
case AGVState.Charging: return Brushes.Green;
case AGVState.Error: return Brushes.Red;
default: return Brushes.Gray; // Idle
}
}
private void DrawAGVDirection(Graphics g, Point position, AgvDirection direction)
{
var arrowSize = 10;
var pen = new Pen(Color.White, 2);
switch (direction)
{
case AgvDirection.Forward:
// 위쪽 화살표
g.DrawLine(pen, position.X, position.Y - arrowSize, position.X, position.Y + arrowSize);
g.DrawLine(pen, position.X, position.Y - arrowSize, position.X - 5, position.Y - arrowSize + 5);
g.DrawLine(pen, position.X, position.Y - arrowSize, position.X + 5, position.Y - arrowSize + 5);
break;
case AgvDirection.Backward:
// 아래쪽 화살표
g.DrawLine(pen, position.X, position.Y - arrowSize, position.X, position.Y + arrowSize);
g.DrawLine(pen, position.X, position.Y + arrowSize, position.X - 5, position.Y + arrowSize - 5);
g.DrawLine(pen, position.X, position.Y + arrowSize, position.X + 5, position.Y + arrowSize - 5);
break;
case AgvDirection.Left:
// 왼쪽 화살표
g.DrawLine(pen, position.X - arrowSize, position.Y, position.X + arrowSize, position.Y);
g.DrawLine(pen, position.X - arrowSize, position.Y, position.X - arrowSize + 5, position.Y - 5);
g.DrawLine(pen, position.X - arrowSize, position.Y, position.X - arrowSize + 5, position.Y + 5);
break;
case AgvDirection.Right:
// 오른쪽 화살표
g.DrawLine(pen, position.X - arrowSize, position.Y, position.X + arrowSize, position.Y);
g.DrawLine(pen, position.X + arrowSize, position.Y, position.X + arrowSize - 5, position.Y - 5);
g.DrawLine(pen, position.X + arrowSize, position.Y, position.X + arrowSize - 5, position.Y + 5);
break;
}
pen.Dispose();
}
private void DrawInfo(Graphics g)
{
var font = new Font("Arial", 10);
var brush = Brushes.Black;
var y = 10;
// 줌 레벨 표시
g.DrawString($"줌: {_zoom:P0}", font, brush, new PointF(10, y));
y += 20;
// AGV 정보 표시
if (_agvList != null)
{
g.DrawString($"AGV 수: {_agvList.Count}", font, brush, new PointF(10, y));
y += 20;
foreach (var agv in _agvList)
{
var info = $"{agv.AgvId}: {agv.CurrentState} ({agv.CurrentPosition.X},{agv.CurrentPosition.Y})";
g.DrawString(info, font, brush, new PointF(10, y));
y += 15;
}
}
// 경로 정보 표시
if (_currentPath != null && _currentPath.Success)
{
y += 10;
g.DrawString($"경로: {_currentPath.Path.Count}개 노드", font, brush, new PointF(10, y));
y += 15;
g.DrawString($"거리: {_currentPath.TotalDistance:F1}", font, brush, new PointF(10, y));
y += 15;
g.DrawString($"계산시간: {_currentPath.CalculationTimeMs}ms", font, brush, new PointF(10, y));
}
font.Dispose();
}
#endregion
#region Cleanup
private void CleanupResources()
{
// AGV 이벤트 연결 해제
if (_agvList != null)
{
foreach (var agv in _agvList)
{
agv.PositionChanged -= OnAGVPositionChanged;
agv.StateChanged -= OnAGVStateChanged;
}
}
// 리소스 정리
_connectionPen?.Dispose();
_pathPen?.Dispose();
_agvPen?.Dispose();
}
#endregion
}
}