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>
This commit is contained in:
ChiKyun Kim
2025-09-11 16:41:52 +09:00
parent 7567602479
commit de0e39e030
50 changed files with 9578 additions and 1854 deletions

View File

@@ -43,12 +43,17 @@
<Reference Include="Microsoft.VisualBasic" /> <Reference Include="Microsoft.VisualBasic" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Models\MapNode.cs" /> <ProjectReference Include="..\AGVNavigationCore\AGVNavigationCore.csproj">
<Compile Include="Models\RfidMapping.cs" /> <Project>{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}</Project>
<Name>AGVNavigationCore</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="Models\MapData.cs" />
<Compile Include="Models\MapImage.cs" />
<Compile Include="Models\MapLabel.cs" />
<Compile Include="Models\NodePropertyWrapper.cs" />
<Compile Include="Models\NodeResolver.cs" /> <Compile Include="Models\NodeResolver.cs" />
<Compile Include="Models\Enums.cs" />
<Compile Include="Models\PathNode.cs" />
<Compile Include="Models\PathResult.cs" />
<Compile Include="Models\PathCalculator.cs" /> <Compile Include="Models\PathCalculator.cs" />
<Compile Include="Forms\MainForm.cs"> <Compile Include="Forms\MainForm.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
@@ -56,12 +61,6 @@
<Compile Include="Forms\MainForm.Designer.cs"> <Compile Include="Forms\MainForm.Designer.cs">
<DependentUpon>MainForm.cs</DependentUpon> <DependentUpon>MainForm.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Controls\MapCanvas.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Controls\MapCanvas.Designer.cs">
<DependentUpon>MapCanvas.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" /> <Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
@@ -69,11 +68,9 @@
<EmbeddedResource Include="Forms\MainForm.resx"> <EmbeddedResource Include="Forms\MainForm.resx">
<DependentUpon>MainForm.cs</DependentUpon> <DependentUpon>MainForm.cs</DependentUpon>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Include="Controls\MapCanvas.resx">
<DependentUpon>MapCanvas.cs</DependentUpon>
</EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="build.bat" />
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,59 +0,0 @@
namespace AGVMapEditor.Controls
{
partial class MapCanvas
{
/// <summary>
/// 필수 디자이너 변수입니다.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 사용 중인 모든 리소스를 정리합니다.
/// </summary>
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (components != null)
{
components.Dispose();
}
// MapCanvas의 추가 리소스 정리
_normalNodeBrush?.Dispose();
_rotationNodeBrush?.Dispose();
_dockingNodeBrush?.Dispose();
_chargingNodeBrush?.Dispose();
_selectedNodeBrush?.Dispose();
_hoveredNodeBrush?.Dispose();
_connectionPen?.Dispose();
_gridPen?.Dispose();
}
base.Dispose(disposing);
}
#region
/// <summary>
/// 디자이너 지원에 필요한 메서드입니다.
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
/// </summary>
private void InitializeComponent()
{
this.SuspendLayout();
//
// MapCanvas
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.White;
this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.Name = "MapCanvas";
this.ResumeLayout(false);
}
#endregion
}
}

View File

@@ -1,608 +0,0 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Windows.Forms;
using AGVMapEditor.Models;
namespace AGVMapEditor.Controls
{
/// <summary>
/// 맵 편집을 위한 그래픽 캔버스 컨트롤
/// </summary>
public partial class MapCanvas : UserControl
{
#region Constants
private const int NODE_SIZE = 20;
private const int NODE_RADIUS = NODE_SIZE / 2;
private const int GRID_SIZE = 20;
private const float CONNECTION_WIDTH = 2.0f;
#endregion
#region Fields
private List<MapNode> _nodes;
private MapNode _selectedNode;
private MapNode _hoveredNode;
private bool _isDragging;
private Point _dragOffset;
private Point _lastMousePosition;
// 그리드 및 줌 관련
private bool _showGrid = true;
private float _zoomFactor = 1.0f;
private Point _panOffset = Point.Empty;
// 브러쉬 및 펜
private Brush _normalNodeBrush;
private Brush _rotationNodeBrush;
private Brush _dockingNodeBrush;
private Brush _chargingNodeBrush;
private Brush _selectedNodeBrush;
private Brush _hoveredNodeBrush;
private Pen _connectionPen;
private Pen _gridPen;
#endregion
#region Events
/// <summary>
/// 노드가 선택되었을 때 발생하는 이벤트
/// </summary>
public event EventHandler<MapNode> NodeSelected;
/// <summary>
/// 노드가 이동되었을 때 발생하는 이벤트
/// </summary>
public event EventHandler<MapNode> NodeMoved;
/// <summary>
/// 배경이 클릭되었을 때 발생하는 이벤트
/// </summary>
public event EventHandler<Point> BackgroundClicked;
#endregion
#region Constructor
public MapCanvas() : this(new List<MapNode>())
{
}
public MapCanvas(List<MapNode> nodes)
{
InitializeComponent();
InitializeGraphics();
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw, true);
_nodes = nodes ?? new List<MapNode>();
// 이벤트 연결
this.MouseDown += MapCanvas_MouseDown;
this.MouseMove += MapCanvas_MouseMove;
this.MouseUp += MapCanvas_MouseUp;
this.MouseWheel += MapCanvas_MouseWheel;
this.KeyDown += MapCanvas_KeyDown;
// 포커스를 받을 수 있도록 설정
this.TabStop = true;
}
#endregion
#region Graphics Initialization
private void InitializeGraphics()
{
_normalNodeBrush = new SolidBrush(Color.Blue);
_rotationNodeBrush = new SolidBrush(Color.Orange);
_dockingNodeBrush = new SolidBrush(Color.Green);
_chargingNodeBrush = new SolidBrush(Color.Red);
_selectedNodeBrush = new SolidBrush(Color.Yellow);
_hoveredNodeBrush = new SolidBrush(Color.LightBlue);
_connectionPen = new Pen(Color.Gray, CONNECTION_WIDTH);
_gridPen = new Pen(Color.LightGray, 1.0f) { DashStyle = DashStyle.Dot };
}
#endregion
#region Properties
/// <summary>
/// 그리드 표시 여부
/// </summary>
public bool ShowGrid
{
get { return _showGrid; }
set
{
_showGrid = value;
Invalidate();
}
}
/// <summary>
/// 줌 팩터
/// </summary>
public float ZoomFactor
{
get { return _zoomFactor; }
set
{
_zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value));
Invalidate();
}
}
/// <summary>
/// 선택된 노드
/// </summary>
public MapNode SelectedNode
{
get { return _selectedNode; }
set
{
if (_selectedNode != value)
{
_selectedNode = value;
Invalidate();
}
}
}
#endregion
#region Mouse Events
private void MapCanvas_MouseDown(object sender, MouseEventArgs e)
{
this.Focus(); // 키보드 이벤트를 받기 위해 포커스 설정
var worldPoint = ScreenToWorld(e.Location);
var hitNode = GetNodeAt(worldPoint);
if (e.Button == MouseButtons.Left)
{
if (hitNode != null)
{
// 노드 선택 및 드래그 시작
SelectNode(hitNode);
_isDragging = true;
_dragOffset = new Point(worldPoint.X - hitNode.Position.X, worldPoint.Y - hitNode.Position.Y);
}
else
{
// 배경 클릭
SelectNode(null);
BackgroundClicked?.Invoke(this, worldPoint);
}
}
else if (e.Button == MouseButtons.Right)
{
// 우클릭 메뉴 (추후 구현)
ShowContextMenu(worldPoint, hitNode);
}
_lastMousePosition = e.Location;
}
private void MapCanvas_MouseMove(object sender, MouseEventArgs e)
{
var worldPoint = ScreenToWorld(e.Location);
if (_isDragging && _selectedNode != null)
{
// 노드 드래그
var newPosition = new Point(worldPoint.X - _dragOffset.X, worldPoint.Y - _dragOffset.Y);
// 그리드에 맞춤 (Ctrl 키를 누르지 않은 경우)
if (!ModifierKeys.HasFlag(Keys.Control))
{
newPosition.X = (newPosition.X / GRID_SIZE) * GRID_SIZE;
newPosition.Y = (newPosition.Y / GRID_SIZE) * GRID_SIZE;
}
_selectedNode.Position = newPosition;
Invalidate();
}
else if (e.Button == MouseButtons.Middle || (e.Button == MouseButtons.Left && ModifierKeys.HasFlag(Keys.Space)))
{
// 팬 (화면 이동)
var deltaX = e.X - _lastMousePosition.X;
var deltaY = e.Y - _lastMousePosition.Y;
_panOffset.X += deltaX;
_panOffset.Y += deltaY;
Invalidate();
}
else
{
// 호버 효과
var hitNode = GetNodeAt(worldPoint);
if (_hoveredNode != hitNode)
{
_hoveredNode = hitNode;
Invalidate();
}
}
_lastMousePosition = e.Location;
}
private void MapCanvas_MouseUp(object sender, MouseEventArgs e)
{
if (_isDragging && _selectedNode != null)
{
NodeMoved?.Invoke(this, _selectedNode);
}
_isDragging = false;
}
private void MapCanvas_MouseWheel(object sender, MouseEventArgs e)
{
// 줌
var delta = e.Delta > 0 ? 1.1f : 0.9f;
ZoomFactor *= delta;
}
#endregion
#region Keyboard Events
private void MapCanvas_KeyDown(object sender, KeyEventArgs e)
{
switch (e.KeyCode)
{
case Keys.Delete:
// 선택된 노드 삭제 (메인 폼에서 처리하도록 이벤트 발생)
if (_selectedNode != null)
{
// 삭제 확인 후 처리는 메인 폼에서
}
break;
case Keys.G:
// 그리드 토글
ShowGrid = !ShowGrid;
break;
case Keys.Home:
// 뷰 리셋
ZoomFactor = 1.0f;
_panOffset = Point.Empty;
Invalidate();
break;
}
}
#endregion
#region Coordinate Conversion
/// <summary>
/// 스크린 좌표를 월드 좌표로 변환
/// </summary>
private Point ScreenToWorld(Point screenPoint)
{
var worldX = (int)((screenPoint.X - _panOffset.X) / _zoomFactor);
var worldY = (int)((screenPoint.Y - _panOffset.Y) / _zoomFactor);
return new Point(worldX, worldY);
}
/// <summary>
/// 월드 좌표를 스크린 좌표로 변환
/// </summary>
private Point WorldToScreen(Point worldPoint)
{
var screenX = (int)(worldPoint.X * _zoomFactor + _panOffset.X);
var screenY = (int)(worldPoint.Y * _zoomFactor + _panOffset.Y);
return new Point(screenX, screenY);
}
#endregion
#region Node Management
/// <summary>
/// 지정된 위치의 노드 검색
/// </summary>
private MapNode GetNodeAt(Point worldPoint)
{
foreach (var node in _nodes)
{
var distance = Math.Sqrt(Math.Pow(worldPoint.X - node.Position.X, 2) + Math.Pow(worldPoint.Y - node.Position.Y, 2));
if (distance <= NODE_RADIUS)
{
return node;
}
}
return null;
}
/// <summary>
/// 노드 선택
/// </summary>
private void SelectNode(MapNode node)
{
if (_selectedNode != node)
{
_selectedNode = node;
NodeSelected?.Invoke(this, node);
Invalidate();
}
}
/// <summary>
/// 우클릭 컨텍스트 메뉴 표시
/// </summary>
private void ShowContextMenu(Point worldPoint, MapNode node)
{
var contextMenu = new ContextMenuStrip();
if (node != null)
{
contextMenu.Items.Add("노드 삭제", null, (s, e) => { /* 삭제 처리 */ });
contextMenu.Items.Add(new ToolStripSeparator());
contextMenu.Items.Add("일반 노드로 변경", null, (s, e) => ChangeNodeType(node, NodeType.Normal));
contextMenu.Items.Add("회전 지점으로 변경", null, (s, e) => ChangeNodeType(node, NodeType.Rotation));
contextMenu.Items.Add("도킹 스테이션으로 변경", null, (s, e) => ChangeNodeType(node, NodeType.Docking));
contextMenu.Items.Add("충전 스테이션으로 변경", null, (s, e) => ChangeNodeType(node, NodeType.Charging));
}
else
{
contextMenu.Items.Add("새 노드 추가", null, (s, e) => { /* 노드 추가 처리 */ });
}
var screenPoint = WorldToScreen(worldPoint);
contextMenu.Show(this, screenPoint);
}
/// <summary>
/// 노드 타입 변경
/// </summary>
private void ChangeNodeType(MapNode node, NodeType newType)
{
node.Type = newType;
node.SetDefaultColorByType(newType);
Invalidate();
}
#endregion
#region Painting
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
// 변환 매트릭스 적용
g.ResetTransform();
g.TranslateTransform(_panOffset.X, _panOffset.Y);
g.ScaleTransform(_zoomFactor, _zoomFactor);
// 배경 그리드 그리기
if (_showGrid)
{
DrawGrid(g);
}
// 연결선 그리기
DrawConnections(g);
// 노드 그리기
DrawNodes(g);
// 선택된 노드의 연결 정보 강조 표시
if (_selectedNode != null)
{
DrawSelectedNodeConnections(g);
}
}
private void DrawGrid(Graphics g)
{
var bounds = GetVisibleWorldBounds();
// 수직선
for (int x = (bounds.Left / GRID_SIZE) * GRID_SIZE; x <= bounds.Right; x += GRID_SIZE)
{
g.DrawLine(_gridPen, x, bounds.Top, x, bounds.Bottom);
}
// 수평선
for (int y = (bounds.Top / GRID_SIZE) * GRID_SIZE; y <= bounds.Bottom; y += GRID_SIZE)
{
g.DrawLine(_gridPen, bounds.Left, y, bounds.Right, y);
}
}
private void DrawConnections(Graphics g)
{
foreach (var node in _nodes)
{
foreach (var connectedNodeId in node.ConnectedNodes)
{
var connectedNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
if (connectedNode != null)
{
g.DrawLine(_connectionPen, node.Position, connectedNode.Position);
}
}
}
}
private void DrawNodes(Graphics g)
{
foreach (var node in _nodes)
{
DrawNode(g, node);
}
}
private void DrawNode(Graphics g, MapNode node)
{
var brush = GetNodeBrush(node);
var rect = new Rectangle(
node.Position.X - NODE_RADIUS,
node.Position.Y - NODE_RADIUS,
NODE_SIZE,
NODE_SIZE);
// 노드 그리기
switch (node.Type)
{
case NodeType.Normal:
g.FillEllipse(brush, rect);
break;
case NodeType.Rotation:
g.FillRectangle(brush, rect);
break;
case NodeType.Docking:
g.FillRectangle(brush, rect);
// 도킹 방향 표시
DrawDockingDirection(g, node);
break;
case NodeType.Charging:
g.FillEllipse(brush, rect);
// 충전 표시 (+)
DrawChargingSymbol(g, node);
break;
}
// 노드 테두리
g.DrawEllipse(Pens.Black, rect);
// 노드 이름 표시
if (_zoomFactor > 0.5f) // 줌이 충분히 큰 경우만 텍스트 표시
{
var font = new Font("Arial", 8 * _zoomFactor);
var textRect = new RectangleF(
node.Position.X - 30,
node.Position.Y + NODE_RADIUS + 2,
60, 15);
var format = new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Near
};
g.DrawString(node.Name, font, Brushes.Black, textRect, format);
}
}
private Brush GetNodeBrush(MapNode node)
{
if (node == _selectedNode)
return _selectedNodeBrush;
if (node == _hoveredNode)
return _hoveredNodeBrush;
switch (node.Type)
{
case NodeType.Normal:
return _normalNodeBrush;
case NodeType.Rotation:
return _rotationNodeBrush;
case NodeType.Docking:
return _dockingNodeBrush;
case NodeType.Charging:
return _chargingNodeBrush;
default:
return _normalNodeBrush;
}
}
private void DrawDockingDirection(Graphics g, MapNode node)
{
if (node.DockDirection == null)
return;
var arrowSize = 8;
var arrowPen = new Pen(Color.White, 2);
Point arrowStart, arrowEnd;
if (node.DockDirection == DockingDirection.Forward)
{
arrowStart = new Point(node.Position.X - arrowSize/2, node.Position.Y);
arrowEnd = new Point(node.Position.X + arrowSize/2, node.Position.Y);
}
else // Backward
{
arrowStart = new Point(node.Position.X + arrowSize/2, node.Position.Y);
arrowEnd = new Point(node.Position.X - arrowSize/2, node.Position.Y);
}
g.DrawLine(arrowPen, arrowStart, arrowEnd);
// 화살표 머리
var headSize = 3;
var headPoints = new Point[]
{
arrowEnd,
new Point(arrowEnd.X - headSize, arrowEnd.Y - headSize),
new Point(arrowEnd.X - headSize, arrowEnd.Y + headSize)
};
g.FillPolygon(Brushes.White, headPoints);
}
private void DrawChargingSymbol(Graphics g, MapNode node)
{
var symbolSize = 6;
var symbolPen = new Pen(Color.White, 2);
// + 모양
g.DrawLine(symbolPen,
node.Position.X - symbolSize/2, node.Position.Y,
node.Position.X + symbolSize/2, node.Position.Y);
g.DrawLine(symbolPen,
node.Position.X, node.Position.Y - symbolSize/2,
node.Position.X, node.Position.Y + symbolSize/2);
}
private void DrawSelectedNodeConnections(Graphics g)
{
var highlightPen = new Pen(Color.Yellow, CONNECTION_WIDTH + 2);
foreach (var connectedNodeId in _selectedNode.ConnectedNodes)
{
var connectedNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
if (connectedNode != null)
{
g.DrawLine(highlightPen, _selectedNode.Position, connectedNode.Position);
}
}
}
#endregion
#region Helper Methods
/// <summary>
/// 현재 보이는 월드 영역 계산
/// </summary>
private Rectangle GetVisibleWorldBounds()
{
var topLeft = ScreenToWorld(new Point(0, 0));
var bottomRight = ScreenToWorld(new Point(Width, Height));
return new Rectangle(
topLeft.X, topLeft.Y,
bottomRight.X - topLeft.X,
bottomRight.Y - topLeft.Y);
}
#endregion
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,34 @@
namespace AGVMapEditor.Controls
{
partial class MapCanvasInteractive
{
/// <summary>
/// 필수 디자이너 변수입니다.
/// </summary>
private System.ComponentModel.IContainer components = null;
#region
/// <summary>
/// 디자이너 지원에 필요한 메서드입니다.
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
/// </summary>
private void InitializeComponent()
{
this.SuspendLayout();
//
// MapCanvasInteractive
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.White;
this.Name = "MapCanvasInteractive";
this.Size = new System.Drawing.Size(800, 600);
this.ResumeLayout(false);
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,7 @@ namespace AGVMapEditor.Forms
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator(); this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.saveToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.saveToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.saveAsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.saveAsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.closeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator(); this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.statusStrip1 = new System.Windows.Forms.StatusStrip(); this.statusStrip1 = new System.Windows.Forms.StatusStrip();
@@ -42,20 +43,9 @@ namespace AGVMapEditor.Forms
this.splitContainer1 = new System.Windows.Forms.SplitContainer(); this.splitContainer1 = new System.Windows.Forms.SplitContainer();
this.tabControl1 = new System.Windows.Forms.TabControl(); this.tabControl1 = new System.Windows.Forms.TabControl();
this.tabPageNodes = new System.Windows.Forms.TabPage(); this.tabPageNodes = new System.Windows.Forms.TabPage();
this.btnRemoveConnection = new System.Windows.Forms.Button();
this.btnAddConnection = new System.Windows.Forms.Button();
this.btnDeleteNode = new System.Windows.Forms.Button();
this.btnAddNode = new System.Windows.Forms.Button();
this.listBoxNodes = new System.Windows.Forms.ListBox(); this.listBoxNodes = new System.Windows.Forms.ListBox();
this._propertyGrid = new System.Windows.Forms.PropertyGrid();
this.label1 = new System.Windows.Forms.Label(); this.label1 = new System.Windows.Forms.Label();
this.tabPageRfid = new System.Windows.Forms.TabPage();
this.btnDeleteRfidMapping = new System.Windows.Forms.Button();
this.btnAddRfidMapping = new System.Windows.Forms.Button();
this.listBoxRfidMappings = new System.Windows.Forms.ListBox();
this.label2 = new System.Windows.Forms.Label();
this.tabPageProperties = new System.Windows.Forms.TabPage();
this.labelSelectedNode = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.menuStrip1.SuspendLayout(); this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout(); this.statusStrip1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
@@ -63,8 +53,6 @@ namespace AGVMapEditor.Forms
this.splitContainer1.SuspendLayout(); this.splitContainer1.SuspendLayout();
this.tabControl1.SuspendLayout(); this.tabControl1.SuspendLayout();
this.tabPageNodes.SuspendLayout(); this.tabPageNodes.SuspendLayout();
this.tabPageRfid.SuspendLayout();
this.tabPageProperties.SuspendLayout();
this.SuspendLayout(); this.SuspendLayout();
// //
// menuStrip1 // menuStrip1
@@ -82,6 +70,7 @@ namespace AGVMapEditor.Forms
this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.newToolStripMenuItem, this.newToolStripMenuItem,
this.openToolStripMenuItem, this.openToolStripMenuItem,
this.closeToolStripMenuItem,
this.toolStripSeparator1, this.toolStripSeparator1,
this.saveToolStripMenuItem, this.saveToolStripMenuItem,
this.saveAsToolStripMenuItem, this.saveAsToolStripMenuItem,
@@ -95,7 +84,7 @@ namespace AGVMapEditor.Forms
// //
this.newToolStripMenuItem.Name = "newToolStripMenuItem"; this.newToolStripMenuItem.Name = "newToolStripMenuItem";
this.newToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.N))); this.newToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.N)));
this.newToolStripMenuItem.Size = new System.Drawing.Size(180, 22); this.newToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.newToolStripMenuItem.Text = "새로 만들기(&N)"; this.newToolStripMenuItem.Text = "새로 만들기(&N)";
this.newToolStripMenuItem.Click += new System.EventHandler(this.newToolStripMenuItem_Click); this.newToolStripMenuItem.Click += new System.EventHandler(this.newToolStripMenuItem_Click);
// //
@@ -103,39 +92,46 @@ namespace AGVMapEditor.Forms
// //
this.openToolStripMenuItem.Name = "openToolStripMenuItem"; this.openToolStripMenuItem.Name = "openToolStripMenuItem";
this.openToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O))); this.openToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O)));
this.openToolStripMenuItem.Size = new System.Drawing.Size(180, 22); this.openToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.openToolStripMenuItem.Text = "열기(&O)"; this.openToolStripMenuItem.Text = "열기(&O)";
this.openToolStripMenuItem.Click += new System.EventHandler(this.openToolStripMenuItem_Click); this.openToolStripMenuItem.Click += new System.EventHandler(this.openToolStripMenuItem_Click);
// //
// toolStripSeparator1 // toolStripSeparator1
// //
this.toolStripSeparator1.Name = "toolStripSeparator1"; this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(177, 6); this.toolStripSeparator1.Size = new System.Drawing.Size(195, 6);
// //
// saveToolStripMenuItem // saveToolStripMenuItem
// //
this.saveToolStripMenuItem.Name = "saveToolStripMenuItem"; this.saveToolStripMenuItem.Name = "saveToolStripMenuItem";
this.saveToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.S))); this.saveToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.S)));
this.saveToolStripMenuItem.Size = new System.Drawing.Size(180, 22); this.saveToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.saveToolStripMenuItem.Text = "저장(&S)"; this.saveToolStripMenuItem.Text = "저장(&S)";
this.saveToolStripMenuItem.Click += new System.EventHandler(this.saveToolStripMenuItem_Click); this.saveToolStripMenuItem.Click += new System.EventHandler(this.saveToolStripMenuItem_Click);
// //
// saveAsToolStripMenuItem // saveAsToolStripMenuItem
// //
this.saveAsToolStripMenuItem.Name = "saveAsToolStripMenuItem"; this.saveAsToolStripMenuItem.Name = "saveAsToolStripMenuItem";
this.saveAsToolStripMenuItem.Size = new System.Drawing.Size(180, 22); this.saveAsToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.saveAsToolStripMenuItem.Text = "다른 이름으로 저장(&A)"; this.saveAsToolStripMenuItem.Text = "다른 이름으로 저장(&A)";
this.saveAsToolStripMenuItem.Click += new System.EventHandler(this.saveAsToolStripMenuItem_Click); this.saveAsToolStripMenuItem.Click += new System.EventHandler(this.saveAsToolStripMenuItem_Click);
// //
// closeToolStripMenuItem
//
this.closeToolStripMenuItem.Name = "closeToolStripMenuItem";
this.closeToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.closeToolStripMenuItem.Text = "닫기(&C)";
this.closeToolStripMenuItem.Click += new System.EventHandler(this.closeToolStripMenuItem_Click);
//
// toolStripSeparator2 // toolStripSeparator2
// //
this.toolStripSeparator2.Name = "toolStripSeparator2"; this.toolStripSeparator2.Name = "toolStripSeparator2";
this.toolStripSeparator2.Size = new System.Drawing.Size(177, 6); this.toolStripSeparator2.Size = new System.Drawing.Size(195, 6);
// //
// exitToolStripMenuItem // exitToolStripMenuItem
// //
this.exitToolStripMenuItem.Name = "exitToolStripMenuItem"; this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
this.exitToolStripMenuItem.Size = new System.Drawing.Size(180, 22); this.exitToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.exitToolStripMenuItem.Text = "종료(&X)"; this.exitToolStripMenuItem.Text = "종료(&X)";
this.exitToolStripMenuItem.Click += new System.EventHandler(this.exitToolStripMenuItem_Click); this.exitToolStripMenuItem.Click += new System.EventHandler(this.exitToolStripMenuItem_Click);
// //
@@ -172,8 +168,6 @@ namespace AGVMapEditor.Forms
// tabControl1 // tabControl1
// //
this.tabControl1.Controls.Add(this.tabPageNodes); this.tabControl1.Controls.Add(this.tabPageNodes);
this.tabControl1.Controls.Add(this.tabPageRfid);
this.tabControl1.Controls.Add(this.tabPageProperties);
this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill; this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tabControl1.Location = new System.Drawing.Point(0, 0); this.tabControl1.Location = new System.Drawing.Point(0, 0);
this.tabControl1.Name = "tabControl1"; this.tabControl1.Name = "tabControl1";
@@ -183,11 +177,8 @@ namespace AGVMapEditor.Forms
// //
// tabPageNodes // tabPageNodes
// //
this.tabPageNodes.Controls.Add(this.btnRemoveConnection);
this.tabPageNodes.Controls.Add(this.btnAddConnection);
this.tabPageNodes.Controls.Add(this.btnDeleteNode);
this.tabPageNodes.Controls.Add(this.btnAddNode);
this.tabPageNodes.Controls.Add(this.listBoxNodes); this.tabPageNodes.Controls.Add(this.listBoxNodes);
this.tabPageNodes.Controls.Add(this._propertyGrid);
this.tabPageNodes.Controls.Add(this.label1); this.tabPageNodes.Controls.Add(this.label1);
this.tabPageNodes.Location = new System.Drawing.Point(4, 22); this.tabPageNodes.Location = new System.Drawing.Point(4, 22);
this.tabPageNodes.Name = "tabPageNodes"; this.tabPageNodes.Name = "tabPageNodes";
@@ -197,163 +188,33 @@ namespace AGVMapEditor.Forms
this.tabPageNodes.Text = "노드 관리"; this.tabPageNodes.Text = "노드 관리";
this.tabPageNodes.UseVisualStyleBackColor = true; this.tabPageNodes.UseVisualStyleBackColor = true;
// //
// btnRemoveConnection
//
this.btnRemoveConnection.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.btnRemoveConnection.Location = new System.Drawing.Point(6, 637);
this.btnRemoveConnection.Name = "btnRemoveConnection";
this.btnRemoveConnection.Size = new System.Drawing.Size(280, 25);
this.btnRemoveConnection.TabIndex = 5;
this.btnRemoveConnection.Text = "연결 제거";
this.btnRemoveConnection.UseVisualStyleBackColor = true;
this.btnRemoveConnection.Click += new System.EventHandler(this.btnRemoveConnection_Click);
//
// btnAddConnection
//
this.btnAddConnection.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.btnAddConnection.Location = new System.Drawing.Point(6, 606);
this.btnAddConnection.Name = "btnAddConnection";
this.btnAddConnection.Size = new System.Drawing.Size(280, 25);
this.btnAddConnection.TabIndex = 4;
this.btnAddConnection.Text = "연결 추가";
this.btnAddConnection.UseVisualStyleBackColor = true;
this.btnAddConnection.Click += new System.EventHandler(this.btnAddConnection_Click);
//
// btnDeleteNode
//
this.btnDeleteNode.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.btnDeleteNode.Location = new System.Drawing.Point(148, 670);
this.btnDeleteNode.Name = "btnDeleteNode";
this.btnDeleteNode.Size = new System.Drawing.Size(138, 25);
this.btnDeleteNode.TabIndex = 3;
this.btnDeleteNode.Text = "노드 삭제";
this.btnDeleteNode.UseVisualStyleBackColor = true;
this.btnDeleteNode.Click += new System.EventHandler(this.btnDeleteNode_Click);
//
// btnAddNode
//
this.btnAddNode.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.btnAddNode.Location = new System.Drawing.Point(6, 670);
this.btnAddNode.Name = "btnAddNode";
this.btnAddNode.Size = new System.Drawing.Size(138, 25);
this.btnAddNode.TabIndex = 2;
this.btnAddNode.Text = "노드 추가";
this.btnAddNode.UseVisualStyleBackColor = true;
this.btnAddNode.Click += new System.EventHandler(this.btnAddNode_Click);
//
// listBoxNodes // listBoxNodes
// //
this.listBoxNodes.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) this.listBoxNodes.Dock = System.Windows.Forms.DockStyle.Fill;
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.listBoxNodes.FormattingEnabled = true; this.listBoxNodes.FormattingEnabled = true;
this.listBoxNodes.ItemHeight = 12; this.listBoxNodes.ItemHeight = 12;
this.listBoxNodes.Location = new System.Drawing.Point(6, 25); this.listBoxNodes.Location = new System.Drawing.Point(3, 3);
this.listBoxNodes.Name = "listBoxNodes"; this.listBoxNodes.Name = "listBoxNodes";
this.listBoxNodes.Size = new System.Drawing.Size(280, 568); this.listBoxNodes.Size = new System.Drawing.Size(286, 245);
this.listBoxNodes.TabIndex = 1; this.listBoxNodes.TabIndex = 1;
// //
// _propertyGrid
//
this._propertyGrid.Dock = System.Windows.Forms.DockStyle.Bottom;
this._propertyGrid.Location = new System.Drawing.Point(3, 248);
this._propertyGrid.Name = "_propertyGrid";
this._propertyGrid.Size = new System.Drawing.Size(286, 450);
this._propertyGrid.TabIndex = 6;
//
// label1 // label1
// //
this.label1.AutoSize = true; this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(6, 6); this.label1.Location = new System.Drawing.Point(6, 6);
this.label1.Name = "label1"; this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(53, 12); this.label1.Size = new System.Drawing.Size(57, 12);
this.label1.TabIndex = 0; this.label1.TabIndex = 0;
this.label1.Text = "노드 목록"; this.label1.Text = "노드 목록";
// //
// tabPageRfid
//
this.tabPageRfid.Controls.Add(this.btnDeleteRfidMapping);
this.tabPageRfid.Controls.Add(this.btnAddRfidMapping);
this.tabPageRfid.Controls.Add(this.listBoxRfidMappings);
this.tabPageRfid.Controls.Add(this.label2);
this.tabPageRfid.Location = new System.Drawing.Point(4, 22);
this.tabPageRfid.Name = "tabPageRfid";
this.tabPageRfid.Padding = new System.Windows.Forms.Padding(3);
this.tabPageRfid.Size = new System.Drawing.Size(292, 701);
this.tabPageRfid.TabIndex = 1;
this.tabPageRfid.Text = "RFID 매핑";
this.tabPageRfid.UseVisualStyleBackColor = true;
//
// btnDeleteRfidMapping
//
this.btnDeleteRfidMapping.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.btnDeleteRfidMapping.Location = new System.Drawing.Point(148, 670);
this.btnDeleteRfidMapping.Name = "btnDeleteRfidMapping";
this.btnDeleteRfidMapping.Size = new System.Drawing.Size(138, 25);
this.btnDeleteRfidMapping.TabIndex = 3;
this.btnDeleteRfidMapping.Text = "매핑 삭제";
this.btnDeleteRfidMapping.UseVisualStyleBackColor = true;
this.btnDeleteRfidMapping.Click += new System.EventHandler(this.btnDeleteRfidMapping_Click);
//
// btnAddRfidMapping
//
this.btnAddRfidMapping.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.btnAddRfidMapping.Location = new System.Drawing.Point(6, 670);
this.btnAddRfidMapping.Name = "btnAddRfidMapping";
this.btnAddRfidMapping.Size = new System.Drawing.Size(138, 25);
this.btnAddRfidMapping.TabIndex = 2;
this.btnAddRfidMapping.Text = "매핑 추가";
this.btnAddRfidMapping.UseVisualStyleBackColor = true;
this.btnAddRfidMapping.Click += new System.EventHandler(this.btnAddRfidMapping_Click);
//
// listBoxRfidMappings
//
this.listBoxRfidMappings.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.listBoxRfidMappings.FormattingEnabled = true;
this.listBoxRfidMappings.ItemHeight = 12;
this.listBoxRfidMappings.Location = new System.Drawing.Point(6, 25);
this.listBoxRfidMappings.Name = "listBoxRfidMappings";
this.listBoxRfidMappings.Size = new System.Drawing.Size(280, 628);
this.listBoxRfidMappings.TabIndex = 1;
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(6, 6);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(77, 12);
this.label2.TabIndex = 0;
this.label2.Text = "RFID 매핑 목록";
//
// tabPageProperties
//
this.tabPageProperties.Controls.Add(this.labelSelectedNode);
this.tabPageProperties.Controls.Add(this.label3);
this.tabPageProperties.Location = new System.Drawing.Point(4, 22);
this.tabPageProperties.Name = "tabPageProperties";
this.tabPageProperties.Size = new System.Drawing.Size(292, 701);
this.tabPageProperties.TabIndex = 2;
this.tabPageProperties.Text = "속성";
this.tabPageProperties.UseVisualStyleBackColor = true;
//
// labelSelectedNode
//
this.labelSelectedNode.AutoSize = true;
this.labelSelectedNode.Location = new System.Drawing.Point(8, 35);
this.labelSelectedNode.Name = "labelSelectedNode";
this.labelSelectedNode.Size = new System.Drawing.Size(93, 12);
this.labelSelectedNode.TabIndex = 1;
this.labelSelectedNode.Text = "선택된 노드: 없음";
//
// label3
//
this.label3.AutoSize = true;
this.label3.Location = new System.Drawing.Point(8, 12);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(53, 12);
this.label3.TabIndex = 0;
this.label3.Text = "노드 속성";
//
// MainForm // MainForm
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F); this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
@@ -378,11 +239,6 @@ namespace AGVMapEditor.Forms
this.tabControl1.ResumeLayout(false); this.tabControl1.ResumeLayout(false);
this.tabPageNodes.ResumeLayout(false); this.tabPageNodes.ResumeLayout(false);
this.tabPageNodes.PerformLayout(); this.tabPageNodes.PerformLayout();
this.tabPageRfid.ResumeLayout(false);
this.tabPageRfid.ResumeLayout(false);
this.tabPageRfid.PerformLayout();
this.tabPageProperties.ResumeLayout(false);
this.tabPageProperties.PerformLayout();
this.ResumeLayout(false); this.ResumeLayout(false);
this.PerformLayout(); this.PerformLayout();
@@ -397,6 +253,7 @@ namespace AGVMapEditor.Forms
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1; private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripMenuItem saveToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem saveToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem saveAsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem saveAsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem closeToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2; private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
private System.Windows.Forms.StatusStrip statusStrip1; private System.Windows.Forms.StatusStrip statusStrip1;
@@ -404,19 +261,8 @@ namespace AGVMapEditor.Forms
private System.Windows.Forms.SplitContainer splitContainer1; private System.Windows.Forms.SplitContainer splitContainer1;
private System.Windows.Forms.TabControl tabControl1; private System.Windows.Forms.TabControl tabControl1;
private System.Windows.Forms.TabPage tabPageNodes; private System.Windows.Forms.TabPage tabPageNodes;
private System.Windows.Forms.TabPage tabPageRfid;
private System.Windows.Forms.Button btnDeleteNode;
private System.Windows.Forms.Button btnAddNode;
private System.Windows.Forms.ListBox listBoxNodes; private System.Windows.Forms.ListBox listBoxNodes;
private System.Windows.Forms.Label label1; private System.Windows.Forms.Label label1;
private System.Windows.Forms.Button btnDeleteRfidMapping; private System.Windows.Forms.PropertyGrid _propertyGrid;
private System.Windows.Forms.Button btnAddRfidMapping;
private System.Windows.Forms.ListBox listBoxRfidMappings;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.TabPage tabPageProperties;
private System.Windows.Forms.Label labelSelectedNode;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.Button btnRemoveConnection;
private System.Windows.Forms.Button btnAddConnection;
} }
} }

View File

@@ -5,7 +5,8 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using AGVMapEditor.Models; using AGVMapEditor.Models;
using AGVMapEditor.Controls; using AGVNavigationCore.Controls;
using AGVNavigationCore.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace AGVMapEditor.Forms namespace AGVMapEditor.Forms
@@ -17,10 +18,8 @@ namespace AGVMapEditor.Forms
{ {
#region Fields #region Fields
private NodeResolver _nodeResolver;
private List<MapNode> _mapNodes; private List<MapNode> _mapNodes;
private List<RfidMapping> _rfidMappings; private UnifiedAGVCanvas _mapCanvas;
private MapCanvas _mapCanvas;
// 현재 선택된 노드 // 현재 선택된 노드
private MapNode _selectedNode; private MapNode _selectedNode;
@@ -29,16 +28,36 @@ namespace AGVMapEditor.Forms
private string _currentMapFile = string.Empty; private string _currentMapFile = string.Empty;
private bool _hasChanges = false; private bool _hasChanges = false;
#endregion #endregion
#region Constructor #region Constructor
public MainForm() public MainForm() : this(null)
{
}
public MainForm(string[] args)
{ {
InitializeComponent(); InitializeComponent();
InitializeData(); InitializeData();
InitializeMapCanvas(); InitializeMapCanvas();
UpdateTitle(); UpdateTitle();
// 명령줄 인수로 파일이 전달되었으면 자동으로 열기
if (args != null && args.Length > 0)
{
string filePath = args[0];
if (System.IO.File.Exists(filePath))
{
LoadMapFromFile(filePath);
}
else
{
MessageBox.Show($"지정된 파일을 찾을 수 없습니다: {filePath}", "파일 오류",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
} }
#endregion #endregion
@@ -48,20 +67,117 @@ namespace AGVMapEditor.Forms
private void InitializeData() private void InitializeData()
{ {
_mapNodes = new List<MapNode>(); _mapNodes = new List<MapNode>();
_rfidMappings = new List<RfidMapping>();
_nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
} }
private void InitializeMapCanvas() private void InitializeMapCanvas()
{ {
_mapCanvas = new MapCanvas(_mapNodes); _mapCanvas = new UnifiedAGVCanvas();
_mapCanvas.Dock = DockStyle.Fill; _mapCanvas.Dock = DockStyle.Fill;
_mapCanvas.Mode = UnifiedAGVCanvas.CanvasMode.Edit;
_mapCanvas.Nodes = _mapNodes;
// RfidMappings 제거 - MapNode에 통합됨
// 이벤트 연결
_mapCanvas.NodeAdded += OnNodeAdded;
_mapCanvas.NodeSelected += OnNodeSelected; _mapCanvas.NodeSelected += OnNodeSelected;
_mapCanvas.NodeMoved += OnNodeMoved; _mapCanvas.NodeMoved += OnNodeMoved;
_mapCanvas.BackgroundClicked += OnBackgroundClicked; _mapCanvas.NodeDeleted += OnNodeDeleted;
_mapCanvas.MapChanged += OnMapChanged;
// 스플리터 패널에 맵 캔버스 추가 // 스플리터 패널에 맵 캔버스 추가
splitContainer1.Panel2.Controls.Add(_mapCanvas); splitContainer1.Panel2.Controls.Add(_mapCanvas);
// 편집 모드 툴바 초기화
InitializeEditModeToolbar();
}
private void InitializeEditModeToolbar()
{
// 툴바 패널 생성
var toolbarPanel = new Panel();
toolbarPanel.Height = 35;
toolbarPanel.Dock = DockStyle.Top;
toolbarPanel.BackColor = SystemColors.Control;
// 선택 모드 버튼
var btnSelect = new Button();
btnSelect.Text = "선택 (S)";
btnSelect.Size = new Size(70, 28);
btnSelect.Location = new Point(5, 3);
btnSelect.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select;
// 이동 모드 버튼
var btnMove = new Button();
btnMove.Text = "이동 (M)";
btnMove.Size = new Size(70, 28);
btnMove.Location = new Point(80, 3);
btnMove.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Move;
// 노드 추가 버튼
var btnAddNode = new Button();
btnAddNode.Text = "노드 추가 (A)";
btnAddNode.Size = new Size(80, 28);
btnAddNode.Location = new Point(155, 3);
btnAddNode.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.AddNode;
// 라벨 추가 버튼
var btnAddLabel = new Button();
btnAddLabel.Text = "라벨 추가 (L)";
btnAddLabel.Size = new Size(80, 28);
btnAddLabel.Location = new Point(240, 3);
btnAddLabel.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.AddLabel;
// 이미지 추가 버튼
var btnAddImage = new Button();
btnAddImage.Text = "이미지 추가 (I)";
btnAddImage.Size = new Size(90, 28);
btnAddImage.Location = new Point(325, 3);
btnAddImage.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.AddImage;
// 연결 모드 버튼
var btnConnect = new Button();
btnConnect.Text = "연결 (C)";
btnConnect.Size = new Size(70, 28);
btnConnect.Location = new Point(420, 3);
btnConnect.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Connect;
// 삭제 모드 버튼
var btnDelete = new Button();
btnDelete.Text = "삭제 (D)";
btnDelete.Size = new Size(70, 28);
btnDelete.Location = new Point(495, 3);
btnDelete.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Delete;
// 구분선
var separator1 = new Label();
separator1.Text = "|";
separator1.Size = new Size(10, 28);
separator1.Location = new Point(570, 3);
separator1.TextAlign = ContentAlignment.MiddleCenter;
// 그리드 토글 버튼
var btnToggleGrid = new Button();
btnToggleGrid.Text = "그리드";
btnToggleGrid.Size = new Size(60, 28);
btnToggleGrid.Location = new Point(585, 3);
btnToggleGrid.Click += (s, e) => _mapCanvas.ShowGrid = !_mapCanvas.ShowGrid;
// 맵 맞춤 버튼
var btnFitMap = new Button();
btnFitMap.Text = "맵 맞춤";
btnFitMap.Size = new Size(70, 28);
btnFitMap.Location = new Point(650, 3);
btnFitMap.Click += (s, e) => _mapCanvas.FitToNodes();
// 툴바에 버튼들 추가
toolbarPanel.Controls.AddRange(new Control[]
{
btnSelect, btnMove, btnAddNode, btnAddLabel, btnAddImage, btnConnect, btnDelete, separator1, btnToggleGrid, btnFitMap
});
// 스플리터 패널에 툴바 추가 (맨 위에)
splitContainer1.Panel2.Controls.Add(toolbarPanel);
toolbarPanel.BringToFront();
} }
#endregion #endregion
@@ -71,7 +187,16 @@ namespace AGVMapEditor.Forms
private void MainForm_Load(object sender, EventArgs e) private void MainForm_Load(object sender, EventArgs e)
{ {
RefreshNodeList(); RefreshNodeList();
RefreshRfidMappingList(); // 속성 변경 시 이벤트 연결
_propertyGrid.PropertyValueChanged += PropertyGrid_PropertyValueChanged;
}
private void OnNodeAdded(object sender, MapNode node)
{
_hasChanges = true;
UpdateTitle();
RefreshNodeList();
// RFID 자동 할당
} }
private void OnNodeSelected(object sender, MapNode node) private void OnNodeSelected(object sender, MapNode node)
@@ -87,6 +212,28 @@ namespace AGVMapEditor.Forms
RefreshNodeList(); RefreshNodeList();
} }
private void OnNodeDeleted(object sender, MapNode node)
{
_hasChanges = true;
UpdateTitle();
RefreshNodeList();
ClearNodeProperties();
// RFID 자동 할당
}
private void OnConnectionCreated(object sender, (MapNode From, MapNode To) connection)
{
_hasChanges = true;
UpdateTitle();
UpdateNodeProperties(); // 연결 정보 업데이트
}
private void OnMapChanged(object sender, EventArgs e)
{
_hasChanges = true;
UpdateTitle();
}
private void OnBackgroundClicked(object sender, Point location) private void OnBackgroundClicked(object sender, Point location)
{ {
_selectedNode = null; _selectedNode = null;
@@ -123,6 +270,11 @@ namespace AGVMapEditor.Forms
SaveAsMap(); SaveAsMap();
} }
private void closeToolStripMenuItem_Click(object sender, EventArgs e)
{
CloseMap();
}
private void exitToolStripMenuItem_Click(object sender, EventArgs e) private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{ {
this.Close(); this.Close();
@@ -152,16 +304,6 @@ namespace AGVMapEditor.Forms
RemoveConnectionFromSelectedNode(); RemoveConnectionFromSelectedNode();
} }
private void btnAddRfidMapping_Click(object sender, EventArgs e)
{
AddNewRfidMapping();
}
private void btnDeleteRfidMapping_Click(object sender, EventArgs e)
{
DeleteSelectedRfidMapping();
}
#endregion #endregion
#region Node Management #region Node Management
@@ -195,12 +337,12 @@ namespace AGVMapEditor.Forms
if (result == DialogResult.Yes) if (result == DialogResult.Yes)
{ {
_nodeResolver.RemoveMapNode(_selectedNode.NodeId); // 노드 제거
_mapNodes.Remove(_selectedNode);
_selectedNode = null; _selectedNode = null;
_hasChanges = true; _hasChanges = true;
RefreshNodeList(); RefreshNodeList();
RefreshRfidMappingList();
RefreshMapCanvas(); RefreshMapCanvas();
ClearNodeProperties(); ClearNodeProperties();
UpdateTitle(); UpdateTitle();
@@ -284,85 +426,11 @@ namespace AGVMapEditor.Forms
#endregion #endregion
#region RFID Mapping Management
private void AddNewRfidMapping()
{
if (_mapNodes.Count == 0)
{
MessageBox.Show("매핑할 노드가 없습니다. 먼저 노드를 추가하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var unmappedNodes = _nodeResolver.GetUnmappedNodes();
if (unmappedNodes.Count == 0)
{
MessageBox.Show("모든 노드가 이미 RFID에 매핑되어 있습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// RFID 값 입력
var rfidValue = Microsoft.VisualBasic.Interaction.InputBox("RFID 값을 입력하세요:", "RFID 매핑 추가");
if (string.IsNullOrEmpty(rfidValue))
return;
// 노드 선택
var nodeNames = unmappedNodes.Select(n => $"{n.NodeId}: {n.Name}").ToArray();
var selectedNode = Microsoft.VisualBasic.Interaction.InputBox("매핑할 노드를 선택하세요:", "노드 선택", nodeNames[0]);
var nodeId = selectedNode.Split(':')[0];
var description = Microsoft.VisualBasic.Interaction.InputBox("설명을 입력하세요 (선택사항):", "설명");
if (_nodeResolver.AddRfidMapping(rfidValue, nodeId, description))
{
_hasChanges = true;
RefreshRfidMappingList();
UpdateTitle();
MessageBox.Show("RFID 매핑이 추가되었습니다.", "성공", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
MessageBox.Show("RFID 매핑 추가에 실패했습니다. 중복된 RFID이거나 노드가 존재하지 않습니다.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void DeleteSelectedRfidMapping()
{
if (listBoxRfidMappings.SelectedItem == null)
{
MessageBox.Show("삭제할 RFID 매핑을 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var mapping = listBoxRfidMappings.SelectedItem as RfidMapping;
var result = MessageBox.Show($"RFID 매핑 '{mapping.RfidId} → {mapping.LogicalNodeId}'를 삭제하시겠습니까?",
"삭제 확인", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
if (_nodeResolver.RemoveRfidMapping(mapping.RfidId))
{
_hasChanges = true;
RefreshRfidMappingList();
UpdateTitle();
MessageBox.Show("RFID 매핑이 삭제되었습니다.", "성공", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
MessageBox.Show("RFID 매핑 삭제에 실패했습니다.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
#endregion
#region File Operations #region File Operations
private void NewMap() private void NewMap()
{ {
_mapNodes.Clear(); _mapNodes.Clear();
_rfidMappings.Clear();
_nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
_selectedNode = null; _selectedNode = null;
_currentMapFile = string.Empty; _currentMapFile = string.Empty;
_hasChanges = false; _hasChanges = false;
@@ -371,11 +439,25 @@ namespace AGVMapEditor.Forms
UpdateTitle(); UpdateTitle();
} }
private void CloseMap()
{
if (CheckSaveChanges())
{
_mapNodes.Clear();
_selectedNode = null;
_currentMapFile = string.Empty;
_hasChanges = false;
RefreshAll();
UpdateTitle();
}
}
private void OpenMap() private void OpenMap()
{ {
var openFileDialog = new OpenFileDialog var openFileDialog = new OpenFileDialog
{ {
Filter = "AGV Map Files (*.agvmap)|*.agvmap|JSON Files (*.json)|*.json|All Files (*.*)|*.*", Filter = "AGV Map Files (*.agvmap)|*.agvmap|All Files (*.*)|*.*",
DefaultExt = "agvmap" DefaultExt = "agvmap"
}; };
@@ -388,7 +470,6 @@ namespace AGVMapEditor.Forms
_hasChanges = false; _hasChanges = false;
RefreshAll(); RefreshAll();
UpdateTitle(); UpdateTitle();
MessageBox.Show("맵이 성공적으로 로드되었습니다.", "성공", MessageBoxButtons.OK, MessageBoxIcon.Information);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -423,7 +504,7 @@ namespace AGVMapEditor.Forms
{ {
var saveFileDialog = new SaveFileDialog var saveFileDialog = new SaveFileDialog
{ {
Filter = "AGV Map Files (*.agvmap)|*.agvmap|JSON Files (*.json)|*.json", Filter = "AGV Map Files (*.agvmap)|*.agvmap",
DefaultExt = "agvmap", DefaultExt = "agvmap",
FileName = "NewMap.agvmap" FileName = "NewMap.agvmap"
}; };
@@ -447,26 +528,39 @@ namespace AGVMapEditor.Forms
private void LoadMapFromFile(string filePath) private void LoadMapFromFile(string filePath)
{ {
var json = File.ReadAllText(filePath); var result = MapLoader.LoadMapFromFile(filePath);
var mapData = JsonConvert.DeserializeObject<MapData>(json);
_mapNodes = mapData.Nodes ?? new List<MapNode>(); if (result.Success)
_rfidMappings = mapData.RfidMappings ?? new List<RfidMapping>(); {
_nodeResolver = new NodeResolver(_rfidMappings, _mapNodes); _mapNodes = result.Nodes;
// 맵 캔버스에 데이터 설정
_mapCanvas.Nodes = _mapNodes;
// RfidMappings 제거됨 - MapNode에 통합
}
else
{
MessageBox.Show($"맵 파일 로딩 실패: {result.ErrorMessage}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
} }
private void SaveMapToFile(string filePath) private void SaveMapToFile(string filePath)
{ {
var mapData = new MapData if (!MapLoader.SaveMapToFile(filePath, _mapNodes))
{ {
Nodes = _mapNodes, MessageBox.Show("맵 파일 저장 실패", "오류",
RfidMappings = _rfidMappings, MessageBoxButtons.OK, MessageBoxIcon.Error);
CreatedDate = DateTime.Now, }
Version = "1.0" }
};
var json = JsonConvert.SerializeObject(mapData, Formatting.Indented); /// <summary>
File.WriteAllText(filePath, json); /// RFID 매핑 업데이트 (공용 MapLoader 사용)
/// </summary>
private void UpdateRfidMappings()
{
// 네비게이션 노드들에 RFID 자동 할당
MapLoader.AssignAutoRfidIds(_mapNodes);
} }
private bool CheckSaveChanges() private bool CheckSaveChanges()
@@ -497,7 +591,6 @@ namespace AGVMapEditor.Forms
private void RefreshAll() private void RefreshAll()
{ {
RefreshNodeList(); RefreshNodeList();
RefreshRfidMappingList();
RefreshMapCanvas(); RefreshMapCanvas();
ClearNodeProperties(); ClearNodeProperties();
} }
@@ -506,15 +599,99 @@ namespace AGVMapEditor.Forms
{ {
listBoxNodes.DataSource = null; listBoxNodes.DataSource = null;
listBoxNodes.DataSource = _mapNodes; listBoxNodes.DataSource = _mapNodes;
listBoxNodes.DisplayMember = "Name"; listBoxNodes.DisplayMember = "DisplayText";
listBoxNodes.ValueMember = "NodeId"; listBoxNodes.ValueMember = "NodeId";
// 노드 목록 클릭 이벤트 연결
listBoxNodes.SelectedIndexChanged -= ListBoxNodes_SelectedIndexChanged;
listBoxNodes.SelectedIndexChanged += ListBoxNodes_SelectedIndexChanged;
// 노드 타입별 색상 적용
listBoxNodes.DrawMode = DrawMode.OwnerDrawFixed;
listBoxNodes.DrawItem -= ListBoxNodes_DrawItem;
listBoxNodes.DrawItem += ListBoxNodes_DrawItem;
} }
private void RefreshRfidMappingList() private void ListBoxNodes_SelectedIndexChanged(object sender, EventArgs e)
{ {
listBoxRfidMappings.DataSource = null; if (listBoxNodes.SelectedItem is MapNode selectedNode)
listBoxRfidMappings.DataSource = _rfidMappings; {
listBoxRfidMappings.DisplayMember = "ToString"; _selectedNode = selectedNode;
UpdateNodeProperties();
// 맵 캔버스에서도 선택된 노드 표시
if (_mapCanvas != null)
{
_mapCanvas.Invalidate();
}
}
}
private void ListBoxNodes_DrawItem(object sender, DrawItemEventArgs e)
{
e.DrawBackground();
if (e.Index >= 0 && e.Index < _mapNodes.Count)
{
var node = _mapNodes[e.Index];
// 노드 타입에 따른 색상 설정
Color foreColor = Color.Black;
Color backColor = e.BackColor;
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
backColor = SystemColors.Highlight;
foreColor = SystemColors.HighlightText;
}
else
{
switch (node.Type)
{
case NodeType.Normal:
foreColor = Color.Black;
backColor = Color.White;
break;
case NodeType.Rotation:
foreColor = Color.DarkOrange;
backColor = Color.LightYellow;
break;
case NodeType.Docking:
foreColor = Color.DarkGreen;
backColor = Color.LightGreen;
break;
case NodeType.Charging:
foreColor = Color.DarkRed;
backColor = Color.LightPink;
break;
}
}
// 배경 그리기
using (var brush = new SolidBrush(backColor))
{
e.Graphics.FillRectangle(brush, e.Bounds);
}
// 텍스트 그리기 (노드ID - 설명 - RFID 순서)
var displayText = node.NodeId;
if (!string.IsNullOrEmpty(node.Description))
{
displayText += $" - {node.Description}";
}
if (!string.IsNullOrEmpty(node.RfidId))
{
displayText += $" - [{node.RfidId}]";
}
using (var brush = new SolidBrush(foreColor))
{
e.Graphics.DrawString(displayText, e.Font, brush, e.Bounds.X + 2, e.Bounds.Y + 2);
}
}
e.DrawFocusRectangle();
} }
private void RefreshMapCanvas() private void RefreshMapCanvas()
@@ -530,14 +707,15 @@ namespace AGVMapEditor.Forms
return; return;
} }
// 선택된 노드의 속성을 프로퍼티 패널에 표시 // 노드 래퍼 객체 생성 (타입에 따라 다른 래퍼 사용)
// (실제로는 PropertyGrid나 별도 컨트롤 사용) var nodeWrapper = NodePropertyWrapperFactory.CreateWrapper(_selectedNode, _mapNodes);
labelSelectedNode.Text = $"선택된 노드: {_selectedNode.Name} ({_selectedNode.NodeId})"; _propertyGrid.SelectedObject = nodeWrapper;
_propertyGrid.Focus();
} }
private void ClearNodeProperties() private void ClearNodeProperties()
{ {
labelSelectedNode.Text = "선택된 노드: 없음"; _propertyGrid.SelectedObject = null;
} }
private void UpdateTitle() private void UpdateTitle()
@@ -571,16 +749,38 @@ namespace AGVMapEditor.Forms
#endregion #endregion
#region Data Model for Serialization #region PropertyGrid
private class MapData
private void PropertyGrid_PropertyValueChanged(object s, PropertyValueChangedEventArgs e)
{ {
public List<MapNode> Nodes { get; set; } = new List<MapNode>(); // 속성이 변경되었을 때 자동으로 변경사항 표시
public List<RfidMapping> RfidMappings { get; set; } = new List<RfidMapping>(); _hasChanges = true;
public DateTime CreatedDate { get; set; } UpdateTitle();
public string Version { get; set; } = "1.0";
// 현재 선택된 노드를 기억
var currentSelectedNode = _selectedNode;
RefreshNodeList();
RefreshMapCanvas();
// 선택된 노드를 다시 선택
if (currentSelectedNode != null)
{
var nodeIndex = _mapNodes.IndexOf(currentSelectedNode);
if (nodeIndex >= 0)
{
listBoxNodes.SelectedIndex = nodeIndex;
}
}
} }
#endregion #endregion
#region Data Model for Serialization
#endregion
} }
} }

View File

@@ -1,5 +1,64 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
@@ -28,9 +87,9 @@
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space" />
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
@@ -39,7 +98,7 @@
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>

View File

@@ -14,7 +14,11 @@ namespace AGVMapEditor.Models
/// <summary>도킹 스테이션</summary> /// <summary>도킹 스테이션</summary>
Docking, Docking,
/// <summary>충전 스테이션</summary> /// <summary>충전 스테이션</summary>
Charging Charging,
/// <summary>라벨 (UI 요소)</summary>
Label,
/// <summary>이미지 (UI 요소)</summary>
Image
} }
/// <summary> /// <summary>

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using AGVNavigationCore.Models;
namespace AGVMapEditor.Models
{
public class MapData
{
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
public DateTime CreatedDate { get; set; }
public string Version { get; set; } = "1.0";
}
}

View File

@@ -0,0 +1,210 @@
using System;
using System.Drawing;
namespace AGVMapEditor.Models
{
/// <summary>
/// 맵 이미지 정보를 관리하는 클래스
/// 디자인 요소용 이미지/비트맵 요소
/// </summary>
public class MapImage
{
/// <summary>
/// 이미지 고유 ID
/// </summary>
public string ImageId { get; set; } = string.Empty;
/// <summary>
/// 이미지 파일 경로
/// </summary>
public string ImagePath { get; set; } = string.Empty;
/// <summary>
/// 맵 상의 위치 좌표 (좌상단 기준)
/// </summary>
public Point Position { get; set; } = Point.Empty;
/// <summary>
/// 이미지 크기 (원본 크기 기준 배율)
/// </summary>
public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f);
/// <summary>
/// 이미지 투명도 (0.0 ~ 1.0)
/// </summary>
public float Opacity { get; set; } = 1.0f;
/// <summary>
/// 이미지 회전 각도 (도 단위)
/// </summary>
public float Rotation { get; set; } = 0.0f;
/// <summary>
/// 이미지 설명
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// 이미지 생성 일자
/// </summary>
public DateTime CreatedDate { get; set; } = DateTime.Now;
/// <summary>
/// 이미지 수정 일자
/// </summary>
public DateTime ModifiedDate { get; set; } = DateTime.Now;
/// <summary>
/// 이미지 활성화 여부
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// 로딩된 이미지 (런타임에서만 사용, JSON 직렬화 제외)
/// </summary>
[Newtonsoft.Json.JsonIgnore]
public Image LoadedImage { get; set; }
/// <summary>
/// 기본 생성자
/// </summary>
public MapImage()
{
}
/// <summary>
/// 매개변수 생성자
/// </summary>
/// <param name="imageId">이미지 ID</param>
/// <param name="imagePath">이미지 파일 경로</param>
/// <param name="position">위치</param>
public MapImage(string imageId, string imagePath, Point position)
{
ImageId = imageId;
ImagePath = imagePath;
Position = position;
CreatedDate = DateTime.Now;
ModifiedDate = DateTime.Now;
}
/// <summary>
/// 이미지 로드 (256x256 이상일 경우 자동 리사이즈)
/// </summary>
/// <returns>로드 성공 여부</returns>
public bool LoadImage()
{
try
{
if (!string.IsNullOrEmpty(ImagePath) && System.IO.File.Exists(ImagePath))
{
LoadedImage?.Dispose();
var originalImage = Image.FromFile(ImagePath);
// 이미지 크기 체크 및 리사이즈
if (originalImage.Width > 256 || originalImage.Height > 256)
{
LoadedImage = ResizeImage(originalImage, 256, 256);
originalImage.Dispose();
}
else
{
LoadedImage = originalImage;
}
return true;
}
}
catch (Exception)
{
// 이미지 로드 실패
}
return false;
}
/// <summary>
/// 이미지 리사이즈 (비율 유지)
/// </summary>
/// <param name="image">원본 이미지</param>
/// <param name="maxWidth">최대 너비</param>
/// <param name="maxHeight">최대 높이</param>
/// <returns>리사이즈된 이미지</returns>
private Image ResizeImage(Image image, int maxWidth, int maxHeight)
{
// 비율 계산
double ratioX = (double)maxWidth / image.Width;
double ratioY = (double)maxHeight / image.Height;
double ratio = Math.Min(ratioX, ratioY);
// 새로운 크기 계산
int newWidth = (int)(image.Width * ratio);
int newHeight = (int)(image.Height * ratio);
// 리사이즈된 이미지 생성
var resizedImage = new Bitmap(newWidth, newHeight);
using (var graphics = Graphics.FromImage(resizedImage))
{
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
graphics.DrawImage(image, 0, 0, newWidth, newHeight);
}
return resizedImage;
}
/// <summary>
/// 실제 표시될 크기 계산
/// </summary>
/// <returns>실제 크기</returns>
public Size GetDisplaySize()
{
if (LoadedImage == null) return Size.Empty;
return new Size(
(int)(LoadedImage.Width * Scale.Width),
(int)(LoadedImage.Height * Scale.Height)
);
}
/// <summary>
/// 문자열 표현
/// </summary>
public override string ToString()
{
return $"{ImageId}: {System.IO.Path.GetFileName(ImagePath)} at ({Position.X}, {Position.Y})";
}
/// <summary>
/// 이미지 복사
/// </summary>
/// <returns>복사된 이미지</returns>
public MapImage Clone()
{
var clone = new MapImage
{
ImageId = ImageId,
ImagePath = ImagePath,
Position = Position,
Scale = Scale,
Opacity = Opacity,
Rotation = Rotation,
Description = Description,
CreatedDate = CreatedDate,
ModifiedDate = ModifiedDate,
IsActive = IsActive
};
// 이미지는 복사하지 않음 (필요시 LoadImage() 호출)
return clone;
}
/// <summary>
/// 리소스 정리
/// </summary>
public void Dispose()
{
LoadedImage?.Dispose();
LoadedImage = null;
}
}
}

View File

@@ -0,0 +1,125 @@
using System;
using System.Drawing;
namespace AGVMapEditor.Models
{
/// <summary>
/// 맵 라벨 정보를 관리하는 클래스
/// 디자인 요소용 텍스트 라벨
/// </summary>
public class MapLabel
{
/// <summary>
/// 라벨 고유 ID
/// </summary>
public string LabelId { get; set; } = string.Empty;
/// <summary>
/// 라벨 텍스트
/// </summary>
public string Text { get; set; } = string.Empty;
/// <summary>
/// 맵 상의 위치 좌표
/// </summary>
public Point Position { get; set; } = Point.Empty;
/// <summary>
/// 폰트 정보
/// </summary>
public string FontFamily { get; set; } = "Arial";
/// <summary>
/// 폰트 크기
/// </summary>
public float FontSize { get; set; } = 12;
/// <summary>
/// 폰트 스타일 (Bold, Italic 등)
/// </summary>
public FontStyle FontStyle { get; set; } = FontStyle.Regular;
/// <summary>
/// 글자 색상
/// </summary>
public Color ForeColor { get; set; } = Color.Black;
/// <summary>
/// 배경 색상
/// </summary>
public Color BackColor { get; set; } = Color.Transparent;
/// <summary>
/// 배경 표시 여부
/// </summary>
public bool ShowBackground { get; set; } = false;
/// <summary>
/// 라벨 생성 일자
/// </summary>
public DateTime CreatedDate { get; set; } = DateTime.Now;
/// <summary>
/// 라벨 수정 일자
/// </summary>
public DateTime ModifiedDate { get; set; } = DateTime.Now;
/// <summary>
/// 라벨 활성화 여부
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// 기본 생성자
/// </summary>
public MapLabel()
{
}
/// <summary>
/// 매개변수 생성자
/// </summary>
/// <param name="labelId">라벨 ID</param>
/// <param name="text">라벨 텍스트</param>
/// <param name="position">위치</param>
public MapLabel(string labelId, string text, Point position)
{
LabelId = labelId;
Text = text;
Position = position;
CreatedDate = DateTime.Now;
ModifiedDate = DateTime.Now;
}
/// <summary>
/// 문자열 표현
/// </summary>
public override string ToString()
{
return $"{LabelId}: {Text} at ({Position.X}, {Position.Y})";
}
/// <summary>
/// 라벨 복사
/// </summary>
/// <returns>복사된 라벨</returns>
public MapLabel Clone()
{
return new MapLabel
{
LabelId = LabelId,
Text = Text,
Position = Position,
FontFamily = FontFamily,
FontSize = FontSize,
FontStyle = FontStyle,
ForeColor = ForeColor,
BackColor = BackColor,
ShowBackground = ShowBackground,
CreatedDate = CreatedDate,
ModifiedDate = ModifiedDate,
IsActive = IsActive
};
}
}
}

View File

@@ -83,6 +83,72 @@ namespace AGVMapEditor.Models
/// </summary> /// </summary>
public Color DisplayColor { get; set; } = Color.Blue; public Color DisplayColor { get; set; } = Color.Blue;
/// <summary>
/// RFID 태그 ID (이 노드에 매핑된 RFID)
/// </summary>
public string RfidId { get; set; } = string.Empty;
/// <summary>
/// 라벨 텍스트 (NodeType.Label인 경우 사용)
/// </summary>
public string LabelText { get; set; } = string.Empty;
/// <summary>
/// 라벨 폰트 패밀리 (NodeType.Label인 경우 사용)
/// </summary>
public string FontFamily { get; set; } = "Arial";
/// <summary>
/// 라벨 폰트 크기 (NodeType.Label인 경우 사용)
/// </summary>
public float FontSize { get; set; } = 12.0f;
/// <summary>
/// 라벨 폰트 스타일 (NodeType.Label인 경우 사용)
/// </summary>
public FontStyle FontStyle { get; set; } = FontStyle.Regular;
/// <summary>
/// 라벨 전경색 (NodeType.Label인 경우 사용)
/// </summary>
public Color ForeColor { get; set; } = Color.Black;
/// <summary>
/// 라벨 배경색 (NodeType.Label인 경우 사용)
/// </summary>
public Color BackColor { get; set; } = Color.Transparent;
/// <summary>
/// 라벨 배경 표시 여부 (NodeType.Label인 경우 사용)
/// </summary>
public bool ShowBackground { get; set; } = false;
/// <summary>
/// 이미지 파일 경로 (NodeType.Image인 경우 사용)
/// </summary>
public string ImagePath { get; set; } = string.Empty;
/// <summary>
/// 이미지 크기 배율 (NodeType.Image인 경우 사용)
/// </summary>
public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f);
/// <summary>
/// 이미지 투명도 (NodeType.Image인 경우 사용, 0.0~1.0)
/// </summary>
public float Opacity { get; set; } = 1.0f;
/// <summary>
/// 이미지 회전 각도 (NodeType.Image인 경우 사용, 도 단위)
/// </summary>
public float Rotation { get; set; } = 0.0f;
/// <summary>
/// 로딩된 이미지 (런타임에서만 사용, JSON 직렬화 제외)
/// </summary>
[Newtonsoft.Json.JsonIgnore]
public Image LoadedImage { get; set; }
/// <summary> /// <summary>
/// 기본 생성자 /// 기본 생성자
/// </summary> /// </summary>
@@ -130,6 +196,12 @@ namespace AGVMapEditor.Models
case NodeType.Charging: case NodeType.Charging:
DisplayColor = Color.Red; DisplayColor = Color.Red;
break; break;
case NodeType.Label:
DisplayColor = Color.Purple;
break;
case NodeType.Image:
DisplayColor = Color.Brown;
break;
} }
} }
@@ -196,6 +268,29 @@ namespace AGVMapEditor.Models
return $"{NodeId}: {Name} ({Type}) at ({Position.X}, {Position.Y})"; return $"{NodeId}: {Name} ({Type}) at ({Position.X}, {Position.Y})";
} }
/// <summary>
/// 리스트박스 표시용 텍스트 (노드ID - 설명 - RFID 순서)
/// </summary>
public string DisplayText
{
get
{
var displayText = NodeId;
if (!string.IsNullOrEmpty(Description))
{
displayText += $" - {Description}";
}
if (!string.IsNullOrEmpty(RfidId))
{
displayText += $" - [{RfidId}]";
}
return displayText;
}
}
/// <summary> /// <summary>
/// 노드 복사 /// 노드 복사
/// </summary> /// </summary>
@@ -217,9 +312,111 @@ namespace AGVMapEditor.Models
ModifiedDate = ModifiedDate, ModifiedDate = ModifiedDate,
Description = Description, Description = Description,
IsActive = IsActive, IsActive = IsActive,
DisplayColor = DisplayColor DisplayColor = DisplayColor,
RfidId = RfidId,
LabelText = LabelText,
FontFamily = FontFamily,
FontSize = FontSize,
FontStyle = FontStyle,
ForeColor = ForeColor,
BackColor = BackColor,
ShowBackground = ShowBackground,
ImagePath = ImagePath,
Scale = Scale,
Opacity = Opacity,
Rotation = Rotation
}; };
return clone; return clone;
} }
/// <summary>
/// 이미지 로드 (256x256 이상일 경우 자동 리사이즈)
/// </summary>
/// <returns>로드 성공 여부</returns>
public bool LoadImage()
{
if (Type != NodeType.Image) return false;
try
{
if (!string.IsNullOrEmpty(ImagePath) && System.IO.File.Exists(ImagePath))
{
LoadedImage?.Dispose();
var originalImage = Image.FromFile(ImagePath);
// 이미지 크기 체크 및 리사이즈
if (originalImage.Width > 256 || originalImage.Height > 256)
{
LoadedImage = ResizeImage(originalImage, 256, 256);
originalImage.Dispose();
}
else
{
LoadedImage = originalImage;
}
return true;
}
}
catch (Exception)
{
// 이미지 로드 실패
}
return false;
}
/// <summary>
/// 이미지 리사이즈 (비율 유지)
/// </summary>
/// <param name="image">원본 이미지</param>
/// <param name="maxWidth">최대 너비</param>
/// <param name="maxHeight">최대 높이</param>
/// <returns>리사이즈된 이미지</returns>
private Image ResizeImage(Image image, int maxWidth, int maxHeight)
{
// 비율 계산
double ratioX = (double)maxWidth / image.Width;
double ratioY = (double)maxHeight / image.Height;
double ratio = Math.Min(ratioX, ratioY);
// 새로운 크기 계산
int newWidth = (int)(image.Width * ratio);
int newHeight = (int)(image.Height * ratio);
// 리사이즈된 이미지 생성
var resizedImage = new Bitmap(newWidth, newHeight);
using (var graphics = Graphics.FromImage(resizedImage))
{
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
graphics.DrawImage(image, 0, 0, newWidth, newHeight);
}
return resizedImage;
}
/// <summary>
/// 실제 표시될 크기 계산 (이미지 노드인 경우)
/// </summary>
/// <returns>실제 크기</returns>
public Size GetDisplaySize()
{
if (Type != NodeType.Image || LoadedImage == null) return Size.Empty;
return new Size(
(int)(LoadedImage.Width * Scale.Width),
(int)(LoadedImage.Height * Scale.Height)
);
}
/// <summary>
/// 리소스 정리
/// </summary>
public void Dispose()
{
LoadedImage?.Dispose();
LoadedImage = null;
}
} }
} }

View File

@@ -0,0 +1,499 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using AGVNavigationCore.Models;
namespace AGVMapEditor.Models
{
/// <summary>
/// 노드 타입에 따른 PropertyWrapper 팩토리
/// </summary>
public static class NodePropertyWrapperFactory
{
public static object CreateWrapper(MapNode node, List<MapNode> mapNodes)
{
switch (node.Type)
{
case NodeType.Label:
return new LabelNodePropertyWrapper(node, mapNodes);
case NodeType.Image:
return new ImageNodePropertyWrapper(node, mapNodes);
default:
return new NodePropertyWrapper(node, mapNodes);
}
}
}
/// <summary>
/// 라벨 노드 전용 PropertyWrapper
/// </summary>
public class LabelNodePropertyWrapper
{
private MapNode _node;
private List<MapNode> _mapNodes;
public LabelNodePropertyWrapper(MapNode node, List<MapNode> mapNodes)
{
_node = node;
_mapNodes = mapNodes;
}
[Category("기본 정보")]
[DisplayName("노드 ID")]
[Description("노드의 고유 식별자")]
[ReadOnly(true)]
public string NodeId
{
get => _node.NodeId;
}
[Category("기본 정보")]
[DisplayName("노드 타입")]
[Description("노드의 타입")]
[ReadOnly(true)]
public NodeType Type
{
get => _node.Type;
}
[Category("라벨")]
[DisplayName("텍스트")]
[Description("표시할 텍스트")]
public string LabelText
{
get => _node.LabelText;
set
{
_node.LabelText = value ?? "";
_node.ModifiedDate = DateTime.Now;
}
}
[Category("라벨")]
[DisplayName("폰트 패밀리")]
[Description("폰트 패밀리")]
public string FontFamily
{
get => _node.FontFamily;
set
{
_node.FontFamily = value ?? "Arial";
_node.ModifiedDate = DateTime.Now;
}
}
[Category("라벨")]
[DisplayName("폰트 크기")]
[Description("폰트 크기")]
public float FontSize
{
get => _node.FontSize;
set
{
_node.FontSize = Math.Max(6, Math.Min(72, value));
_node.ModifiedDate = DateTime.Now;
}
}
[Category("라벨")]
[DisplayName("폰트 스타일")]
[Description("폰트 스타일")]
public FontStyle FontStyle
{
get => _node.FontStyle;
set
{
_node.FontStyle = value;
_node.ModifiedDate = DateTime.Now;
}
}
[Category("라벨")]
[DisplayName("전경색")]
[Description("텍스트 색상")]
public Color ForeColor
{
get => _node.ForeColor;
set
{
_node.ForeColor = value;
_node.ModifiedDate = DateTime.Now;
}
}
[Category("라벨")]
[DisplayName("배경색")]
[Description("배경 색상")]
public Color BackColor
{
get => _node.BackColor;
set
{
_node.BackColor = value;
_node.ModifiedDate = DateTime.Now;
}
}
[Category("라벨")]
[DisplayName("배경 표시")]
[Description("배경을 표시할지 여부")]
public bool ShowBackground
{
get => _node.ShowBackground;
set
{
_node.ShowBackground = value;
_node.ModifiedDate = DateTime.Now;
}
}
[Category("위치")]
[DisplayName("X 좌표")]
[Description("맵에서의 X 좌표")]
public int PositionX
{
get => _node.Position.X;
set
{
_node.Position = new Point(value, _node.Position.Y);
_node.ModifiedDate = DateTime.Now;
}
}
[Category("위치")]
[DisplayName("Y 좌표")]
[Description("맵에서의 Y 좌표")]
public int PositionY
{
get => _node.Position.Y;
set
{
_node.Position = new Point(_node.Position.X, value);
_node.ModifiedDate = DateTime.Now;
}
}
[Category("정보")]
[DisplayName("생성 일시")]
[Description("노드가 생성된 일시")]
[ReadOnly(true)]
public DateTime CreatedDate
{
get => _node.CreatedDate;
}
[Category("정보")]
[DisplayName("수정 일시")]
[Description("노드가 마지막으로 수정된 일시")]
[ReadOnly(true)]
public DateTime ModifiedDate
{
get => _node.ModifiedDate;
}
}
/// <summary>
/// 이미지 노드 전용 PropertyWrapper
/// </summary>
public class ImageNodePropertyWrapper
{
private MapNode _node;
private List<MapNode> _mapNodes;
public ImageNodePropertyWrapper(MapNode node, List<MapNode> mapNodes)
{
_node = node;
_mapNodes = mapNodes;
}
[Category("기본 정보")]
[DisplayName("노드 ID")]
[Description("노드의 고유 식별자")]
[ReadOnly(true)]
public string NodeId
{
get => _node.NodeId;
}
[Category("기본 정보")]
[DisplayName("노드 타입")]
[Description("노드의 타입")]
[ReadOnly(true)]
public NodeType Type
{
get => _node.Type;
}
[Category("이미지")]
[DisplayName("이미지 경로")]
[Description("이미지 파일 경로")]
// 파일 선택 에디터는 나중에 구현
public string ImagePath
{
get => _node.ImagePath;
set
{
_node.ImagePath = value ?? "";
_node.LoadImage(); // 이미지 다시 로드
_node.ModifiedDate = DateTime.Now;
}
}
[Category("이미지")]
[DisplayName("가로 배율")]
[Description("가로 배율 (1.0 = 원본 크기)")]
public float ScaleWidth
{
get => _node.Scale.Width;
set
{
_node.Scale = new SizeF(Math.Max(0.1f, Math.Min(5.0f, value)), _node.Scale.Height);
_node.ModifiedDate = DateTime.Now;
}
}
[Category("이미지")]
[DisplayName("세로 배율")]
[Description("세로 배율 (1.0 = 원본 크기)")]
public float ScaleHeight
{
get => _node.Scale.Height;
set
{
_node.Scale = new SizeF(_node.Scale.Width, Math.Max(0.1f, Math.Min(5.0f, value)));
_node.ModifiedDate = DateTime.Now;
}
}
[Category("이미지")]
[DisplayName("투명도")]
[Description("투명도 (0.0 = 투명, 1.0 = 불투명)")]
public float Opacity
{
get => _node.Opacity;
set
{
_node.Opacity = Math.Max(0.0f, Math.Min(1.0f, value));
_node.ModifiedDate = DateTime.Now;
}
}
[Category("이미지")]
[DisplayName("회전각도")]
[Description("회전 각도 (도 단위)")]
public float Rotation
{
get => _node.Rotation;
set
{
_node.Rotation = value % 360;
_node.ModifiedDate = DateTime.Now;
}
}
[Category("위치")]
[DisplayName("X 좌표")]
[Description("맵에서의 X 좌표")]
public int PositionX
{
get => _node.Position.X;
set
{
_node.Position = new Point(value, _node.Position.Y);
_node.ModifiedDate = DateTime.Now;
}
}
[Category("위치")]
[DisplayName("Y 좌표")]
[Description("맵에서의 Y 좌표")]
public int PositionY
{
get => _node.Position.Y;
set
{
_node.Position = new Point(_node.Position.X, value);
_node.ModifiedDate = DateTime.Now;
}
}
[Category("정보")]
[DisplayName("생성 일시")]
[Description("노드가 생성된 일시")]
[ReadOnly(true)]
public DateTime CreatedDate
{
get => _node.CreatedDate;
}
[Category("정보")]
[DisplayName("수정 일시")]
[Description("노드가 마지막으로 수정된 일시")]
[ReadOnly(true)]
public DateTime ModifiedDate
{
get => _node.ModifiedDate;
}
}
/// <summary>
/// PropertyGrid에서 사용할 노드 속성 래퍼 클래스
/// </summary>
public class NodePropertyWrapper
{
private MapNode _node;
private List<MapNode> _mapNodes;
public NodePropertyWrapper(MapNode node, List<MapNode> mapNodes)
{
_node = node;
_mapNodes = mapNodes;
}
[Category("기본 정보")]
[DisplayName("노드 ID")]
[Description("노드의 고유 식별자")]
[ReadOnly(true)]
public string NodeId
{
get => _node.NodeId;
set => _node.NodeId = value;
}
[Category("기본 정보")]
[DisplayName("노드 이름")]
[Description("노드의 표시 이름")]
public string Name
{
get => _node.Name;
set
{
_node.Name = value;
_node.ModifiedDate = DateTime.Now;
}
}
[Category("기본 정보")]
[DisplayName("노드 타입")]
[Description("노드의 타입 (Normal, Rotation, Docking, Charging)")]
public NodeType Type
{
get => _node.Type;
set
{
_node.Type = value;
_node.CanRotate = value == NodeType.Rotation;
_node.SetDefaultColorByType(value);
_node.ModifiedDate = DateTime.Now;
}
}
[Category("위치")]
[DisplayName("X 좌표")]
[Description("맵에서의 X 좌표")]
public int PositionX
{
get => _node.Position.X;
set
{
_node.Position = new Point(value, _node.Position.Y);
_node.ModifiedDate = DateTime.Now;
}
}
[Category("위치")]
[DisplayName("Y 좌표")]
[Description("맵에서의 Y 좌표")]
public int PositionY
{
get => _node.Position.Y;
set
{
_node.Position = new Point(_node.Position.X, value);
_node.ModifiedDate = DateTime.Now;
}
}
[Category("고급")]
[DisplayName("회전 가능")]
[Description("이 노드에서 AGV가 회전할 수 있는지 여부")]
public bool CanRotate
{
get => _node.CanRotate;
set
{
_node.CanRotate = value;
_node.ModifiedDate = DateTime.Now;
}
}
[Category("고급")]
[DisplayName("RFID")]
[Description("RFID ID")]
public string RFID
{
get => _node.RfidId;
set
{
_node.RfidId = value;
_node.ModifiedDate = DateTime.Now;
}
}
[Category("고급")]
[DisplayName("설명")]
[Description("노드에 대한 추가 설명")]
public string Description
{
get => _node.Description;
set
{
_node.Description = value ?? "";
_node.ModifiedDate = DateTime.Now;
}
}
[Category("고급")]
[DisplayName("활성화")]
[Description("노드 활성화 여부")]
public bool IsActive
{
get => _node.IsActive;
set
{
_node.IsActive = value;
_node.ModifiedDate = DateTime.Now;
}
}
[Category("정보")]
[DisplayName("생성 일시")]
[Description("노드가 생성된 일시")]
[ReadOnly(true)]
public DateTime CreatedDate
{
get => _node.CreatedDate;
}
[Category("정보")]
[DisplayName("수정 일시")]
[Description("노드가 마지막으로 수정된 일시")]
[ReadOnly(true)]
public DateTime ModifiedDate
{
get => _node.ModifiedDate;
}
}
}

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AGVNavigationCore.Models;
namespace AGVMapEditor.Models namespace AGVMapEditor.Models
{ {

View File

@@ -3,467 +3,266 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using AGVNavigationCore.Models;
using AGVNavigationCore.PathFinding;
namespace AGVMapEditor.Models namespace AGVMapEditor.Models
{ {
/// <summary> /// <summary>
/// AGV 전용 경로 계산기 (A* 알고리즘 기반) /// AGV 전용 경로 계산기 (AGVNavigationCore 래퍼)
/// AGV의 방향성, 도킹 제약, 회전 제약을 고려한 경로 계산 /// AGVMapEditor와 AGVNavigationCore 간의 호환성 제공
/// RFID 기반 경로 계산을 우선 사용
/// </summary> /// </summary>
public class PathCalculator public class PathCalculator
{ {
#region Constants private AGVPathfinder _agvPathfinder;
private AStarPathfinder _astarPathfinder;
private const float BASE_MOVE_COST = 1.0f; // 기본 이동 비용 private RfidBasedPathfinder _rfidPathfinder;
private const float ROTATION_COST = 0.5f; // 회전 비용
private const float DOCKING_APPROACH_COST = 0.2f; // 도킹 접근 추가 비용
private const float HEURISTIC_WEIGHT = 1.0f; // 휴리스틱 가중치
#endregion
#region Fields
private List<MapNode> _mapNodes;
private NodeResolver _nodeResolver;
#endregion
#region Constructor
/// <summary> /// <summary>
/// 생성자 /// 생성자
/// </summary> /// </summary>
/// <param name="mapNodes">맵 노드 목록</param> public PathCalculator()
/// <param name="nodeResolver">노드 해결기</param>
public PathCalculator(List<MapNode> mapNodes, NodeResolver nodeResolver)
{ {
_mapNodes = mapNodes ?? throw new ArgumentNullException(nameof(mapNodes)); _agvPathfinder = new AGVPathfinder();
_nodeResolver = nodeResolver ?? throw new ArgumentNullException(nameof(nodeResolver)); _astarPathfinder = new AStarPathfinder();
_rfidPathfinder = new RfidBasedPathfinder();
} }
#endregion /// <summary>
/// 맵 노드 설정
#region Public Methods /// </summary>
/// <param name="mapNodes">맵 노드 목록</param>
public void SetMapNodes(List<MapNode> mapNodes)
{
_agvPathfinder.SetMapNodes(mapNodes);
_astarPathfinder.SetMapNodes(mapNodes);
}
/// <summary> /// <summary>
/// 경로 계산 (메인 메서드) /// 맵 데이터 설정
/// </summary>
/// <param name="mapNodes">맵 노드 목록</param>
public void SetMapData(List<MapNode> mapNodes)
{
_agvPathfinder.SetMapNodes(mapNodes);
_astarPathfinder.SetMapNodes(mapNodes);
// RfidPathfinder는 MapNode의 RFID 정보를 직접 사용
_rfidPathfinder.SetMapNodes(mapNodes);
}
/// <summary>
/// AGV 경로 계산
/// </summary> /// </summary>
/// <param name="startNodeId">시작 노드 ID</param> /// <param name="startNodeId">시작 노드 ID</param>
/// <param name="targetNodeId">목 노드 ID</param> /// <param name="endNodeId">목적지 노드 ID</param>
/// <param name="currentDirection">현재 AGV 방향</param> /// <param name="targetDirection">목적지 도착 방향</param>
/// <returns>AGV 경로 계산 결과</returns>
public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection = null)
{
return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection);
}
/// <summary>
/// 충전 스테이션으로의 경로 찾기
/// </summary>
/// <param name="startNodeId">시작 노드 ID</param>
/// <returns>AGV 경로 계산 결과</returns>
public AGVPathResult FindPathToChargingStation(string startNodeId)
{
return _agvPathfinder.FindPathToChargingStation(startNodeId);
}
/// <summary>
/// 도킹 스테이션으로의 경로 찾기
/// </summary>
/// <param name="startNodeId">시작 노드 ID</param>
/// <param name="stationType">장비 타입</param>
/// <returns>AGV 경로 계산 결과</returns>
public AGVPathResult FindPathToDockingStation(string startNodeId, StationType stationType)
{
return _agvPathfinder.FindPathToDockingStation(startNodeId, stationType);
}
/// <summary>
/// 여러 목적지 중 가장 가까운 노드로의 경로 찾기
/// </summary>
/// <param name="startNodeId">시작 노드 ID</param>
/// <param name="targetNodeIds">목적지 후보 노드 ID 목록</param>
/// <returns>경로 계산 결과</returns> /// <returns>경로 계산 결과</returns>
public PathResult CalculatePath(string startNodeId, string targetNodeId, AgvDirection currentDirection) public PathResult FindNearestPath(string startNodeId, List<string> targetNodeIds)
{ {
var stopwatch = Stopwatch.StartNew(); return _astarPathfinder.FindNearestPath(startNodeId, targetNodeIds);
try
{
// 입력 검증
var validationResult = ValidateInput(startNodeId, targetNodeId, currentDirection);
if (!validationResult.Success)
{
stopwatch.Stop();
validationResult.CalculationTime = stopwatch.ElapsedMilliseconds;
return validationResult;
}
// A* 알고리즘 실행
var result = ExecuteAStar(startNodeId, targetNodeId, currentDirection);
stopwatch.Stop();
result.CalculationTime = stopwatch.ElapsedMilliseconds;
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
return new PathResult($"경로 계산 중 오류 발생: {ex.Message}")
{
CalculationTime = stopwatch.ElapsedMilliseconds
};
}
} }
/// <summary> /// <summary>
/// 경로 유효성 검증 (RFID 이탈 감지시 사용) /// 두 노드가 연결되어 있는지 확인
/// </summary> /// </summary>
/// <param name="currentPath">현재 경로</param> /// <param name="nodeId1">노드 1 ID</param>
/// <param name="currentRfidId">현재 감지된 RFID</param> /// <param name="nodeId2">노드 2 ID</param>
/// <returns>경로 유효성 여부</returns> /// <returns>연결 여부</returns>
public bool ValidateCurrentPath(PathResult currentPath, string currentRfidId) public bool AreNodesConnected(string nodeId1, string nodeId2)
{ {
if (currentPath == null || !currentPath.Success) return _astarPathfinder.AreNodesConnected(nodeId1, nodeId2);
return false;
var currentNode = _nodeResolver.GetNodeByRfid(currentRfidId);
if (currentNode == null)
return false;
// 현재 노드가 계획된 경로에 포함되어 있는지 확인
return currentPath.NodeSequence.Contains(currentNode.NodeId);
} }
/// <summary> /// <summary>
/// 동적 경로 재계산 (경로 이탈시 사용) /// 경로 유효성 검증
/// </summary> /// </summary>
/// <param name="currentRfidId">현재 RFID 위치</param> /// <param name="path">검증할 경로</param>
/// <param name="targetNodeId">목표 노드 ID</param> /// <returns>유효성 검증 결과</returns>
/// <param name="currentDirection">현재 방향</param> public bool ValidatePath(List<string> path)
/// <param name="originalPath">원래 경로 (참고용)</param>
/// <returns>새로운 경로</returns>
public PathResult RecalculatePath(string currentRfidId, string targetNodeId,
AgvDirection currentDirection, PathResult originalPath = null)
{ {
var currentNode = _nodeResolver.GetNodeByRfid(currentRfidId); return _agvPathfinder.ValidatePath(path);
if (currentNode == null)
{
return new PathResult("현재 위치를 확인할 수 없습니다.");
}
// 새로운 경로 계산
var result = CalculatePath(currentNode.NodeId, targetNodeId, currentDirection);
// 원래 경로와 비교 (로그용)
if (originalPath != null && result.Success)
{
// TODO: 경로 변경 로그 기록
}
return result;
}
#endregion
#region Private Methods - Input Validation
/// <summary>
/// 입력 값 검증
/// </summary>
private PathResult ValidateInput(string startNodeId, string targetNodeId, AgvDirection currentDirection)
{
if (string.IsNullOrEmpty(startNodeId))
return new PathResult("시작 노드가 지정되지 않았습니다.");
if (string.IsNullOrEmpty(targetNodeId))
return new PathResult("목표 노드가 지정되지 않았습니다.");
var startNode = _mapNodes.FirstOrDefault(n => n.NodeId == startNodeId);
if (startNode == null)
return new PathResult($"시작 노드를 찾을 수 없습니다: {startNodeId}");
var targetNode = _mapNodes.FirstOrDefault(n => n.NodeId == targetNodeId);
if (targetNode == null)
return new PathResult($"목표 노드를 찾을 수 없습니다: {targetNodeId}");
if (startNodeId == targetNodeId)
return new PathResult("시작점과 목표점이 동일합니다.");
return new PathResult { Success = true };
}
#endregion
#region Private Methods - A* Algorithm
/// <summary>
/// A* 알고리즘 실행
/// </summary>
private PathResult ExecuteAStar(string startNodeId, string targetNodeId, AgvDirection currentDirection)
{
var openSet = new SortedSet<PathNode>();
var closedSet = new HashSet<string>();
var gScore = new Dictionary<string, float>();
// 시작 노드 설정
var startPathNode = new PathNode(startNodeId, currentDirection)
{
GCost = 0,
HCost = CalculateHeuristic(startNodeId, targetNodeId)
};
openSet.Add(startPathNode);
gScore[startPathNode.GetKey()] = 0;
while (openSet.Count > 0)
{
// 가장 낮은 F 비용을 가진 노드 선택
var current = openSet.Min;
openSet.Remove(current);
// 목표 도달 확인
if (current.NodeId == targetNodeId)
{
// 도킹 방향 검증
if (IsValidDockingApproach(targetNodeId, current.Direction))
{
return ReconstructPath(current, startNodeId, targetNodeId, currentDirection);
}
// 도킹 방향이 맞지 않으면 계속 탐색
}
closedSet.Add(current.GetKey());
// 인접 노드들 처리
ProcessNeighbors(current, targetNodeId, openSet, closedSet, gScore);
}
return new PathResult("경로를 찾을 수 없습니다.");
} }
/// <summary> /// <summary>
/// 인접 노드들 처리 /// 네비게이션 가능한 노드 목록 반환
/// </summary> /// </summary>
private void ProcessNeighbors(PathNode current, string targetNodeId, /// <returns>노드 ID 목록</returns>
SortedSet<PathNode> openSet, HashSet<string> closedSet, public List<string> GetNavigationNodes()
Dictionary<string, float> gScore)
{ {
var currentMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == current.NodeId); return _astarPathfinder.GetNavigationNodes();
if (currentMapNode == null) return;
foreach (var neighborId in currentMapNode.ConnectedNodes)
{
var neighborMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == neighborId);
if (neighborMapNode == null) continue;
// 가능한 모든 방향으로 이웃 노드 방문
foreach (var direction in GetPossibleDirections(current, neighborMapNode))
{
var neighborPathNode = new PathNode(neighborId, direction);
var neighborKey = neighborPathNode.GetKey();
if (closedSet.Contains(neighborKey))
continue;
// 이동 비용 계산
var moveCost = CalculateMoveCost(current, neighborPathNode, neighborMapNode);
var tentativeGScore = current.GCost + moveCost;
// 더 좋은 경로인지 확인
if (!gScore.ContainsKey(neighborKey) || tentativeGScore < gScore[neighborKey])
{
// 경로 정보 업데이트
neighborPathNode.Parent = current;
neighborPathNode.GCost = tentativeGScore;
neighborPathNode.HCost = CalculateHeuristic(neighborId, targetNodeId);
neighborPathNode.RotationCount = current.RotationCount +
(current.Direction != direction ? 1 : 0);
// 이동 명령 시퀀스 구성
neighborPathNode.MovementSequence = GenerateMovementSequence(current, neighborPathNode);
gScore[neighborKey] = tentativeGScore;
// openSet에 추가 (중복 제거)
openSet.RemoveWhere(n => n.GetKey() == neighborKey);
openSet.Add(neighborPathNode);
}
}
}
} }
/// <summary> /// <summary>
/// 가능한 방향들 계산 /// AGV 현재 방향 설정
/// </summary> /// </summary>
private List<AgvDirection> GetPossibleDirections(PathNode current, MapNode neighborNode) /// <param name="direction">현재 방향</param>
public void SetCurrentDirection(AgvDirection direction)
{ {
var directions = new List<AgvDirection>(); _agvPathfinder.CurrentDirection = direction;
// 기본적으로 전진/후진 가능
directions.Add(AgvDirection.Forward);
directions.Add(AgvDirection.Backward);
// 회전 가능한 노드에서만 방향 전환 가능
if (CanRotateAt(current.NodeId))
{
// 현재 방향과 다른 방향도 고려
if (current.Direction == AgvDirection.Forward)
directions.Add(AgvDirection.Backward);
else if (current.Direction == AgvDirection.Backward)
directions.Add(AgvDirection.Forward);
}
return directions;
} }
/// <summary> /// <summary>
/// 이동 비용 계산 /// 회전 비용 가중치 설정
/// </summary> /// </summary>
private float CalculateMoveCost(PathNode from, PathNode to, MapNode toMapNode) /// <param name="weight">회전 비용 가중치</param>
public void SetRotationCostWeight(float weight)
{ {
float cost = BASE_MOVE_COST; _agvPathfinder.RotationCostWeight = weight;
// 방향 전환 비용
if (from.Direction != to.Direction)
{
cost += ROTATION_COST;
}
// 도킹 스테이션 접근 비용
if (toMapNode.Type == NodeType.Docking || toMapNode.Type == NodeType.Charging)
{
cost += DOCKING_APPROACH_COST;
}
// 실제 거리 기반 비용 (좌표가 있는 경우)
var fromMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == from.NodeId);
if (fromMapNode != null && toMapNode != null)
{
var distance = CalculateDistance(fromMapNode.Position, toMapNode.Position);
cost *= (distance / 100.0f); // 좌표 단위를 거리 단위로 조정
}
return cost;
} }
/// <summary> /// <summary>
/// 휴리스틱 함수 (목표까지의 추정 거리) /// 휴리스틱 가중치 설정
/// </summary> /// </summary>
private float CalculateHeuristic(string fromNodeId, string toNodeId) /// <param name="weight">휴리스틱 가중치</param>
public void SetHeuristicWeight(float weight)
{ {
var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == fromNodeId); _astarPathfinder.HeuristicWeight = weight;
var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId);
if (fromNode == null || toNode == null)
return 0;
// 유클리드 거리 계산
var distance = CalculateDistance(fromNode.Position, toNode.Position);
return distance * HEURISTIC_WEIGHT / 100.0f; // 좌표 단위 조정
} }
/// <summary> /// <summary>
/// 두 점 사이의 거리 계산 /// 최대 탐색 노드 수 설정
/// </summary> /// </summary>
private float CalculateDistance(Point from, Point to) /// <param name="maxNodes">최대 탐색 노드 수</param>
public void SetMaxSearchNodes(int maxNodes)
{ {
var dx = to.X - from.X; _astarPathfinder.MaxSearchNodes = maxNodes;
var dy = to.Y - from.Y; }
return (float)Math.Sqrt(dx * dx + dy * dy);
// ==================== RFID 기반 경로 계산 메서드들 ====================
/// <summary>
/// RFID 기반 AGV 경로 계산
/// </summary>
/// <param name="startRfidId">시작 RFID</param>
/// <param name="endRfidId">목적지 RFID</param>
/// <param name="targetDirection">목적지 도착 방향</param>
/// <returns>RFID 기반 AGV 경로 계산 결과</returns>
public RfidPathResult FindAGVPathByRfid(string startRfidId, string endRfidId, AgvDirection? targetDirection = null)
{
return _rfidPathfinder.FindAGVPath(startRfidId, endRfidId, targetDirection);
} }
/// <summary> /// <summary>
/// 회전 가능한 위치인지 확인 /// RFID 기반 충전소 경로 찾기
/// </summary> /// </summary>
private bool CanRotateAt(string nodeId) /// <param name="startRfidId">시작 RFID</param>
/// <returns>RFID 기반 경로 계산 결과</returns>
public RfidPathResult FindPathToChargingStationByRfid(string startRfidId)
{ {
var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); return _rfidPathfinder.FindPathToChargingStation(startRfidId);
return node != null && (node.CanRotate || node.Type == NodeType.Rotation);
} }
/// <summary> /// <summary>
/// 도킹 접근 방향이 유효한지 확인 /// RFID 기반 도킹 스테이션 경로 찾기
/// </summary> /// </summary>
private bool IsValidDockingApproach(string nodeId, AgvDirection approachDirection) /// <param name="startRfidId">시작 RFID</param>
/// <param name="stationType">장비 타입</param>
/// <returns>RFID 기반 경로 계산 결과</returns>
public RfidPathResult FindPathToDockingStationByRfid(string startRfidId, StationType stationType)
{ {
var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); return _rfidPathfinder.FindPathToDockingStation(startRfidId, stationType);
if (node == null) return true;
// 도킹/충전 스테이션이 아니면 방향 제약 없음
if (node.Type != NodeType.Docking && node.Type != NodeType.Charging)
return true;
// 도킹 방향 확인
if (node.DockDirection == null)
return true;
// 충전기는 전진으로만, 다른 장비는 후진으로만
if (node.Type == NodeType.Charging)
return approachDirection == AgvDirection.Forward;
else
return approachDirection == AgvDirection.Backward;
} }
/// <summary> /// <summary>
/// 이동 명령 시퀀스 생성 /// 여러 RFID 목적지 중 가장 가까운 곳으로의 경로 찾기
/// </summary> /// </summary>
private List<AgvDirection> GenerateMovementSequence(PathNode from, PathNode to) /// <param name="startRfidId">시작 RFID</param>
/// <param name="targetRfidIds">목적지 후보 RFID 목록</param>
/// <returns>RFID 기반 경로 계산 결과</returns>
public RfidPathResult FindNearestPathByRfid(string startRfidId, List<string> targetRfidIds)
{ {
var sequence = new List<AgvDirection>(); return _rfidPathfinder.FindNearestPath(startRfidId, targetRfidIds);
// 방향 전환이 필요한 경우
if (from.Direction != to.Direction)
{
if (from.Direction == AgvDirection.Forward && to.Direction == AgvDirection.Backward)
{
sequence.Add(AgvDirection.Right); // 180도 회전 (마크센서까지)
}
else if (from.Direction == AgvDirection.Backward && to.Direction == AgvDirection.Forward)
{
sequence.Add(AgvDirection.Right); // 180도 회전 (마크센서까지)
}
}
// 이동 명령
sequence.Add(to.Direction);
return sequence;
} }
/// <summary> /// <summary>
/// 경로 재구성 /// RFID 매핑 정보 조회 (MapNode 반환)
/// </summary> /// </summary>
private PathResult ReconstructPath(PathNode goalNode, string startNodeId, string targetNodeId, AgvDirection startDirection) /// <param name="rfidId">RFID</param>
/// <returns>MapNode 또는 null</returns>
public MapNode GetRfidMapping(string rfidId)
{ {
var path = new List<PathNode>(); return _rfidPathfinder.GetRfidMapping(rfidId);
var current = goalNode;
// 역순으로 경로 구성
while (current != null)
{
path.Insert(0, current);
current = current.Parent;
}
return new PathResult(path, startNodeId, targetNodeId, startDirection);
}
#endregion
#region Public Utility Methods
/// <summary>
/// 노드 간 직선 거리 계산
/// </summary>
public float GetDistance(string fromNodeId, string toNodeId)
{
var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == fromNodeId);
var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId);
if (fromNode == null || toNode == null)
return float.MaxValue;
return CalculateDistance(fromNode.Position, toNode.Position);
} }
/// <summary> /// <summary>
/// 특정 노드에서 가능한 다음 노드들 조회 /// RFID로 NodeId 조회
/// </summary> /// </summary>
public List<string> GetPossibleNextNodes(string currentNodeId, AgvDirection currentDirection) /// <param name="rfidId">RFID</param>
/// <returns>NodeId 또는 null</returns>
public string GetNodeIdByRfid(string rfidId)
{ {
var currentNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNodeId); return _rfidPathfinder.GetNodeIdByRfid(rfidId);
if (currentNode == null)
return new List<string>();
return currentNode.ConnectedNodes.ToList();
} }
/// <summary> /// <summary>
/// 경로 최적화 (선택적 기능) /// NodeId로 RFID 조회
/// </summary> /// </summary>
public PathResult OptimizePath(PathResult originalPath) /// <param name="nodeId">NodeId</param>
/// <returns>RFID 또는 null</returns>
public string GetRfidByNodeId(string nodeId)
{ {
if (originalPath == null || !originalPath.Success) return _rfidPathfinder.GetRfidByNodeId(nodeId);
return originalPath;
// TODO: 경로 최적화 로직 구현
// - 불필요한 중간 정지점 제거
// - 회전 최소화
// - 경로 단순화
return originalPath;
} }
#endregion /// <summary>
/// 활성화된 RFID 목록 반환
/// </summary>
/// <returns>활성화된 RFID 목록</returns>
public List<string> GetActiveRfidList()
{
return _rfidPathfinder.GetActiveRfidList();
}
/// <summary>
/// RFID pathfinder의 AGV 현재 방향 설정
/// </summary>
/// <param name="direction">현재 방향</param>
public void SetRfidPathfinderCurrentDirection(AgvDirection direction)
{
_rfidPathfinder.CurrentDirection = direction;
}
/// <summary>
/// RFID pathfinder의 회전 비용 가중치 설정
/// </summary>
/// <param name="weight">회전 비용 가중치</param>
public void SetRfidPathfinderRotationCostWeight(float weight)
{
_rfidPathfinder.RotationCostWeight = weight;
}
} }
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using AGVNavigationCore.Models;
namespace AGVMapEditor.Models namespace AGVMapEditor.Models
{ {

View File

@@ -13,14 +13,14 @@ namespace AGVMapEditor
/// 애플리케이션의 기본 진입점입니다. /// 애플리케이션의 기본 진입점입니다.
/// </summary> /// </summary>
[STAThread] [STAThread]
static void Main() static void Main(string[] args)
{ {
// Windows Forms 애플리케이션 초기화 // Windows Forms 애플리케이션 초기화
Application.EnableVisualStyles(); Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false); Application.SetCompatibleTextRenderingDefault(false);
// 메인 폼 실행 // 메인 폼 실행 (명령줄 인수 전달)
Application.Run(new MainForm()); Application.Run(new MainForm(args));
} }
} }
} }

View File

@@ -0,0 +1,29 @@
@echo off
echo Building V2GDecoder VC++ Project...
REM Check if Visual Studio 2022 is installed (Professional or Community)
set MSBUILD_PRO="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
set MSBUILD_COM="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
set MSBUILD_BT="F:\(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\MSBuild.exe"
if exist %MSBUILD_PRO% (
echo "Found Visual Studio 2022 Professional"
set MSBUILD=%MSBUILD_PRO%
) else if exist %MSBUILD_COM% (
echo "Found Visual Studio 2022 Community"
set MSBUILD=%MSBUILD_COM%
) else if exist %MSBUILD_BT% (
echo "Found Visual Studio 2022 BuildTools"
set MSBUILD=%MSBUILD_BT%
) else (
echo "Visual Studio 2022 (Professional or Community) not found!"
echo "Please install Visual Studio 2022 or update the MSBuild path."
pause
exit /b 1
)
REM Build Debug x64 configuration
echo Building Debug x64 configuration...
%MSBUILD% agvmapeditor.csproj
pause

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>AGVNavigationCore</RootNamespace>
<AssemblyName>AGVNavigationCore</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Models\Enums.cs" />
<Compile Include="Models\MapLoader.cs" />
<Compile Include="Models\MapNode.cs" />
<Compile Include="Models\RfidMapping.cs" />
<Compile Include="PathFinding\PathNode.cs" />
<Compile Include="PathFinding\PathResult.cs" />
<Compile Include="PathFinding\AStarPathfinder.cs" />
<Compile Include="PathFinding\AGVPathfinder.cs" />
<Compile Include="PathFinding\AGVPathResult.cs" />
<Compile Include="PathFinding\RfidBasedPathfinder.cs" />
<Compile Include="PathFinding\RfidPathResult.cs" />
<Compile Include="Controls\UnifiedAGVCanvas.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Controls\UnifiedAGVCanvas.Designer.cs">
<DependentUpon>UnifiedAGVCanvas.cs</DependentUpon>
</Compile>
<Compile Include="Controls\UnifiedAGVCanvas.Events.cs">
<DependentUpon>UnifiedAGVCanvas.cs</DependentUpon>
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Controls\UnifiedAGVCanvas.Mouse.cs">
<DependentUpon>UnifiedAGVCanvas.cs</DependentUpon>
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="Utils\" />
</ItemGroup>
<ItemGroup>
<None Include="build.bat" />
<None Include="packages.config" />
<None Include="README.md" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -0,0 +1,41 @@
namespace AGVNavigationCore.Controls
{
partial class UnifiedAGVCanvas
{
/// <summary>
/// 필수 디자이너 변수입니다.
/// </summary>
private System.ComponentModel.IContainer components = null;
#region
/// <summary>
/// 디자이너 지원에 필요한 메서드입니다.
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
/// </summary>
private void InitializeComponent()
{
this.SuspendLayout();
//
// UnifiedAGVCanvas
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.White;
this.Name = "UnifiedAGVCanvas";
this.Size = new System.Drawing.Size(800, 600);
this.Paint += new System.Windows.Forms.PaintEventHandler(this.UnifiedAGVCanvas_Paint);
this.MouseClick += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseClick);
this.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseDoubleClick);
this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseDown);
this.MouseMove += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseMove);
this.MouseUp += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseUp);
this.MouseWheel += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseWheel);
this.ResumeLayout(false);
}
#endregion
}
}

View File

@@ -0,0 +1,801 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Windows.Forms;
using AGVNavigationCore.Models;
using AGVNavigationCore.PathFinding;
namespace AGVNavigationCore.Controls
{
public partial class UnifiedAGVCanvas
{
#region Paint Events
private void UnifiedAGVCanvas_Paint(object sender, PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.InterpolationMode = InterpolationMode.High;
// 변환 행렬 설정 (줌 및 팬)
var transform = new Matrix();
transform.Scale(_zoomFactor, _zoomFactor);
transform.Translate(_panOffset.X, _panOffset.Y);
g.Transform = transform;
try
{
// 그리드 그리기
if (_showGrid)
{
DrawGrid(g);
}
// 노드 연결선 그리기
DrawConnections(g);
// 경로 그리기
DrawPaths(g);
// 노드 그리기
DrawNodes(g);
// AGV 그리기
DrawAGVs(g);
// 임시 연결선 그리기 (편집 모드)
if (_canvasMode == CanvasMode.Edit && _isConnectionMode)
{
DrawTemporaryConnection(g);
}
}
finally
{
g.Transform = new Matrix(); // 변환 행렬 리셋
}
// UI 정보 그리기 (변환 없이)
DrawUIInfo(g);
}
private void DrawGrid(Graphics g)
{
if (!_showGrid) return;
var bounds = GetVisibleBounds();
var gridSize = (int)(GRID_SIZE * _zoomFactor);
if (gridSize < 5) return; // 너무 작으면 그리지 않음
for (int x = bounds.Left; x < bounds.Right; x += GRID_SIZE)
{
if (x % (GRID_SIZE * 5) == 0)
g.DrawLine(new Pen(Color.Gray, 1), x, bounds.Top, x, bounds.Bottom);
else
g.DrawLine(_gridPen, x, bounds.Top, x, bounds.Bottom);
}
for (int y = bounds.Top; y < bounds.Bottom; y += GRID_SIZE)
{
if (y % (GRID_SIZE * 5) == 0)
g.DrawLine(new Pen(Color.Gray, 1), bounds.Left, y, bounds.Right, y);
else
g.DrawLine(_gridPen, bounds.Left, y, bounds.Right, y);
}
}
private void DrawConnections(Graphics g)
{
if (_nodes == null) return;
foreach (var node in _nodes)
{
if (node.ConnectedNodes == null) continue;
foreach (var connectedNodeId in node.ConnectedNodes)
{
var targetNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
if (targetNode == null) continue;
DrawConnection(g, node, targetNode);
}
}
}
private void DrawConnection(Graphics g, MapNode fromNode, MapNode toNode)
{
var startPoint = fromNode.Position;
var endPoint = toNode.Position;
// 연결선만 그리기 (단순한 도로 연결, 방향성 없음)
g.DrawLine(_connectionPen, startPoint, endPoint);
}
private void DrawDirectionArrow(Graphics g, Point point, double angle, AgvDirection direction)
{
var arrowSize = CONNECTION_ARROW_SIZE;
var arrowAngle = Math.PI / 6; // 30도
var cos = Math.Cos(angle);
var sin = Math.Sin(angle);
var arrowPoint1 = new Point(
(int)(point.X - arrowSize * Math.Cos(angle - arrowAngle)),
(int)(point.Y - arrowSize * Math.Sin(angle - arrowAngle))
);
var arrowPoint2 = new Point(
(int)(point.X - arrowSize * Math.Cos(angle + arrowAngle)),
(int)(point.Y - arrowSize * Math.Sin(angle + arrowAngle))
);
var arrowColor = direction == AgvDirection.Forward ? Color.Blue : Color.Red;
var arrowPen = new Pen(arrowColor, 2);
g.DrawLine(arrowPen, point, arrowPoint1);
g.DrawLine(arrowPen, point, arrowPoint2);
arrowPen.Dispose();
}
private void DrawPaths(Graphics g)
{
// 모든 경로 그리기
if (_allPaths != null)
{
foreach (var path in _allPaths)
{
DrawPath(g, path, Color.LightBlue);
}
}
// 현재 선택된 경로 그리기
if (_currentPath != null)
{
DrawPath(g, _currentPath, Color.Purple);
}
}
private void DrawPath(Graphics g, PathResult path, Color color)
{
if (path?.Path == null || path.Path.Count < 2) return;
var pathPen = new Pen(color, 4) { DashStyle = DashStyle.Dash };
for (int i = 0; i < path.Path.Count - 1; i++)
{
var currentNodeId = path.Path[i];
var nextNodeId = path.Path[i + 1];
var currentNode = _nodes?.FirstOrDefault(n => n.NodeId == currentNodeId);
var nextNode = _nodes?.FirstOrDefault(n => n.NodeId == nextNodeId);
if (currentNode != null && nextNode != null)
{
// 경로 선 그리기
g.DrawLine(pathPen, currentNode.Position, nextNode.Position);
// 경로 방향 표시 (계산된 경로의 경우에만 방향 화살표 표시)
var midPoint = new Point(
(currentNode.Position.X + nextNode.Position.X) / 2,
(currentNode.Position.Y + nextNode.Position.Y) / 2
);
var angle = Math.Atan2(nextNode.Position.Y - currentNode.Position.Y,
nextNode.Position.X - currentNode.Position.X);
DrawDirectionArrow(g, midPoint, angle, AgvDirection.Forward);
}
}
pathPen.Dispose();
}
private void DrawNodes(Graphics g)
{
if (_nodes == null) return;
foreach (var node in _nodes)
{
DrawNode(g, node);
}
}
private void DrawNode(Graphics g, MapNode node)
{
switch (node.Type)
{
case NodeType.Label:
DrawLabelNode(g, node);
break;
case NodeType.Image:
DrawImageNode(g, node);
break;
default:
DrawCircularNode(g, node);
break;
}
}
private void DrawCircularNode(Graphics g, MapNode node)
{
var brush = GetNodeBrush(node);
switch (node.Type)
{
case NodeType.Docking:
DrawPentagonNode(g, node, brush);
break;
case NodeType.Charging:
DrawTriangleNode(g, node, brush);
break;
default:
DrawCircleNode(g, node, brush);
break;
}
}
private void DrawCircleNode(Graphics g, MapNode node, Brush brush)
{
var rect = new Rectangle(
node.Position.X - NODE_RADIUS,
node.Position.Y - NODE_RADIUS,
NODE_SIZE,
NODE_SIZE
);
// 노드 그리기
g.FillEllipse(brush, rect);
g.DrawEllipse(Pens.Black, rect);
// 선택된 노드 강조
if (node == _selectedNode)
{
g.DrawEllipse(_selectedNodePen, rect);
}
// 호버된 노드 강조
if (node == _hoveredNode)
{
var hoverRect = new Rectangle(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4);
g.DrawEllipse(new Pen(Color.Orange, 2), hoverRect);
}
DrawNodeLabel(g, node);
}
private void DrawPentagonNode(Graphics g, MapNode node, Brush brush)
{
var radius = NODE_RADIUS;
var center = node.Position;
// 5각형 꼭짓점 계산 (위쪽부터 시계방향)
var points = new Point[5];
for (int i = 0; i < 5; i++)
{
var angle = (Math.PI * 2 * i / 5) - Math.PI / 2; // -90도부터 시작 (위쪽)
points[i] = new Point(
(int)(center.X + radius * Math.Cos(angle)),
(int)(center.Y + radius * Math.Sin(angle))
);
}
// 5각형 그리기
g.FillPolygon(brush, points);
g.DrawPolygon(Pens.Black, points);
// 선택된 노드 강조
if (node == _selectedNode)
{
g.DrawPolygon(_selectedNodePen, points);
}
// 호버된 노드 강조
if (node == _hoveredNode)
{
// 확장된 5각형 계산
var hoverPoints = new Point[5];
for (int i = 0; i < 5; i++)
{
var angle = (Math.PI * 2 * i / 5) - Math.PI / 2;
hoverPoints[i] = new Point(
(int)(center.X + (radius + 3) * Math.Cos(angle)),
(int)(center.Y + (radius + 3) * Math.Sin(angle))
);
}
g.DrawPolygon(new Pen(Color.Orange, 2), hoverPoints);
}
DrawNodeLabel(g, node);
}
private void DrawTriangleNode(Graphics g, MapNode node, Brush brush)
{
var radius = NODE_RADIUS;
var center = node.Position;
// 삼각형 꼭짓점 계산 (위쪽 꼭짓점부터 시계방향)
var points = new Point[3];
for (int i = 0; i < 3; i++)
{
var angle = (Math.PI * 2 * i / 3) - Math.PI / 2; // -90도부터 시작 (위쪽)
points[i] = new Point(
(int)(center.X + radius * Math.Cos(angle)),
(int)(center.Y + radius * Math.Sin(angle))
);
}
// 삼각형 그리기
g.FillPolygon(brush, points);
g.DrawPolygon(Pens.Black, points);
// 선택된 노드 강조
if (node == _selectedNode)
{
g.DrawPolygon(_selectedNodePen, points);
}
// 호버된 노드 강조
if (node == _hoveredNode)
{
// 확장된 삼각형 계산
var hoverPoints = new Point[3];
for (int i = 0; i < 3; i++)
{
var angle = (Math.PI * 2 * i / 3) - Math.PI / 2;
hoverPoints[i] = new Point(
(int)(center.X + (radius + 3) * Math.Cos(angle)),
(int)(center.Y + (radius + 3) * Math.Sin(angle))
);
}
g.DrawPolygon(new Pen(Color.Orange, 2), hoverPoints);
}
DrawNodeLabel(g, node);
}
private void DrawNodeLabel(Graphics g, MapNode node)
{
string displayText;
Color textColor;
string descriptionText;
// 위쪽에 표시할 설명 (노드의 Description 속성)
descriptionText = string.IsNullOrEmpty(node.Description) ? "" : node.Description;
// 아래쪽에 표시할 값 (RFID 우선, 없으면 노드ID)
if (node.HasRfid())
{
// RFID가 있는 경우: 순수 RFID 값만 표시 (진한 색상)
displayText = node.RfidId;
textColor = Color.Black;
}
else
{
// RFID가 없는 경우: 노드 ID 표시 (연한 색상)
displayText = node.NodeId;
textColor = Color.Gray;
}
var font = new Font("Arial", 8, FontStyle.Bold);
var descFont = new Font("Arial", 6, FontStyle.Regular);
// 메인 텍스트 크기 측정
var textSize = g.MeasureString(displayText, font);
var descSize = g.MeasureString(descriptionText, descFont);
// 설명 텍스트 위치 (노드 위쪽)
var descPoint = new Point(
(int)(node.Position.X - descSize.Width / 2),
(int)(node.Position.Y - NODE_RADIUS - descSize.Height - 2)
);
// 메인 텍스트 위치 (노드 아래쪽)
var textPoint = new Point(
(int)(node.Position.X - textSize.Width / 2),
(int)(node.Position.Y + NODE_RADIUS + 2)
);
// 설명 텍스트 그리기 (설명이 있는 경우에만)
if (!string.IsNullOrEmpty(descriptionText))
{
using (var descBrush = new SolidBrush(Color.FromArgb(120, Color.Black)))
{
g.DrawString(descriptionText, descFont, descBrush, descPoint);
}
}
// 메인 텍스트 그리기
using (var textBrush = new SolidBrush(textColor))
{
g.DrawString(displayText, font, textBrush, textPoint);
}
font.Dispose();
descFont.Dispose();
}
private void DrawLabelNode(Graphics g, MapNode node)
{
var text = string.IsNullOrEmpty(node.LabelText) ? node.NodeId : node.LabelText;
// 폰트 설정
var font = new Font(node.FontFamily, node.FontSize, node.FontStyle);
var textBrush = new SolidBrush(node.ForeColor);
// 텍스트 크기 측정
var textSize = g.MeasureString(text, font);
var textPoint = new Point(
(int)(node.Position.X - textSize.Width / 2),
(int)(node.Position.Y - textSize.Height / 2)
);
// 배경 그리기 (설정된 경우)
if (node.ShowBackground)
{
var backgroundBrush = new SolidBrush(node.BackColor);
var backgroundRect = new Rectangle(
textPoint.X - 2,
textPoint.Y - 2,
(int)textSize.Width + 4,
(int)textSize.Height + 4
);
g.FillRectangle(backgroundBrush, backgroundRect);
g.DrawRectangle(Pens.Black, backgroundRect);
backgroundBrush.Dispose();
}
// 텍스트 그리기
g.DrawString(text, font, textBrush, textPoint);
// 선택된 노드 강조
if (node == _selectedNode)
{
var selectionRect = new Rectangle(
textPoint.X - 4,
textPoint.Y - 4,
(int)textSize.Width + 8,
(int)textSize.Height + 8
);
g.DrawRectangle(_selectedNodePen, selectionRect);
}
// 호버된 노드 강조
if (node == _hoveredNode)
{
var hoverRect = new Rectangle(
textPoint.X - 6,
textPoint.Y - 6,
(int)textSize.Width + 12,
(int)textSize.Height + 12
);
g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect);
}
font.Dispose();
textBrush.Dispose();
}
private void DrawImageNode(Graphics g, MapNode node)
{
// 이미지 로드 (필요시)
if (node.LoadedImage == null && !string.IsNullOrEmpty(node.ImagePath))
{
node.LoadImage();
}
if (node.LoadedImage != null)
{
// 실제 표시 크기 계산
var displaySize = node.GetDisplaySize();
if (displaySize.IsEmpty)
displaySize = new Size(50, 50); // 기본 크기
var imageRect = new Rectangle(
node.Position.X - displaySize.Width / 2,
node.Position.Y - displaySize.Height / 2,
displaySize.Width,
displaySize.Height
);
// 회전이 있는 경우
if (node.Rotation != 0)
{
var oldTransform = g.Transform;
g.TranslateTransform(node.Position.X, node.Position.Y);
g.RotateTransform(node.Rotation);
g.TranslateTransform(-node.Position.X, -node.Position.Y);
// 투명도 적용하여 이미지 그리기
if (node.Opacity < 1.0f)
{
var imageAttributes = new System.Drawing.Imaging.ImageAttributes();
var colorMatrix = new System.Drawing.Imaging.ColorMatrix();
colorMatrix.Matrix33 = node.Opacity;
imageAttributes.SetColorMatrix(colorMatrix, System.Drawing.Imaging.ColorMatrixFlag.Default,
System.Drawing.Imaging.ColorAdjustType.Bitmap);
g.DrawImage(node.LoadedImage, imageRect, 0, 0, node.LoadedImage.Width, node.LoadedImage.Height,
GraphicsUnit.Pixel, imageAttributes);
imageAttributes.Dispose();
}
else
{
g.DrawImage(node.LoadedImage, imageRect);
}
g.Transform = oldTransform;
}
else
{
// 투명도 적용하여 이미지 그리기
if (node.Opacity < 1.0f)
{
var imageAttributes = new System.Drawing.Imaging.ImageAttributes();
var colorMatrix = new System.Drawing.Imaging.ColorMatrix();
colorMatrix.Matrix33 = node.Opacity;
imageAttributes.SetColorMatrix(colorMatrix, System.Drawing.Imaging.ColorMatrixFlag.Default,
System.Drawing.Imaging.ColorAdjustType.Bitmap);
g.DrawImage(node.LoadedImage, imageRect, 0, 0, node.LoadedImage.Width, node.LoadedImage.Height,
GraphicsUnit.Pixel, imageAttributes);
imageAttributes.Dispose();
}
else
{
g.DrawImage(node.LoadedImage, imageRect);
}
}
// 선택된 노드 강조
if (node == _selectedNode)
{
g.DrawRectangle(_selectedNodePen, imageRect);
}
// 호버된 노드 강조
if (node == _hoveredNode)
{
var hoverRect = new Rectangle(imageRect.X - 2, imageRect.Y - 2, imageRect.Width + 4, imageRect.Height + 4);
g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect);
}
}
else
{
// 이미지가 없는 경우 기본 사각형으로 표시
var rect = new Rectangle(
node.Position.X - 25,
node.Position.Y - 25,
50,
50
);
g.FillRectangle(Brushes.LightGray, rect);
g.DrawRectangle(Pens.Black, rect);
// "이미지 없음" 텍스트
var font = new Font("Arial", 8);
var text = "No Image";
var textSize = g.MeasureString(text, font);
var textPoint = new Point(
(int)(node.Position.X - textSize.Width / 2),
(int)(node.Position.Y - textSize.Height / 2)
);
g.DrawString(text, font, Brushes.Black, textPoint);
font.Dispose();
// 선택된 노드 강조
if (node == _selectedNode)
{
g.DrawRectangle(_selectedNodePen, rect);
}
// 호버된 노드 강조
if (node == _hoveredNode)
{
var hoverRect = new Rectangle(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4);
g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect);
}
}
}
private Brush GetNodeBrush(MapNode node)
{
switch (node.Type)
{
case NodeType.Normal:
return _normalNodeBrush;
case NodeType.Rotation:
return _rotationNodeBrush;
case NodeType.Docking:
return _dockingNodeBrush;
case NodeType.Charging:
return _chargingNodeBrush;
case NodeType.Label:
return new SolidBrush(Color.Purple);
case NodeType.Image:
return new SolidBrush(Color.Brown);
default:
return _normalNodeBrush;
}
}
private void DrawAGVs(Graphics g)
{
if (_agvList == null) return;
foreach (var agv in _agvList)
{
if (_agvPositions.ContainsKey(agv.AgvId))
{
DrawAGV(g, agv);
}
}
}
private void DrawAGV(Graphics g, IAGV agv)
{
if (!_agvPositions.ContainsKey(agv.AgvId)) return;
var position = _agvPositions[agv.AgvId];
var direction = _agvDirections.ContainsKey(agv.AgvId) ? _agvDirections[agv.AgvId] : AgvDirection.Forward;
var state = _agvStates.ContainsKey(agv.AgvId) ? _agvStates[agv.AgvId] : AGVState.Idle;
// AGV 색상 결정
var brush = GetAGVBrush(state);
// AGV 사각형 그리기
var rect = new Rectangle(
position.X - AGV_SIZE / 2,
position.Y - AGV_SIZE / 2,
AGV_SIZE,
AGV_SIZE
);
g.FillRectangle(brush, rect);
g.DrawRectangle(_agvPen, rect);
// 방향 표시 (화살표)
DrawAGVDirection(g, position, direction);
// AGV ID 표시
var font = new Font("Arial", 10, FontStyle.Bold);
var textSize = g.MeasureString(agv.AgvId, font);
var textPoint = new Point(
(int)(position.X - textSize.Width / 2),
(int)(position.Y - AGV_SIZE / 2 - textSize.Height - 2)
);
g.DrawString(agv.AgvId, font, Brushes.Black, textPoint);
// 배터리 레벨 표시
var batteryText = $"{agv.BatteryLevel:F0}%";
var batterySize = g.MeasureString(batteryText, font);
var batteryPoint = new Point(
(int)(position.X - batterySize.Width / 2),
(int)(position.Y + AGV_SIZE / 2 + 2)
);
g.DrawString(batteryText, font, Brushes.Black, batteryPoint);
font.Dispose();
}
private Brush GetAGVBrush(AGVState state)
{
switch (state)
{
case AGVState.Idle:
return Brushes.LightGray;
case AGVState.Moving:
return Brushes.LightGreen;
case AGVState.Rotating:
return Brushes.Yellow;
case AGVState.Docking:
return Brushes.Orange;
case AGVState.Charging:
return Brushes.Blue;
case AGVState.Error:
return Brushes.Red;
default:
return Brushes.LightGray;
}
}
private void DrawAGVDirection(Graphics g, Point position, AgvDirection direction)
{
var arrowSize = 10;
Point[] arrowPoints = null;
switch (direction)
{
case AgvDirection.Forward:
arrowPoints = new Point[]
{
new Point(position.X + arrowSize, position.Y),
new Point(position.X - arrowSize/2, position.Y - arrowSize/2),
new Point(position.X - arrowSize/2, position.Y + arrowSize/2)
};
break;
case AgvDirection.Backward:
arrowPoints = new Point[]
{
new Point(position.X - arrowSize, position.Y),
new Point(position.X + arrowSize/2, position.Y - arrowSize/2),
new Point(position.X + arrowSize/2, position.Y + arrowSize/2)
};
break;
}
if (arrowPoints != null)
{
g.FillPolygon(Brushes.White, arrowPoints);
g.DrawPolygon(Pens.Black, arrowPoints);
}
}
private void DrawTemporaryConnection(Graphics g)
{
if (_connectionStartNode != null && _connectionEndPoint != Point.Empty)
{
g.DrawLine(_tempConnectionPen, _connectionStartNode.Position, _connectionEndPoint);
}
}
private void DrawUIInfo(Graphics g)
{
// 회사 로고
if (_companyLogo != null)
{
var logoRect = new Rectangle(10, 10, 100, 50);
g.DrawImage(_companyLogo, logoRect);
}
// 측정 정보
if (!string.IsNullOrEmpty(_measurementInfo))
{
var font = new Font("Arial", 9);
var textBrush = new SolidBrush(Color.Black);
var backgroundBrush = new SolidBrush(Color.FromArgb(200, Color.White));
var textSize = g.MeasureString(_measurementInfo, font);
var textRect = new Rectangle(
Width - (int)textSize.Width - 20,
Height - (int)textSize.Height - 20,
(int)textSize.Width + 10,
(int)textSize.Height + 10
);
g.FillRectangle(backgroundBrush, textRect);
g.DrawRectangle(Pens.Gray, textRect);
g.DrawString(_measurementInfo, font, textBrush, textRect.X + 5, textRect.Y + 5);
font.Dispose();
textBrush.Dispose();
backgroundBrush.Dispose();
}
// 줌 정보
var zoomText = $"Zoom: {_zoomFactor:P0}";
var zoomFont = new Font("Arial", 10, FontStyle.Bold);
var zoomSize = g.MeasureString(zoomText, zoomFont);
var zoomPoint = new Point(10, Height - (int)zoomSize.Height - 10);
g.FillRectangle(new SolidBrush(Color.FromArgb(200, Color.White)),
zoomPoint.X - 5, zoomPoint.Y - 5,
zoomSize.Width + 10, zoomSize.Height + 10);
g.DrawString(zoomText, zoomFont, Brushes.Black, zoomPoint);
zoomFont.Dispose();
}
private Rectangle GetVisibleBounds()
{
var left = (int)(-_panOffset.X / _zoomFactor);
var top = (int)(-_panOffset.Y / _zoomFactor);
var right = (int)((Width - _panOffset.X) / _zoomFactor);
var bottom = (int)((Height - _panOffset.Y) / _zoomFactor);
return new Rectangle(left, top, right - left, bottom - top);
}
#endregion
}
}

View File

@@ -0,0 +1,605 @@
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using AGVNavigationCore.Models;
namespace AGVNavigationCore.Controls
{
public partial class UnifiedAGVCanvas
{
#region Mouse Events
private void UnifiedAGVCanvas_MouseClick(object sender, MouseEventArgs e)
{
Focus(); // 포커스 설정
if (_canvasMode == CanvasMode.ViewOnly) return;
var worldPoint = ScreenToWorld(e.Location);
var hitNode = GetNodeAt(worldPoint);
switch (_editMode)
{
case EditMode.Select:
HandleSelectClick(hitNode);
break;
case EditMode.AddNode:
HandleAddNodeClick(worldPoint);
break;
case EditMode.Connect:
HandleConnectClick(hitNode);
break;
case EditMode.Delete:
HandleDeleteClick(hitNode);
break;
}
}
private void UnifiedAGVCanvas_MouseDoubleClick(object sender, MouseEventArgs e)
{
if (_canvasMode == CanvasMode.ViewOnly) return;
var worldPoint = ScreenToWorld(e.Location);
var hitNode = GetNodeAt(worldPoint);
if (hitNode != null)
{
// 노드 속성 편집 (이벤트 발생)
NodeSelected?.Invoke(this, hitNode);
}
}
private void UnifiedAGVCanvas_MouseDown(object sender, MouseEventArgs e)
{
var worldPoint = ScreenToWorld(e.Location);
if (e.Button == MouseButtons.Left)
{
if (_canvasMode == CanvasMode.Edit && _editMode == EditMode.Move)
{
var hitNode = GetNodeAt(worldPoint);
if (hitNode != null)
{
_isDragging = true;
_selectedNode = hitNode;
_dragOffset = new Point(
worldPoint.X - hitNode.Position.X,
worldPoint.Y - hitNode.Position.Y
);
Cursor = Cursors.SizeAll;
Invalidate();
return;
}
}
// 팬 시작 (우클릭 또는 중간버튼)
_isPanning = true;
_lastMousePosition = e.Location;
Cursor = Cursors.SizeAll;
}
else if (e.Button == MouseButtons.Right)
{
// 컨텍스트 메뉴 (편집 모드에서만)
if (_canvasMode == CanvasMode.Edit)
{
var hitNode = GetNodeAt(worldPoint);
ShowContextMenu(e.Location, hitNode);
}
}
}
private void UnifiedAGVCanvas_MouseMove(object sender, MouseEventArgs e)
{
var worldPoint = ScreenToWorld(e.Location);
// 호버 노드 업데이트
var newHoveredNode = GetNodeAt(worldPoint);
if (newHoveredNode != _hoveredNode)
{
_hoveredNode = newHoveredNode;
Invalidate();
}
if (_isPanning)
{
// 팬 처리
var deltaX = e.X - _lastMousePosition.X;
var deltaY = e.Y - _lastMousePosition.Y;
_panOffset.X += deltaX;
_panOffset.Y += deltaY;
_lastMousePosition = e.Location;
Invalidate();
}
else if (_isDragging && _canvasMode == CanvasMode.Edit)
{
// 노드 드래그
if (_selectedNode != null)
{
var newPosition = new Point(
worldPoint.X - _dragOffset.X,
worldPoint.Y - _dragOffset.Y
);
// 그리드 스냅
if (ModifierKeys.HasFlag(Keys.Control))
{
newPosition.X = (newPosition.X / GRID_SIZE) * GRID_SIZE;
newPosition.Y = (newPosition.Y / GRID_SIZE) * GRID_SIZE;
}
_selectedNode.Position = newPosition;
NodeMoved?.Invoke(this, _selectedNode);
MapChanged?.Invoke(this, EventArgs.Empty);
Invalidate();
}
}
else if (_isConnectionMode && _canvasMode == CanvasMode.Edit)
{
// 임시 연결선 업데이트
_connectionEndPoint = worldPoint;
Invalidate();
}
// 툴팁 표시 (호버된 노드/AGV 정보)
UpdateTooltip(worldPoint);
}
private void UnifiedAGVCanvas_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (_isDragging && _canvasMode == CanvasMode.Edit)
{
_isDragging = false;
Cursor = GetCursorForMode(_editMode);
}
if (_isPanning)
{
_isPanning = false;
Cursor = Cursors.Default;
}
}
}
private void UnifiedAGVCanvas_MouseWheel(object sender, MouseEventArgs e)
{
// 줌 처리
var mouseWorldPoint = ScreenToWorld(e.Location);
var oldZoom = _zoomFactor;
if (e.Delta > 0)
_zoomFactor = Math.Min(_zoomFactor * 1.2f, 5.0f);
else
_zoomFactor = Math.Max(_zoomFactor / 1.2f, 0.1f);
// 마우스 위치를 중심으로 줌
var zoomRatio = _zoomFactor / oldZoom;
_panOffset.X = (int)(e.X - (e.X - _panOffset.X) * zoomRatio);
_panOffset.Y = (int)(e.Y - (e.Y - _panOffset.Y) * zoomRatio);
Invalidate();
}
#endregion
#region Mouse Helper Methods
private Point ScreenToWorld(Point screenPoint)
{
return new Point(
(int)((screenPoint.X - _panOffset.X) / _zoomFactor),
(int)((screenPoint.Y - _panOffset.Y) / _zoomFactor)
);
}
private Point WorldToScreen(Point worldPoint)
{
return new Point(
(int)(worldPoint.X * _zoomFactor + _panOffset.X),
(int)(worldPoint.Y * _zoomFactor + _panOffset.Y)
);
}
private MapNode GetNodeAt(Point worldPoint)
{
if (_nodes == null) return null;
// 역순으로 검사하여 위에 그려진 노드부터 확인
for (int i = _nodes.Count - 1; i >= 0; i--)
{
var node = _nodes[i];
if (IsPointInNode(worldPoint, node))
return node;
}
return null;
}
private bool IsPointInNode(Point point, MapNode node)
{
switch (node.Type)
{
case NodeType.Label:
return IsPointInLabelNode(point, node);
case NodeType.Image:
return IsPointInImageNode(point, node);
default:
return IsPointInCircularNode(point, node);
}
}
private bool IsPointInCircularNode(Point point, MapNode node)
{
switch (node.Type)
{
case NodeType.Docking:
return IsPointInPentagon(point, node);
case NodeType.Charging:
return IsPointInTriangle(point, node);
default:
return IsPointInCircle(point, node);
}
}
private bool IsPointInCircle(Point point, MapNode node)
{
var hitRadius = Math.Max(NODE_RADIUS, 10 / _zoomFactor);
var distance = Math.Sqrt(
Math.Pow(node.Position.X - point.X, 2) +
Math.Pow(node.Position.Y - point.Y, 2)
);
return distance <= hitRadius;
}
private bool IsPointInPentagon(Point point, MapNode node)
{
var radius = NODE_RADIUS;
var center = node.Position;
// 5각형 꼭짓점 계산
var points = new Point[5];
for (int i = 0; i < 5; i++)
{
var angle = (Math.PI * 2 * i / 5) - Math.PI / 2;
points[i] = new Point(
(int)(center.X + radius * Math.Cos(angle)),
(int)(center.Y + radius * Math.Sin(angle))
);
}
return IsPointInPolygon(point, points);
}
private bool IsPointInTriangle(Point point, MapNode node)
{
var radius = NODE_RADIUS;
var center = node.Position;
// 삼각형 꼭짓점 계산
var points = new Point[3];
for (int i = 0; i < 3; i++)
{
var angle = (Math.PI * 2 * i / 3) - Math.PI / 2;
points[i] = new Point(
(int)(center.X + radius * Math.Cos(angle)),
(int)(center.Y + radius * Math.Sin(angle))
);
}
return IsPointInPolygon(point, points);
}
private bool IsPointInPolygon(Point point, Point[] polygon)
{
// Ray casting 알고리즘 사용
bool inside = false;
int j = polygon.Length - 1;
for (int i = 0; i < polygon.Length; i++)
{
var xi = polygon[i].X;
var yi = polygon[i].Y;
var xj = polygon[j].X;
var yj = polygon[j].Y;
if (((yi > point.Y) != (yj > point.Y)) &&
(point.X < (xj - xi) * (point.Y - yi) / (yj - yi) + xi))
{
inside = !inside;
}
j = i;
}
return inside;
}
private bool IsPointInLabelNode(Point point, MapNode node)
{
var text = string.IsNullOrEmpty(node.LabelText) ? node.NodeId : node.LabelText;
// 임시 Graphics로 텍스트 크기 측정
using (var tempBitmap = new Bitmap(1, 1))
using (var tempGraphics = Graphics.FromImage(tempBitmap))
{
var font = new Font(node.FontFamily, node.FontSize, node.FontStyle);
var textSize = tempGraphics.MeasureString(text, font);
var textRect = new Rectangle(
(int)(node.Position.X - textSize.Width / 2),
(int)(node.Position.Y - textSize.Height / 2),
(int)textSize.Width,
(int)textSize.Height
);
font.Dispose();
return textRect.Contains(point);
}
}
private bool IsPointInImageNode(Point point, MapNode node)
{
var displaySize = node.GetDisplaySize();
if (displaySize.IsEmpty)
displaySize = new Size(50, 50); // 기본 크기
var imageRect = new Rectangle(
node.Position.X - displaySize.Width / 2,
node.Position.Y - displaySize.Height / 2,
displaySize.Width,
displaySize.Height
);
return imageRect.Contains(point);
}
private IAGV GetAGVAt(Point worldPoint)
{
if (_agvList == null) return null;
var hitRadius = Math.Max(AGV_SIZE / 2, 15 / _zoomFactor);
return _agvList.FirstOrDefault(agv =>
{
if (!_agvPositions.ContainsKey(agv.AgvId)) return false;
var agvPos = _agvPositions[agv.AgvId];
var distance = Math.Sqrt(
Math.Pow(agvPos.X - worldPoint.X, 2) +
Math.Pow(agvPos.Y - worldPoint.Y, 2)
);
return distance <= hitRadius;
});
}
private void HandleSelectClick(MapNode hitNode)
{
if (hitNode != _selectedNode)
{
_selectedNode = hitNode;
NodeSelected?.Invoke(this, hitNode);
Invalidate();
}
}
private void HandleAddNodeClick(Point worldPoint)
{
// 그리드 스냅
if (ModifierKeys.HasFlag(Keys.Control))
{
worldPoint.X = (worldPoint.X / GRID_SIZE) * GRID_SIZE;
worldPoint.Y = (worldPoint.Y / GRID_SIZE) * GRID_SIZE;
}
var newNode = new MapNode
{
NodeId = $"N{_nodeCounter:D3}",
Position = worldPoint,
Type = NodeType.Normal
};
_nodeCounter++;
_nodes.Add(newNode);
NodeAdded?.Invoke(this, newNode);
MapChanged?.Invoke(this, EventArgs.Empty);
Invalidate();
}
private void HandleConnectClick(MapNode hitNode)
{
if (hitNode == null) return;
if (!_isConnectionMode)
{
// 연결 시작
_isConnectionMode = true;
_connectionStartNode = hitNode;
_selectedNode = hitNode;
}
else
{
// 연결 완료
if (_connectionStartNode != null && _connectionStartNode != hitNode)
{
CreateConnection(_connectionStartNode, hitNode);
}
CancelConnection();
}
Invalidate();
}
private void HandleDeleteClick(MapNode hitNode)
{
if (hitNode == null) return;
// 연결된 모든 연결선도 제거
foreach (var node in _nodes)
{
node.RemoveConnection(hitNode.NodeId);
}
_nodes.Remove(hitNode);
if (_selectedNode == hitNode)
_selectedNode = null;
NodeDeleted?.Invoke(this, hitNode);
MapChanged?.Invoke(this, EventArgs.Empty);
Invalidate();
}
private void CreateConnection(MapNode fromNode, MapNode toNode)
{
// 중복 연결 체크
if (fromNode.ConnectedNodes.Contains(toNode.NodeId))
return;
fromNode.AddConnection(toNode.NodeId);
MapChanged?.Invoke(this, EventArgs.Empty);
}
private float CalculateDistance(Point from, Point to)
{
var dx = to.X - from.X;
var dy = to.Y - from.Y;
return (float)Math.Sqrt(dx * dx + dy * dy);
}
private void ShowContextMenu(Point location, MapNode hitNode)
{
_contextMenu.Items.Clear();
if (hitNode != null)
{
_contextMenu.Items.Add("노드 속성...", null, (s, e) => NodeSelected?.Invoke(this, hitNode));
_contextMenu.Items.Add("노드 삭제", null, (s, e) => HandleDeleteClick(hitNode));
_contextMenu.Items.Add("-");
}
_contextMenu.Items.Add("노드 추가", null, (s, e) =>
{
var worldPoint = ScreenToWorld(location);
HandleAddNodeClick(worldPoint);
});
_contextMenu.Items.Add("전체 맞춤", null, (s, e) => FitToNodes());
_contextMenu.Items.Add("줌 리셋", null, (s, e) => ResetZoom());
_contextMenu.Show(this, location);
}
private void UpdateTooltip(Point worldPoint)
{
string tooltipText = "";
// 노드 툴팁
var hitNode = GetNodeAt(worldPoint);
if (hitNode != null)
{
tooltipText = $"노드: {hitNode.NodeId}\n타입: {hitNode.Type}\n위치: ({hitNode.Position.X}, {hitNode.Position.Y})";
}
else
{
// AGV 툴팁
var hitAGV = GetAGVAt(worldPoint);
if (hitAGV != null)
{
var state = _agvStates.ContainsKey(hitAGV.AgvId) ? _agvStates[hitAGV.AgvId] : AGVState.Idle;
tooltipText = $"AGV: {hitAGV.AgvId}\n상태: {state}\n배터리: {hitAGV.BatteryLevel:F1}%\n위치: ({hitAGV.CurrentPosition.X}, {hitAGV.CurrentPosition.Y})";
}
}
// 툴팁 업데이트 (기존 ToolTip 컨트롤 사용)
if (!string.IsNullOrEmpty(tooltipText))
{
// ToolTip 설정 (필요시 추가 구현)
}
}
#endregion
#region View Control Methods
/// <summary>
/// 모든 노드가 보이도록 뷰 조정
/// </summary>
public void FitToNodes()
{
if (_nodes == null || _nodes.Count == 0) return;
var minX = _nodes.Min(n => n.Position.X);
var maxX = _nodes.Max(n => n.Position.X);
var minY = _nodes.Min(n => n.Position.Y);
var maxY = _nodes.Max(n => n.Position.Y);
var margin = 50;
var contentWidth = maxX - minX + margin * 2;
var contentHeight = maxY - minY + margin * 2;
var zoomX = (float)Width / contentWidth;
var zoomY = (float)Height / contentHeight;
_zoomFactor = Math.Min(zoomX, zoomY) * 0.9f;
var centerX = (minX + maxX) / 2;
var centerY = (minY + maxY) / 2;
_panOffset.X = (int)(Width / 2 - centerX * _zoomFactor);
_panOffset.Y = (int)(Height / 2 - centerY * _zoomFactor);
Invalidate();
}
/// <summary>
/// 줌 리셋
/// </summary>
public void ResetZoom()
{
_zoomFactor = 1.0f;
_panOffset = Point.Empty;
Invalidate();
}
/// <summary>
/// 특정 위치로 이동
/// </summary>
public void PanTo(Point worldPoint)
{
_panOffset.X = (int)(Width / 2 - worldPoint.X * _zoomFactor);
_panOffset.Y = (int)(Height / 2 - worldPoint.Y * _zoomFactor);
Invalidate();
}
/// <summary>
/// 특정 노드로 이동
/// </summary>
public void PanToNode(string nodeId)
{
var node = _nodes?.FirstOrDefault(n => n.NodeId == nodeId);
if (node != null)
{
PanTo(node.Position);
}
}
/// <summary>
/// 특정 AGV로 이동
/// </summary>
public void PanToAGV(string agvId)
{
if (_agvPositions.ContainsKey(agvId))
{
PanTo(_agvPositions[agvId]);
}
}
#endregion
}
}

View File

@@ -0,0 +1,536 @@
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
}

View File

@@ -0,0 +1,68 @@
using System;
namespace AGVNavigationCore.Models
{
/// <summary>
/// 노드 타입 열거형
/// </summary>
public enum NodeType
{
/// <summary>일반 경로 노드</summary>
Normal,
/// <summary>회전 가능 지점</summary>
Rotation,
/// <summary>도킹 스테이션</summary>
Docking,
/// <summary>충전 스테이션</summary>
Charging,
/// <summary>라벨 (UI 요소)</summary>
Label,
/// <summary>이미지 (UI 요소)</summary>
Image
}
/// <summary>
/// 도킹 방향 열거형
/// </summary>
public enum DockingDirection
{
/// <summary>전진 도킹 (충전기)</summary>
Forward,
/// <summary>후진 도킹 (로더, 클리너, 오프로더, 버퍼)</summary>
Backward
}
/// <summary>
/// AGV 이동 방향 열거형
/// </summary>
public enum AgvDirection
{
/// <summary>전진 (모니터 방향)</summary>
Forward,
/// <summary>후진 (리프트 방향)</summary>
Backward,
/// <summary>좌회전</summary>
Left,
/// <summary>우회전</summary>
Right,
/// <summary>정지</summary>
Stop
}
/// <summary>
/// 장비 타입 열거형
/// </summary>
public enum StationType
{
/// <summary>로더</summary>
Loader,
/// <summary>클리너</summary>
Cleaner,
/// <summary>오프로더</summary>
Offloader,
/// <summary>버퍼</summary>
Buffer,
/// <summary>충전기</summary>
Charger
}
}

View File

@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
namespace AGVNavigationCore.Models
{
/// <summary>
/// AGV 맵 파일 로딩/저장을 위한 공용 유틸리티 클래스
/// AGVMapEditor와 AGVSimulator에서 공통으로 사용
/// </summary>
public static class MapLoader
{
/// <summary>
/// 맵 파일 로딩 결과
/// </summary>
public class MapLoadResult
{
public bool Success { get; set; }
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
public string ErrorMessage { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public DateTime CreatedDate { get; set; }
}
/// <summary>
/// 맵 파일 저장용 데이터 구조
/// </summary>
public class MapFileData
{
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
public DateTime CreatedDate { get; set; }
public string Version { get; set; } = "1.0";
}
/// <summary>
/// 맵 파일을 로드하여 노드를 반환
/// </summary>
/// <param name="filePath">맵 파일 경로</param>
/// <returns>로딩 결과</returns>
public static MapLoadResult LoadMapFromFile(string filePath)
{
var result = new MapLoadResult();
try
{
if (!File.Exists(filePath))
{
result.ErrorMessage = $"파일을 찾을 수 없습니다: {filePath}";
return result;
}
var json = File.ReadAllText(filePath);
var mapData = JsonConvert.DeserializeObject<MapFileData>(json);
if (mapData != null)
{
result.Nodes = mapData.Nodes ?? new List<MapNode>();
result.Version = mapData.Version ?? "1.0";
result.CreatedDate = mapData.CreatedDate;
// 이미지 노드들의 이미지 로드
LoadImageNodes(result.Nodes);
result.Success = true;
}
else
{
result.ErrorMessage = "맵 데이터 파싱에 실패했습니다.";
}
}
catch (Exception ex)
{
result.ErrorMessage = $"맵 파일 로딩 중 오류 발생: {ex.Message}";
}
return result;
}
/// <summary>
/// 맵 데이터를 파일로 저장
/// </summary>
/// <param name="filePath">저장할 파일 경로</param>
/// <param name="nodes">맵 노드 목록</param>
/// <returns>저장 성공 여부</returns>
public static bool SaveMapToFile(string filePath, List<MapNode> nodes)
{
try
{
var mapData = new MapFileData
{
Nodes = nodes,
CreatedDate = DateTime.Now,
Version = "1.0"
};
var json = JsonConvert.SerializeObject(mapData, Formatting.Indented);
File.WriteAllText(filePath, json);
return true;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 이미지 노드들의 이미지 로드
/// </summary>
/// <param name="nodes">노드 목록</param>
private static void LoadImageNodes(List<MapNode> nodes)
{
foreach (var node in nodes)
{
if (node.Type == NodeType.Image)
{
node.LoadImage();
}
}
}
/// <summary>
/// MapNode 목록에서 RFID가 없는 노드들에 자동으로 RFID ID를 할당합니다.
/// </summary>
/// <param name="mapNodes">맵 노드 목록</param>
public static void AssignAutoRfidIds(List<MapNode> mapNodes)
{
foreach (var node in mapNodes)
{
// 네비게이션 가능한 노드이면서 RFID가 없는 경우에만 자동 할당
if (node.IsNavigationNode() && !node.HasRfid())
{
// 기본 RFID ID 생성 (N001 -> 001)
var rfidId = node.NodeId.Replace("N", "").PadLeft(3, '0');
node.SetRfidInfo(rfidId, "", "정상");
}
}
}
}
}

View File

@@ -0,0 +1,486 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
namespace AGVNavigationCore.Models
{
/// <summary>
/// 맵 노드 정보를 관리하는 클래스
/// 논리적 노드로서 실제 맵의 위치와 속성을 정의
/// </summary>
public class MapNode
{
/// <summary>
/// 논리적 노드 ID (맵 에디터에서 관리하는 고유 ID)
/// 예: "N001", "N002", "LOADER1", "CHARGER1"
/// </summary>
public string NodeId { get; set; } = string.Empty;
/// <summary>
/// 노드 표시 이름 (사용자 친화적)
/// 예: "로더1", "충전기1", "교차점A", "회전지점1"
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 맵 상의 위치 좌표 (픽셀 단위)
/// </summary>
public Point Position { get; set; } = Point.Empty;
/// <summary>
/// 노드 타입
/// </summary>
public NodeType Type { get; set; } = NodeType.Normal;
/// <summary>
/// 도킹 방향 (도킹/충전 노드인 경우만 사용)
/// </summary>
public DockingDirection? DockDirection { get; set; } = null;
/// <summary>
/// 연결된 노드 ID 목록 (경로 정보)
/// </summary>
public List<string> ConnectedNodes { get; set; } = new List<string>();
/// <summary>
/// 회전 가능 여부 (180도 회전 가능한 지점)
/// </summary>
public bool CanRotate { get; set; } = false;
/// <summary>
/// 장비 ID (도킹/충전 스테이션인 경우)
/// 예: "LOADER1", "CLEANER1", "BUFFER1", "CHARGER1"
/// </summary>
public string StationId { get; set; } = string.Empty;
/// <summary>
/// 장비 타입 (도킹/충전 스테이션인 경우)
/// </summary>
public StationType? StationType { get; set; } = null;
/// <summary>
/// 노드 생성 일자
/// </summary>
public DateTime CreatedDate { get; set; } = DateTime.Now;
/// <summary>
/// 노드 수정 일자
/// </summary>
public DateTime ModifiedDate { get; set; } = DateTime.Now;
/// <summary>
/// 노드 설명 (추가 정보)
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// 노드 활성화 여부
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// 노드 색상 (맵 에디터 표시용)
/// </summary>
public Color DisplayColor { get; set; } = Color.Blue;
/// <summary>
/// RFID 태그 ID (이 노드에 매핑된 RFID)
/// </summary>
public string RfidId { get; set; } = string.Empty;
/// <summary>
/// RFID 상태 (정상, 손상, 교체예정 등)
/// </summary>
public string RfidStatus { get; set; } = "정상";
/// <summary>
/// RFID 설치 위치 설명 (현장 작업자용)
/// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등
/// </summary>
public string RfidDescription { get; set; } = string.Empty;
/// <summary>
/// 라벨 텍스트 (NodeType.Label인 경우 사용)
/// </summary>
public string LabelText { get; set; } = string.Empty;
/// <summary>
/// 라벨 폰트 패밀리 (NodeType.Label인 경우 사용)
/// </summary>
public string FontFamily { get; set; } = "Arial";
/// <summary>
/// 라벨 폰트 크기 (NodeType.Label인 경우 사용)
/// </summary>
public float FontSize { get; set; } = 12.0f;
/// <summary>
/// 라벨 폰트 스타일 (NodeType.Label인 경우 사용)
/// </summary>
public FontStyle FontStyle { get; set; } = FontStyle.Regular;
/// <summary>
/// 라벨 전경색 (NodeType.Label인 경우 사용)
/// </summary>
public Color ForeColor { get; set; } = Color.Black;
/// <summary>
/// 라벨 배경색 (NodeType.Label인 경우 사용)
/// </summary>
public Color BackColor { get; set; } = Color.Transparent;
/// <summary>
/// 라벨 배경 표시 여부 (NodeType.Label인 경우 사용)
/// </summary>
public bool ShowBackground { get; set; } = false;
/// <summary>
/// 이미지 파일 경로 (NodeType.Image인 경우 사용)
/// </summary>
public string ImagePath { get; set; } = string.Empty;
/// <summary>
/// 이미지 크기 배율 (NodeType.Image인 경우 사용)
/// </summary>
public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f);
/// <summary>
/// 이미지 투명도 (NodeType.Image인 경우 사용, 0.0~1.0)
/// </summary>
public float Opacity { get; set; } = 1.0f;
/// <summary>
/// 이미지 회전 각도 (NodeType.Image인 경우 사용, 도 단위)
/// </summary>
public float Rotation { get; set; } = 0.0f;
/// <summary>
/// 로딩된 이미지 (런타임에서만 사용, JSON 직렬화 제외)
/// </summary>
[Newtonsoft.Json.JsonIgnore]
public Image LoadedImage { get; set; }
/// <summary>
/// 기본 생성자
/// </summary>
public MapNode()
{
}
/// <summary>
/// 매개변수 생성자
/// </summary>
/// <param name="nodeId">노드 ID</param>
/// <param name="name">노드 이름</param>
/// <param name="position">위치</param>
/// <param name="type">노드 타입</param>
public MapNode(string nodeId, string name, Point position, NodeType type)
{
NodeId = nodeId;
Name = name;
Position = position;
Type = type;
CreatedDate = DateTime.Now;
ModifiedDate = DateTime.Now;
// 타입별 기본 색상 설정
SetDefaultColorByType(type);
}
/// <summary>
/// 노드 타입에 따른 기본 색상 설정
/// </summary>
/// <param name="type">노드 타입</param>
public void SetDefaultColorByType(NodeType type)
{
switch (type)
{
case NodeType.Normal:
DisplayColor = Color.Blue;
break;
case NodeType.Rotation:
DisplayColor = Color.Orange;
break;
case NodeType.Docking:
DisplayColor = Color.Green;
break;
case NodeType.Charging:
DisplayColor = Color.Red;
break;
case NodeType.Label:
DisplayColor = Color.Purple;
break;
case NodeType.Image:
DisplayColor = Color.Brown;
break;
}
}
/// <summary>
/// 다른 노드와의 연결 추가
/// </summary>
/// <param name="nodeId">연결할 노드 ID</param>
public void AddConnection(string nodeId)
{
if (!ConnectedNodes.Contains(nodeId))
{
ConnectedNodes.Add(nodeId);
ModifiedDate = DateTime.Now;
}
}
/// <summary>
/// 다른 노드와의 연결 제거
/// </summary>
/// <param name="nodeId">연결 해제할 노드 ID</param>
public void RemoveConnection(string nodeId)
{
if (ConnectedNodes.Remove(nodeId))
{
ModifiedDate = DateTime.Now;
}
}
/// <summary>
/// 도킹 스테이션 설정
/// </summary>
/// <param name="stationId">장비 ID</param>
/// <param name="stationType">장비 타입</param>
/// <param name="dockDirection">도킹 방향</param>
public void SetDockingStation(string stationId, StationType stationType, DockingDirection dockDirection)
{
Type = NodeType.Docking;
StationId = stationId;
StationType = stationType;
DockDirection = dockDirection;
SetDefaultColorByType(NodeType.Docking);
ModifiedDate = DateTime.Now;
}
/// <summary>
/// 충전 스테이션 설정
/// </summary>
/// <param name="stationId">충전기 ID</param>
public void SetChargingStation(string stationId)
{
Type = NodeType.Charging;
StationId = stationId;
StationType = Models.StationType.Charger;
DockDirection = DockingDirection.Forward; // 충전기는 항상 전진 도킹
SetDefaultColorByType(NodeType.Charging);
ModifiedDate = DateTime.Now;
}
/// <summary>
/// 문자열 표현
/// </summary>
public override string ToString()
{
return $"{NodeId}: {Name} ({Type}) at ({Position.X}, {Position.Y})";
}
/// <summary>
/// 리스트박스 표시용 텍스트 (노드ID - 설명 - RFID 순서)
/// </summary>
public string DisplayText
{
get
{
var displayText = NodeId;
if (!string.IsNullOrEmpty(Description))
{
displayText += $" - {Description}";
}
if (!string.IsNullOrEmpty(RfidId))
{
displayText += $" - [{RfidId}]";
}
return displayText;
}
}
/// <summary>
/// 노드 복사
/// </summary>
/// <returns>복사된 노드</returns>
public MapNode Clone()
{
var clone = new MapNode
{
NodeId = NodeId,
Name = Name,
Position = Position,
Type = Type,
DockDirection = DockDirection,
ConnectedNodes = new List<string>(ConnectedNodes),
CanRotate = CanRotate,
StationId = StationId,
StationType = StationType,
CreatedDate = CreatedDate,
ModifiedDate = ModifiedDate,
Description = Description,
IsActive = IsActive,
DisplayColor = DisplayColor,
RfidId = RfidId,
RfidStatus = RfidStatus,
RfidDescription = RfidDescription,
LabelText = LabelText,
FontFamily = FontFamily,
FontSize = FontSize,
FontStyle = FontStyle,
ForeColor = ForeColor,
BackColor = BackColor,
ShowBackground = ShowBackground,
ImagePath = ImagePath,
Scale = Scale,
Opacity = Opacity,
Rotation = Rotation
};
return clone;
}
/// <summary>
/// 이미지 로드 (256x256 이상일 경우 자동 리사이즈)
/// </summary>
/// <returns>로드 성공 여부</returns>
public bool LoadImage()
{
if (Type != NodeType.Image) return false;
try
{
if (!string.IsNullOrEmpty(ImagePath) && System.IO.File.Exists(ImagePath))
{
LoadedImage?.Dispose();
var originalImage = Image.FromFile(ImagePath);
// 이미지 크기 체크 및 리사이즈
if (originalImage.Width > 256 || originalImage.Height > 256)
{
LoadedImage = ResizeImage(originalImage, 256, 256);
originalImage.Dispose();
}
else
{
LoadedImage = originalImage;
}
return true;
}
}
catch (Exception)
{
// 이미지 로드 실패
}
return false;
}
/// <summary>
/// 이미지 리사이즈 (비율 유지)
/// </summary>
/// <param name="image">원본 이미지</param>
/// <param name="maxWidth">최대 너비</param>
/// <param name="maxHeight">최대 높이</param>
/// <returns>리사이즈된 이미지</returns>
private Image ResizeImage(Image image, int maxWidth, int maxHeight)
{
// 비율 계산
double ratioX = (double)maxWidth / image.Width;
double ratioY = (double)maxHeight / image.Height;
double ratio = Math.Min(ratioX, ratioY);
// 새로운 크기 계산
int newWidth = (int)(image.Width * ratio);
int newHeight = (int)(image.Height * ratio);
// 리사이즈된 이미지 생성
var resizedImage = new Bitmap(newWidth, newHeight);
using (var graphics = Graphics.FromImage(resizedImage))
{
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.DrawImage(image, 0, 0, newWidth, newHeight);
}
return resizedImage;
}
/// <summary>
/// 실제 표시될 크기 계산 (이미지 노드인 경우)
/// </summary>
/// <returns>실제 크기</returns>
public Size GetDisplaySize()
{
if (Type != NodeType.Image || LoadedImage == null) return Size.Empty;
return new Size(
(int)(LoadedImage.Width * Scale.Width),
(int)(LoadedImage.Height * Scale.Height)
);
}
/// <summary>
/// 리소스 정리
/// </summary>
public void Dispose()
{
LoadedImage?.Dispose();
LoadedImage = null;
}
/// <summary>
/// 경로 찾기에 사용 가능한 노드인지 확인
/// (라벨, 이미지 노드는 경로 찾기에서 제외)
/// </summary>
public bool IsNavigationNode()
{
return Type != NodeType.Label && Type != NodeType.Image && IsActive;
}
/// <summary>
/// RFID가 할당되어 있는지 확인
/// </summary>
public bool HasRfid()
{
return !string.IsNullOrEmpty(RfidId);
}
/// <summary>
/// RFID 정보 설정
/// </summary>
/// <param name="rfidId">RFID ID</param>
/// <param name="rfidDescription">설치 위치 설명</param>
/// <param name="rfidStatus">RFID 상태</param>
public void SetRfidInfo(string rfidId, string rfidDescription = "", string rfidStatus = "정상")
{
RfidId = rfidId;
RfidDescription = rfidDescription;
RfidStatus = rfidStatus;
ModifiedDate = DateTime.Now;
}
/// <summary>
/// RFID 정보 삭제
/// </summary>
public void ClearRfidInfo()
{
RfidId = string.Empty;
RfidDescription = string.Empty;
RfidStatus = "정상";
ModifiedDate = DateTime.Now;
}
/// <summary>
/// RFID 기반 표시 텍스트 (RFID ID 우선, 없으면 노드ID)
/// </summary>
public string GetRfidDisplayText()
{
return HasRfid() ? RfidId : NodeId;
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
namespace AGVNavigationCore.Models
{
/// <summary>
/// RFID와 논리적 노드 ID를 매핑하는 클래스
/// 물리적 RFID는 의미없는 고유값, 논리적 노드는 맵 에디터에서 관리
/// </summary>
public class RfidMapping
{
/// <summary>
/// 물리적 RFID 값 (의미 없는 고유 식별자)
/// 예: "1234567890", "ABCDEF1234" 등
/// </summary>
public string RfidId { get; set; } = string.Empty;
/// <summary>
/// 논리적 노드 ID (맵 에디터에서 관리)
/// 예: "N001", "N002", "LOADER1", "CHARGER1" 등
/// </summary>
public string LogicalNodeId { get; set; } = string.Empty;
/// <summary>
/// 매핑 생성 일자
/// </summary>
public DateTime CreatedDate { get; set; } = DateTime.Now;
/// <summary>
/// 마지막 수정 일자
/// </summary>
public DateTime ModifiedDate { get; set; } = DateTime.Now;
/// <summary>
/// 설치 위치 설명 (현장 작업자용)
/// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// RFID 상태 (정상, 손상, 교체예정 등)
/// </summary>
public string Status { get; set; } = "정상";
/// <summary>
/// 매핑 활성화 여부
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// 기본 생성자
/// </summary>
public RfidMapping()
{
}
/// <summary>
/// 매개변수 생성자
/// </summary>
/// <param name="rfidId">물리적 RFID ID</param>
/// <param name="logicalNodeId">논리적 노드 ID</param>
/// <param name="description">설치 위치 설명</param>
public RfidMapping(string rfidId, string logicalNodeId, string description = "")
{
RfidId = rfidId;
LogicalNodeId = logicalNodeId;
Description = description;
CreatedDate = DateTime.Now;
ModifiedDate = DateTime.Now;
}
/// <summary>
/// 문자열 표현
/// </summary>
public override string ToString()
{
return $"{RfidId} → {LogicalNodeId} ({Description})";
}
}
}

View File

@@ -0,0 +1,244 @@
using System;
using System.Collections.Generic;
using AGVNavigationCore.Models;
namespace AGVNavigationCore.PathFinding
{
/// <summary>
/// AGV 경로 계산 결과 (방향성 및 명령어 포함)
/// </summary>
public class AGVPathResult
{
/// <summary>
/// 경로 찾기 성공 여부
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 경로 노드 ID 목록 (시작 → 목적지 순서)
/// </summary>
public List<string> Path { get; set; }
/// <summary>
/// AGV 명령어 목록 (이동 방향 시퀀스)
/// </summary>
public List<AgvDirection> Commands { get; set; }
/// <summary>
/// 총 거리
/// </summary>
public float TotalDistance { get; set; }
/// <summary>
/// 계산 소요 시간 (밀리초)
/// </summary>
public long CalculationTimeMs { get; set; }
/// <summary>
/// 예상 소요 시간 (초)
/// </summary>
public float EstimatedTimeSeconds { get; set; }
/// <summary>
/// 회전 횟수
/// </summary>
public int RotationCount { get; set; }
/// <summary>
/// 오류 메시지 (실패시)
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// 기본 생성자
/// </summary>
public AGVPathResult()
{
Success = false;
Path = new List<string>();
Commands = new List<AgvDirection>();
TotalDistance = 0;
CalculationTimeMs = 0;
EstimatedTimeSeconds = 0;
RotationCount = 0;
ErrorMessage = string.Empty;
}
/// <summary>
/// 성공 결과 생성
/// </summary>
/// <param name="path">경로</param>
/// <param name="commands">AGV 명령어 목록</param>
/// <param name="totalDistance">총 거리</param>
/// <param name="calculationTimeMs">계산 시간</param>
/// <returns>성공 결과</returns>
public static AGVPathResult CreateSuccess(List<string> path, List<AgvDirection> commands, float totalDistance, long calculationTimeMs)
{
var result = new AGVPathResult
{
Success = true,
Path = new List<string>(path),
Commands = new List<AgvDirection>(commands),
TotalDistance = totalDistance,
CalculationTimeMs = calculationTimeMs
};
result.CalculateMetrics();
return result;
}
/// <summary>
/// 실패 결과 생성
/// </summary>
/// <param name="errorMessage">오류 메시지</param>
/// <param name="calculationTimeMs">계산 시간</param>
/// <returns>실패 결과</returns>
public static AGVPathResult CreateFailure(string errorMessage, long calculationTimeMs)
{
return new AGVPathResult
{
Success = false,
ErrorMessage = errorMessage,
CalculationTimeMs = calculationTimeMs
};
}
/// <summary>
/// 경로 메트릭 계산
/// </summary>
private void CalculateMetrics()
{
RotationCount = CountRotations();
EstimatedTimeSeconds = CalculateEstimatedTime();
}
/// <summary>
/// 회전 횟수 계산
/// </summary>
private int CountRotations()
{
int count = 0;
foreach (var command in Commands)
{
if (command == AgvDirection.Left || command == AgvDirection.Right)
{
count++;
}
}
return count;
}
/// <summary>
/// 예상 소요 시간 계산
/// </summary>
/// <param name="agvSpeed">AGV 속도 (픽셀/초, 기본값: 100)</param>
/// <param name="rotationTime">회전 시간 (초, 기본값: 3)</param>
/// <returns>예상 소요 시간 (초)</returns>
private float CalculateEstimatedTime(float agvSpeed = 100.0f, float rotationTime = 3.0f)
{
float moveTime = TotalDistance / agvSpeed;
float totalRotationTime = RotationCount * rotationTime;
return moveTime + totalRotationTime;
}
/// <summary>
/// 명령어 요약 생성
/// </summary>
/// <returns>명령어 요약 문자열</returns>
public string GetCommandSummary()
{
if (!Success) return "실패";
var summary = new List<string>();
var currentCommand = AgvDirection.Stop;
var count = 0;
foreach (var command in Commands)
{
if (command == currentCommand)
{
count++;
}
else
{
if (count > 0)
{
summary.Add($"{GetCommandText(currentCommand)}×{count}");
}
currentCommand = command;
count = 1;
}
}
if (count > 0)
{
summary.Add($"{GetCommandText(currentCommand)}×{count}");
}
return string.Join(" → ", summary);
}
/// <summary>
/// 명령어 텍스트 반환
/// </summary>
private string GetCommandText(AgvDirection command)
{
switch (command)
{
case AgvDirection.Forward: return "전진";
case AgvDirection.Backward: return "후진";
case AgvDirection.Left: return "좌회전";
case AgvDirection.Right: return "우회전";
case AgvDirection.Stop: return "정지";
default: return command.ToString();
}
}
/// <summary>
/// 상세 경로 정보 반환
/// </summary>
/// <returns>상세 정보 문자열</returns>
public string GetDetailedInfo()
{
if (!Success)
{
return $"경로 계산 실패: {ErrorMessage} (계산시간: {CalculationTimeMs}ms)";
}
return $"경로: {Path.Count}개 노드, 거리: {TotalDistance:F1}px, " +
$"회전: {RotationCount}회, 예상시간: {EstimatedTimeSeconds:F1}초, " +
$"계산시간: {CalculationTimeMs}ms";
}
/// <summary>
/// PathResult로 변환 (호환성을 위해)
/// </summary>
/// <returns>PathResult 객체</returns>
public PathResult ToPathResult()
{
if (Success)
{
return PathResult.CreateSuccess(Path, TotalDistance, CalculationTimeMs, 0);
}
else
{
return PathResult.CreateFailure(ErrorMessage, CalculationTimeMs, 0);
}
}
/// <summary>
/// 문자열 표현
/// </summary>
public override string ToString()
{
if (Success)
{
return $"Success: {Path.Count} nodes, {TotalDistance:F1}px, {RotationCount} rotations, {EstimatedTimeSeconds:F1}s";
}
else
{
return $"Failed: {ErrorMessage}";
}
}
}
}

View File

@@ -0,0 +1,287 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AGVNavigationCore.Models;
namespace AGVNavigationCore.PathFinding
{
/// <summary>
/// AGV 특화 경로 탐색기 (방향성 및 도킹 제약 고려)
/// </summary>
public class AGVPathfinder
{
private AStarPathfinder _pathfinder;
private Dictionary<string, MapNode> _nodeMap;
/// <summary>
/// AGV 현재 방향
/// </summary>
public AgvDirection CurrentDirection { get; set; } = AgvDirection.Forward;
/// <summary>
/// 회전 비용 가중치 (회전이 비싼 동작임을 반영)
/// </summary>
public float RotationCostWeight { get; set; } = 50.0f;
/// <summary>
/// 도킹 접근 거리 (픽셀 단위)
/// </summary>
public float DockingApproachDistance { get; set; } = 100.0f;
/// <summary>
/// 생성자
/// </summary>
public AGVPathfinder()
{
_pathfinder = new AStarPathfinder();
_nodeMap = new Dictionary<string, MapNode>();
}
/// <summary>
/// 맵 노드 설정
/// </summary>
/// <param name="mapNodes">맵 노드 목록</param>
public void SetMapNodes(List<MapNode> mapNodes)
{
_pathfinder.SetMapNodes(mapNodes);
_nodeMap.Clear();
foreach (var node in mapNodes ?? new List<MapNode>())
{
_nodeMap[node.NodeId] = node;
}
}
/// <summary>
/// AGV 경로 계산 (방향성 및 도킹 제약 고려)
/// </summary>
/// <param name="startNodeId">시작 노드 ID</param>
/// <param name="endNodeId">목적지 노드 ID</param>
/// <param name="targetDirection">목적지 도착 방향 (null이면 자동 결정)</param>
/// <returns>AGV 경로 계산 결과</returns>
public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection = null)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
if (!_nodeMap.ContainsKey(startNodeId))
{
return AGVPathResult.CreateFailure($"시작 노드를 찾을 수 없습니다: {startNodeId}", stopwatch.ElapsedMilliseconds);
}
if (!_nodeMap.ContainsKey(endNodeId))
{
return AGVPathResult.CreateFailure($"목적지 노드를 찾을 수 없습니다: {endNodeId}", stopwatch.ElapsedMilliseconds);
}
var endNode = _nodeMap[endNodeId];
if (IsSpecialNode(endNode))
{
return FindPathToSpecialNode(startNodeId, endNode, targetDirection, stopwatch);
}
else
{
return FindNormalPath(startNodeId, endNodeId, targetDirection, stopwatch);
}
}
catch (Exception ex)
{
return AGVPathResult.CreateFailure($"AGV 경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds);
}
}
/// <summary>
/// 충전 스테이션으로의 경로 찾기
/// </summary>
/// <param name="startNodeId">시작 노드 ID</param>
/// <returns>AGV 경로 계산 결과</returns>
public AGVPathResult FindPathToChargingStation(string startNodeId)
{
var chargingStations = _nodeMap.Values
.Where(n => n.Type == NodeType.Charging && n.IsActive)
.Select(n => n.NodeId)
.ToList();
if (chargingStations.Count == 0)
{
return AGVPathResult.CreateFailure("사용 가능한 충전 스테이션이 없습니다", 0);
}
var nearestResult = _pathfinder.FindNearestPath(startNodeId, chargingStations);
if (!nearestResult.Success)
{
return AGVPathResult.CreateFailure("충전 스테이션으로의 경로를 찾을 수 없습니다", nearestResult.CalculationTimeMs);
}
var targetNodeId = nearestResult.Path.Last();
return FindAGVPath(startNodeId, targetNodeId, AgvDirection.Forward);
}
/// <summary>
/// 특정 타입의 도킹 스테이션으로의 경로 찾기
/// </summary>
/// <param name="startNodeId">시작 노드 ID</param>
/// <param name="stationType">장비 타입</param>
/// <returns>AGV 경로 계산 결과</returns>
public AGVPathResult FindPathToDockingStation(string startNodeId, StationType stationType)
{
var dockingStations = _nodeMap.Values
.Where(n => n.Type == NodeType.Docking && n.StationType == stationType && n.IsActive)
.Select(n => n.NodeId)
.ToList();
if (dockingStations.Count == 0)
{
return AGVPathResult.CreateFailure($"{stationType} 타입의 사용 가능한 도킹 스테이션이 없습니다", 0);
}
var nearestResult = _pathfinder.FindNearestPath(startNodeId, dockingStations);
if (!nearestResult.Success)
{
return AGVPathResult.CreateFailure($"{stationType} 도킹 스테이션으로의 경로를 찾을 수 없습니다", nearestResult.CalculationTimeMs);
}
var targetNodeId = nearestResult.Path.Last();
return FindAGVPath(startNodeId, targetNodeId, AgvDirection.Backward);
}
/// <summary>
/// 일반 노드로의 경로 계산
/// </summary>
private AGVPathResult FindNormalPath(string startNodeId, string endNodeId, AgvDirection? targetDirection, System.Diagnostics.Stopwatch stopwatch)
{
var result = _pathfinder.FindPath(startNodeId, endNodeId);
if (!result.Success)
{
return AGVPathResult.CreateFailure(result.ErrorMessage, stopwatch.ElapsedMilliseconds);
}
var agvCommands = GenerateAGVCommands(result.Path, targetDirection ?? AgvDirection.Forward);
return AGVPathResult.CreateSuccess(result.Path, agvCommands, result.TotalDistance, stopwatch.ElapsedMilliseconds);
}
/// <summary>
/// 특수 노드(도킹/충전)로의 경로 계산
/// </summary>
private AGVPathResult FindPathToSpecialNode(string startNodeId, MapNode endNode, AgvDirection? targetDirection, System.Diagnostics.Stopwatch stopwatch)
{
var requiredDirection = GetRequiredDirectionForNode(endNode);
var actualTargetDirection = targetDirection ?? requiredDirection;
var result = _pathfinder.FindPath(startNodeId, endNode.NodeId);
if (!result.Success)
{
return AGVPathResult.CreateFailure(result.ErrorMessage, stopwatch.ElapsedMilliseconds);
}
if (actualTargetDirection != requiredDirection)
{
return AGVPathResult.CreateFailure($"{endNode.NodeId}는 {requiredDirection} 방향으로만 접근 가능합니다", stopwatch.ElapsedMilliseconds);
}
var agvCommands = GenerateAGVCommands(result.Path, actualTargetDirection);
return AGVPathResult.CreateSuccess(result.Path, agvCommands, result.TotalDistance, stopwatch.ElapsedMilliseconds);
}
/// <summary>
/// 노드가 특수 노드(도킹/충전)인지 확인
/// </summary>
private bool IsSpecialNode(MapNode node)
{
return node.Type == NodeType.Docking || node.Type == NodeType.Charging;
}
/// <summary>
/// 노드에 필요한 접근 방향 반환
/// </summary>
private AgvDirection GetRequiredDirectionForNode(MapNode node)
{
switch (node.Type)
{
case NodeType.Charging:
return AgvDirection.Forward;
case NodeType.Docking:
return node.DockDirection == DockingDirection.Forward ? AgvDirection.Forward : AgvDirection.Backward;
default:
return AgvDirection.Forward;
}
}
/// <summary>
/// 경로에서 AGV 명령어 생성
/// </summary>
private List<AgvDirection> GenerateAGVCommands(List<string> path, AgvDirection targetDirection)
{
var commands = new List<AgvDirection>();
if (path.Count < 2) return commands;
var currentDir = CurrentDirection;
for (int i = 0; i < path.Count - 1; i++)
{
var currentNodeId = path[i];
var nextNodeId = path[i + 1];
if (_nodeMap.ContainsKey(currentNodeId) && _nodeMap.ContainsKey(nextNodeId))
{
var currentNode = _nodeMap[currentNodeId];
var nextNode = _nodeMap[nextNodeId];
if (currentNode.CanRotate && ShouldRotate(currentDir, targetDirection))
{
commands.Add(GetRotationCommand(currentDir, targetDirection));
currentDir = targetDirection;
}
commands.Add(currentDir);
}
}
return commands;
}
/// <summary>
/// 회전이 필요한지 판단
/// </summary>
private bool ShouldRotate(AgvDirection current, AgvDirection target)
{
return current != target && (current == AgvDirection.Forward && target == AgvDirection.Backward ||
current == AgvDirection.Backward && target == AgvDirection.Forward);
}
/// <summary>
/// 회전 명령어 반환
/// </summary>
private AgvDirection GetRotationCommand(AgvDirection from, AgvDirection to)
{
if (from == AgvDirection.Forward && to == AgvDirection.Backward)
return AgvDirection.Right;
if (from == AgvDirection.Backward && to == AgvDirection.Forward)
return AgvDirection.Right;
return AgvDirection.Right;
}
/// <summary>
/// 경로 유효성 검증
/// </summary>
/// <param name="path">검증할 경로</param>
/// <returns>유효성 검증 결과</returns>
public bool ValidatePath(List<string> path)
{
if (path == null || path.Count < 2) return true;
for (int i = 0; i < path.Count - 1; i++)
{
if (!_pathfinder.AreNodesConnected(path[i], path[i + 1]))
{
return false;
}
}
return true;
}
}
}

View File

@@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using AGVNavigationCore.Models;
namespace AGVNavigationCore.PathFinding
{
/// <summary>
/// A* 알고리즘 기반 경로 탐색기
/// </summary>
public class AStarPathfinder
{
private Dictionary<string, PathNode> _nodeMap;
private List<MapNode> _mapNodes;
/// <summary>
/// 휴리스틱 가중치 (기본값: 1.0)
/// 값이 클수록 목적지 방향을 우선시하나 최적 경로를 놓칠 수 있음
/// </summary>
public float HeuristicWeight { get; set; } = 1.0f;
/// <summary>
/// 최대 탐색 노드 수 (무한 루프 방지)
/// </summary>
public int MaxSearchNodes { get; set; } = 1000;
/// <summary>
/// 생성자
/// </summary>
public AStarPathfinder()
{
_nodeMap = new Dictionary<string, PathNode>();
_mapNodes = new List<MapNode>();
}
/// <summary>
/// 맵 노드 설정
/// </summary>
/// <param name="mapNodes">맵 노드 목록</param>
public void SetMapNodes(List<MapNode> mapNodes)
{
_mapNodes = mapNodes ?? new List<MapNode>();
_nodeMap.Clear();
// 1단계: 모든 네비게이션 노드를 PathNode로 변환
foreach (var mapNode in _mapNodes)
{
if (mapNode.IsNavigationNode())
{
var pathNode = new PathNode(mapNode.NodeId, mapNode.Position);
pathNode.ConnectedNodes = new List<string>(mapNode.ConnectedNodes);
_nodeMap[mapNode.NodeId] = pathNode;
}
}
// 2단계: 양방향 연결 자동 생성 (A→B 연결이 있으면 B→A도 추가)
EnsureBidirectionalConnections();
}
/// <summary>
/// 단방향 연결을 양방향으로 자동 변환
/// A→B 연결이 있으면 B→A 연결도 자동 생성
/// </summary>
private void EnsureBidirectionalConnections()
{
foreach (var nodeId in _nodeMap.Keys.ToList())
{
var node = _nodeMap[nodeId];
foreach (var connectedNodeId in node.ConnectedNodes.ToList())
{
// 연결된 노드가 존재하고 네비게이션 가능한 노드인지 확인
if (_nodeMap.ContainsKey(connectedNodeId))
{
var connectedNode = _nodeMap[connectedNodeId];
// 역방향 연결이 없으면 추가
if (!connectedNode.ConnectedNodes.Contains(nodeId))
{
connectedNode.ConnectedNodes.Add(nodeId);
}
}
}
}
}
/// <summary>
/// 경로 찾기 (A* 알고리즘)
/// </summary>
/// <param name="startNodeId">시작 노드 ID</param>
/// <param name="endNodeId">목적지 노드 ID</param>
/// <returns>경로 계산 결과</returns>
public PathResult FindPath(string startNodeId, string endNodeId)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
if (!_nodeMap.ContainsKey(startNodeId))
{
return PathResult.CreateFailure($"시작 노드를 찾을 수 없습니다: {startNodeId}", stopwatch.ElapsedMilliseconds, 0);
}
if (!_nodeMap.ContainsKey(endNodeId))
{
return PathResult.CreateFailure($"목적지 노드를 찾을 수 없습니다: {endNodeId}", stopwatch.ElapsedMilliseconds, 0);
}
if (startNodeId == endNodeId)
{
return PathResult.CreateSuccess(new List<string> { startNodeId }, 0, stopwatch.ElapsedMilliseconds, 1);
}
var startNode = _nodeMap[startNodeId];
var endNode = _nodeMap[endNodeId];
var openSet = new List<PathNode>();
var closedSet = new HashSet<string>();
var exploredCount = 0;
startNode.GCost = 0;
startNode.HCost = CalculateHeuristic(startNode, endNode);
startNode.Parent = null;
openSet.Add(startNode);
while (openSet.Count > 0 && exploredCount < MaxSearchNodes)
{
var currentNode = GetLowestFCostNode(openSet);
openSet.Remove(currentNode);
closedSet.Add(currentNode.NodeId);
exploredCount++;
if (currentNode.NodeId == endNodeId)
{
var path = ReconstructPath(currentNode);
var totalDistance = CalculatePathDistance(path);
return PathResult.CreateSuccess(path, totalDistance, stopwatch.ElapsedMilliseconds, exploredCount);
}
foreach (var neighborId in currentNode.ConnectedNodes)
{
if (closedSet.Contains(neighborId) || !_nodeMap.ContainsKey(neighborId))
continue;
var neighbor = _nodeMap[neighborId];
var tentativeGCost = currentNode.GCost + currentNode.DistanceTo(neighbor);
if (!openSet.Contains(neighbor))
{
neighbor.Parent = currentNode;
neighbor.GCost = tentativeGCost;
neighbor.HCost = CalculateHeuristic(neighbor, endNode);
openSet.Add(neighbor);
}
else if (tentativeGCost < neighbor.GCost)
{
neighbor.Parent = currentNode;
neighbor.GCost = tentativeGCost;
}
}
}
return PathResult.CreateFailure("경로를 찾을 수 없습니다", stopwatch.ElapsedMilliseconds, exploredCount);
}
catch (Exception ex)
{
return PathResult.CreateFailure($"경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds, 0);
}
}
/// <summary>
/// 여러 목적지 중 가장 가까운 노드로의 경로 찾기
/// </summary>
/// <param name="startNodeId">시작 노드 ID</param>
/// <param name="targetNodeIds">목적지 후보 노드 ID 목록</param>
/// <returns>경로 계산 결과</returns>
public PathResult FindNearestPath(string startNodeId, List<string> targetNodeIds)
{
if (targetNodeIds == null || targetNodeIds.Count == 0)
{
return PathResult.CreateFailure("목적지 노드가 지정되지 않았습니다", 0, 0);
}
PathResult bestResult = null;
foreach (var targetId in targetNodeIds)
{
var result = FindPath(startNodeId, targetId);
if (result.Success && (bestResult == null || result.TotalDistance < bestResult.TotalDistance))
{
bestResult = result;
}
}
return bestResult ?? PathResult.CreateFailure("모든 목적지로의 경로를 찾을 수 없습니다", 0, 0);
}
/// <summary>
/// 휴리스틱 거리 계산 (유클리드 거리)
/// </summary>
private float CalculateHeuristic(PathNode from, PathNode to)
{
return from.DistanceTo(to) * HeuristicWeight;
}
/// <summary>
/// F cost가 가장 낮은 노드 선택
/// </summary>
private PathNode GetLowestFCostNode(List<PathNode> nodes)
{
PathNode lowest = nodes[0];
foreach (var node in nodes)
{
if (node.FCost < lowest.FCost ||
(Math.Abs(node.FCost - lowest.FCost) < 0.001f && node.HCost < lowest.HCost))
{
lowest = node;
}
}
return lowest;
}
/// <summary>
/// 경로 재구성 (부모 노드를 따라 역추적)
/// </summary>
private List<string> ReconstructPath(PathNode endNode)
{
var path = new List<string>();
var current = endNode;
while (current != null)
{
path.Add(current.NodeId);
current = current.Parent;
}
path.Reverse();
return path;
}
/// <summary>
/// 경로의 총 거리 계산
/// </summary>
private float CalculatePathDistance(List<string> path)
{
if (path.Count < 2) return 0;
float totalDistance = 0;
for (int i = 0; i < path.Count - 1; i++)
{
if (_nodeMap.ContainsKey(path[i]) && _nodeMap.ContainsKey(path[i + 1]))
{
totalDistance += _nodeMap[path[i]].DistanceTo(_nodeMap[path[i + 1]]);
}
}
return totalDistance;
}
/// <summary>
/// 두 노드가 연결되어 있는지 확인
/// </summary>
/// <param name="nodeId1">노드 1 ID</param>
/// <param name="nodeId2">노드 2 ID</param>
/// <returns>연결 여부</returns>
public bool AreNodesConnected(string nodeId1, string nodeId2)
{
if (!_nodeMap.ContainsKey(nodeId1) || !_nodeMap.ContainsKey(nodeId2))
return false;
return _nodeMap[nodeId1].ConnectedNodes.Contains(nodeId2);
}
/// <summary>
/// 네비게이션 가능한 노드 목록 반환
/// </summary>
/// <returns>노드 ID 목록</returns>
public List<string> GetNavigationNodes()
{
return _nodeMap.Keys.ToList();
}
/// <summary>
/// 노드 정보 반환
/// </summary>
/// <param name="nodeId">노드 ID</param>
/// <returns>노드 정보 또는 null</returns>
public PathNode GetNode(string nodeId)
{
return _nodeMap.ContainsKey(nodeId) ? _nodeMap[nodeId] : null;
}
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.Drawing;
namespace AGVNavigationCore.PathFinding
{
/// <summary>
/// A* 알고리즘에서 사용하는 경로 노드
/// </summary>
public class PathNode
{
/// <summary>
/// 노드 ID
/// </summary>
public string NodeId { get; set; }
/// <summary>
/// 노드 위치
/// </summary>
public Point Position { get; set; }
/// <summary>
/// 시작점으로부터의 실제 거리 (G cost)
/// </summary>
public float GCost { get; set; }
/// <summary>
/// 목적지까지의 추정 거리 (H cost - 휴리스틱)
/// </summary>
public float HCost { get; set; }
/// <summary>
/// 총 비용 (F cost = G cost + H cost)
/// </summary>
public float FCost => GCost + HCost;
/// <summary>
/// 부모 노드 (경로 추적용)
/// </summary>
public PathNode Parent { get; set; }
/// <summary>
/// 연결된 노드 ID 목록
/// </summary>
public System.Collections.Generic.List<string> ConnectedNodes { get; set; }
/// <summary>
/// 생성자
/// </summary>
/// <param name="nodeId">노드 ID</param>
/// <param name="position">위치</param>
public PathNode(string nodeId, Point position)
{
NodeId = nodeId;
Position = position;
GCost = 0;
HCost = 0;
Parent = null;
ConnectedNodes = new System.Collections.Generic.List<string>();
}
/// <summary>
/// 다른 노드까지의 유클리드 거리 계산
/// </summary>
/// <param name="other">대상 노드</param>
/// <returns>거리</returns>
public float DistanceTo(PathNode other)
{
float dx = Position.X - other.Position.X;
float dy = Position.Y - other.Position.Y;
return (float)Math.Sqrt(dx * dx + dy * dy);
}
/// <summary>
/// 문자열 표현
/// </summary>
public override string ToString()
{
return $"{NodeId} - F:{FCost:F1} G:{GCost:F1} H:{HCost:F1}";
}
/// <summary>
/// 같음 비교 (NodeId 기준)
/// </summary>
public override bool Equals(object obj)
{
if (obj is PathNode other)
{
return NodeId == other.NodeId;
}
return false;
}
/// <summary>
/// 해시코드 (NodeId 기준)
/// </summary>
public override int GetHashCode()
{
return NodeId?.GetHashCode() ?? 0;
}
}
}

View File

@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
namespace AGVNavigationCore.PathFinding
{
/// <summary>
/// 경로 계산 결과
/// </summary>
public class PathResult
{
/// <summary>
/// 경로 찾기 성공 여부
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 경로 노드 ID 목록 (시작 → 목적지 순서)
/// </summary>
public List<string> Path { get; set; }
/// <summary>
/// 총 거리
/// </summary>
public float TotalDistance { get; set; }
/// <summary>
/// 계산 소요 시간 (밀리초)
/// </summary>
public long CalculationTimeMs { get; set; }
/// <summary>
/// 탐색한 노드 수
/// </summary>
public int ExploredNodeCount { get; set; }
/// <summary>
/// 오류 메시지 (실패시)
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// 기본 생성자
/// </summary>
public PathResult()
{
Success = false;
Path = new List<string>();
TotalDistance = 0;
CalculationTimeMs = 0;
ExploredNodeCount = 0;
ErrorMessage = string.Empty;
}
/// <summary>
/// 성공 결과 생성
/// </summary>
/// <param name="path">경로</param>
/// <param name="totalDistance">총 거리</param>
/// <param name="calculationTimeMs">계산 시간</param>
/// <param name="exploredNodeCount">탐색 노드 수</param>
/// <returns>성공 결과</returns>
public static PathResult CreateSuccess(List<string> path, float totalDistance, long calculationTimeMs, int exploredNodeCount)
{
return new PathResult
{
Success = true,
Path = new List<string>(path),
TotalDistance = totalDistance,
CalculationTimeMs = calculationTimeMs,
ExploredNodeCount = exploredNodeCount
};
}
/// <summary>
/// 실패 결과 생성
/// </summary>
/// <param name="errorMessage">오류 메시지</param>
/// <param name="calculationTimeMs">계산 시간</param>
/// <param name="exploredNodeCount">탐색 노드 수</param>
/// <returns>실패 결과</returns>
public static PathResult CreateFailure(string errorMessage, long calculationTimeMs, int exploredNodeCount)
{
return new PathResult
{
Success = false,
ErrorMessage = errorMessage,
CalculationTimeMs = calculationTimeMs,
ExploredNodeCount = exploredNodeCount
};
}
/// <summary>
/// 문자열 표현
/// </summary>
public override string ToString()
{
if (Success)
{
return $"Success: {Path.Count} nodes, {TotalDistance:F1}px, {CalculationTimeMs}ms";
}
else
{
return $"Failed: {ErrorMessage}, {CalculationTimeMs}ms";
}
}
}
}

View File

@@ -0,0 +1,275 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AGVNavigationCore.Models;
namespace AGVNavigationCore.PathFinding
{
/// <summary>
/// RFID 기반 AGV 경로 탐색기
/// 실제 현장에서 AGV가 RFID를 읽어서 위치를 파악하는 방식에 맞춤
/// </summary>
public class RfidBasedPathfinder
{
private AGVPathfinder _agvPathfinder;
private AStarPathfinder _astarPathfinder;
private Dictionary<string, string> _rfidToNodeMap; // RFID -> NodeId
private Dictionary<string, string> _nodeToRfidMap; // NodeId -> RFID
private List<MapNode> _mapNodes;
/// <summary>
/// AGV 현재 방향
/// </summary>
public AgvDirection CurrentDirection
{
get => _agvPathfinder.CurrentDirection;
set => _agvPathfinder.CurrentDirection = value;
}
/// <summary>
/// 회전 비용 가중치
/// </summary>
public float RotationCostWeight
{
get => _agvPathfinder.RotationCostWeight;
set => _agvPathfinder.RotationCostWeight = value;
}
/// <summary>
/// 생성자
/// </summary>
public RfidBasedPathfinder()
{
_agvPathfinder = new AGVPathfinder();
_astarPathfinder = new AStarPathfinder();
_rfidToNodeMap = new Dictionary<string, string>();
_nodeToRfidMap = new Dictionary<string, string>();
_mapNodes = new List<MapNode>();
}
/// <summary>
/// 맵 노드 설정 (MapNode의 RFID 정보 직접 사용)
/// </summary>
/// <param name="mapNodes">맵 노드 목록</param>
public void SetMapNodes(List<MapNode> mapNodes)
{
// 기존 pathfinder에 맵 노드 설정
_agvPathfinder.SetMapNodes(mapNodes);
_astarPathfinder.SetMapNodes(mapNodes);
// MapNode의 RFID 정보로 매핑 구성
_mapNodes = mapNodes ?? new List<MapNode>();
_rfidToNodeMap.Clear();
_nodeToRfidMap.Clear();
foreach (var node in _mapNodes.Where(n => n.IsActive && n.HasRfid()))
{
_rfidToNodeMap[node.RfidId] = node.NodeId;
_nodeToRfidMap[node.NodeId] = node.RfidId;
}
}
/// <summary>
/// RFID 기반 AGV 경로 계산
/// </summary>
/// <param name="startRfidId">시작 RFID</param>
/// <param name="endRfidId">목적지 RFID</param>
/// <param name="targetDirection">목적지 도착 방향</param>
/// <returns>RFID 기반 AGV 경로 계산 결과</returns>
public RfidPathResult FindAGVPath(string startRfidId, string endRfidId, AgvDirection? targetDirection = null)
{
try
{
// RFID를 NodeId로 변환
if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
{
return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
}
if (!_rfidToNodeMap.TryGetValue(endRfidId, out string endNodeId))
{
return RfidPathResult.CreateFailure($"목적지 RFID를 찾을 수 없습니다: {endRfidId}", 0);
}
// NodeId 기반으로 경로 계산
var nodeResult = _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection);
// 결과를 RFID 기반으로 변환
return ConvertToRfidResult(nodeResult, startRfidId, endRfidId);
}
catch (Exception ex)
{
return RfidPathResult.CreateFailure($"RFID 기반 경로 계산 중 오류: {ex.Message}", 0);
}
}
/// <summary>
/// 가장 가까운 충전소로의 RFID 기반 경로 찾기
/// </summary>
/// <param name="startRfidId">시작 RFID</param>
/// <returns>RFID 기반 경로 계산 결과</returns>
public RfidPathResult FindPathToChargingStation(string startRfidId)
{
try
{
if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
{
return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
}
var nodeResult = _agvPathfinder.FindPathToChargingStation(startNodeId);
return ConvertToRfidResult(nodeResult, startRfidId, null);
}
catch (Exception ex)
{
return RfidPathResult.CreateFailure($"충전소 경로 계산 중 오류: {ex.Message}", 0);
}
}
/// <summary>
/// 특정 장비 타입의 도킹 스테이션으로의 RFID 기반 경로 찾기
/// </summary>
/// <param name="startRfidId">시작 RFID</param>
/// <param name="stationType">장비 타입</param>
/// <returns>RFID 기반 경로 계산 결과</returns>
public RfidPathResult FindPathToDockingStation(string startRfidId, StationType stationType)
{
try
{
if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
{
return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
}
var nodeResult = _agvPathfinder.FindPathToDockingStation(startNodeId, stationType);
return ConvertToRfidResult(nodeResult, startRfidId, null);
}
catch (Exception ex)
{
return RfidPathResult.CreateFailure($"도킹 스테이션 경로 계산 중 오류: {ex.Message}", 0);
}
}
/// <summary>
/// 여러 RFID 목적지 중 가장 가까운 곳으로의 경로 찾기
/// </summary>
/// <param name="startRfidId">시작 RFID</param>
/// <param name="targetRfidIds">목적지 후보 RFID 목록</param>
/// <returns>RFID 기반 경로 계산 결과</returns>
public RfidPathResult FindNearestPath(string startRfidId, List<string> targetRfidIds)
{
try
{
if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
{
return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
}
// RFID 목록을 NodeId 목록으로 변환
var targetNodeIds = new List<string>();
foreach (var rfidId in targetRfidIds)
{
if (_rfidToNodeMap.TryGetValue(rfidId, out string nodeId))
{
targetNodeIds.Add(nodeId);
}
}
if (targetNodeIds.Count == 0)
{
return RfidPathResult.CreateFailure("유효한 목적지 RFID가 없습니다", 0);
}
var pathResult = _astarPathfinder.FindNearestPath(startNodeId, targetNodeIds);
if (!pathResult.Success)
{
return RfidPathResult.CreateFailure(pathResult.ErrorMessage, pathResult.CalculationTimeMs);
}
// AGV 명령어 생성을 위해 AGV pathfinder 사용
var endNodeId = pathResult.Path.Last();
var agvResult = _agvPathfinder.FindAGVPath(startNodeId, endNodeId);
return ConvertToRfidResult(agvResult, startRfidId, null);
}
catch (Exception ex)
{
return RfidPathResult.CreateFailure($"최근접 경로 계산 중 오류: {ex.Message}", 0);
}
}
/// <summary>
/// RFID 매핑 상태 확인 (MapNode 기반)
/// </summary>
/// <param name="rfidId">확인할 RFID</param>
/// <returns>MapNode 또는 null</returns>
public MapNode GetRfidMapping(string rfidId)
{
return _mapNodes.FirstOrDefault(n => n.RfidId == rfidId && n.IsActive && n.HasRfid());
}
/// <summary>
/// RFID로 NodeId 조회
/// </summary>
/// <param name="rfidId">RFID</param>
/// <returns>NodeId 또는 null</returns>
public string GetNodeIdByRfid(string rfidId)
{
return _rfidToNodeMap.TryGetValue(rfidId, out string nodeId) ? nodeId : null;
}
/// <summary>
/// NodeId로 RFID 조회
/// </summary>
/// <param name="nodeId">NodeId</param>
/// <returns>RFID 또는 null</returns>
public string GetRfidByNodeId(string nodeId)
{
return _nodeToRfidMap.TryGetValue(nodeId, out string rfidId) ? rfidId : null;
}
/// <summary>
/// 활성화된 RFID 목록 반환
/// </summary>
/// <returns>활성화된 RFID 목록</returns>
public List<string> GetActiveRfidList()
{
return _mapNodes.Where(n => n.IsActive && n.HasRfid()).Select(n => n.RfidId).ToList();
}
/// <summary>
/// NodeId 기반 결과를 RFID 기반 결과로 변환
/// </summary>
private RfidPathResult ConvertToRfidResult(AGVPathResult nodeResult, string startRfidId, string endRfidId)
{
if (!nodeResult.Success)
{
return RfidPathResult.CreateFailure(nodeResult.ErrorMessage, nodeResult.CalculationTimeMs);
}
// NodeId 경로를 RFID 경로로 변환
var rfidPath = new List<string>();
foreach (var nodeId in nodeResult.Path)
{
if (_nodeToRfidMap.TryGetValue(nodeId, out string rfidId))
{
rfidPath.Add(rfidId);
}
else
{
// 매핑이 없는 경우 NodeId를 그대로 사용 (경고 로그 필요)
rfidPath.Add($"[{nodeId}]");
}
}
return RfidPathResult.CreateSuccess(
rfidPath,
nodeResult.Commands,
nodeResult.TotalDistance,
nodeResult.CalculationTimeMs,
nodeResult.EstimatedTimeSeconds,
nodeResult.RotationCount
);
}
}
}

View File

@@ -0,0 +1,229 @@
using System;
using System.Collections.Generic;
using AGVNavigationCore.Models;
namespace AGVNavigationCore.PathFinding
{
/// <summary>
/// RFID 기반 AGV 경로 계산 결과
/// 실제 현장에서 AGV가 RFID를 기준으로 이동하는 방식에 맞춤
/// </summary>
public class RfidPathResult
{
/// <summary>
/// 경로 찾기 성공 여부
/// </summary>
public bool Success { get; set; }
/// <summary>
/// RFID 경로 목록 (시작 → 목적지 순서)
/// </summary>
public List<string> RfidPath { get; set; }
/// <summary>
/// AGV 명령어 목록 (이동 방향 시퀀스)
/// </summary>
public List<AgvDirection> Commands { get; set; }
/// <summary>
/// 총 거리
/// </summary>
public float TotalDistance { get; set; }
/// <summary>
/// 계산 소요 시간 (밀리초)
/// </summary>
public long CalculationTimeMs { get; set; }
/// <summary>
/// 예상 소요 시간 (초)
/// </summary>
public float EstimatedTimeSeconds { get; set; }
/// <summary>
/// 회전 횟수
/// </summary>
public int RotationCount { get; set; }
/// <summary>
/// 오류 메시지 (실패시)
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// 기본 생성자
/// </summary>
public RfidPathResult()
{
Success = false;
RfidPath = new List<string>();
Commands = new List<AgvDirection>();
TotalDistance = 0;
CalculationTimeMs = 0;
EstimatedTimeSeconds = 0;
RotationCount = 0;
ErrorMessage = string.Empty;
}
/// <summary>
/// 성공 결과 생성
/// </summary>
/// <param name="rfidPath">RFID 경로</param>
/// <param name="commands">AGV 명령어 목록</param>
/// <param name="totalDistance">총 거리</param>
/// <param name="calculationTimeMs">계산 시간</param>
/// <param name="estimatedTimeSeconds">예상 소요 시간</param>
/// <param name="rotationCount">회전 횟수</param>
/// <returns>성공 결과</returns>
public static RfidPathResult CreateSuccess(
List<string> rfidPath,
List<AgvDirection> commands,
float totalDistance,
long calculationTimeMs,
float estimatedTimeSeconds,
int rotationCount)
{
return new RfidPathResult
{
Success = true,
RfidPath = new List<string>(rfidPath),
Commands = new List<AgvDirection>(commands),
TotalDistance = totalDistance,
CalculationTimeMs = calculationTimeMs,
EstimatedTimeSeconds = estimatedTimeSeconds,
RotationCount = rotationCount
};
}
/// <summary>
/// 실패 결과 생성
/// </summary>
/// <param name="errorMessage">오류 메시지</param>
/// <param name="calculationTimeMs">계산 시간</param>
/// <returns>실패 결과</returns>
public static RfidPathResult CreateFailure(string errorMessage, long calculationTimeMs)
{
return new RfidPathResult
{
Success = false,
ErrorMessage = errorMessage,
CalculationTimeMs = calculationTimeMs
};
}
/// <summary>
/// 명령어 요약 생성
/// </summary>
/// <returns>명령어 요약 문자열</returns>
public string GetCommandSummary()
{
if (!Success) return "실패";
var summary = new List<string>();
var currentCommand = AgvDirection.Stop;
var count = 0;
foreach (var command in Commands)
{
if (command == currentCommand)
{
count++;
}
else
{
if (count > 0)
{
summary.Add($"{GetCommandText(currentCommand)}×{count}");
}
currentCommand = command;
count = 1;
}
}
if (count > 0)
{
summary.Add($"{GetCommandText(currentCommand)}×{count}");
}
return string.Join(" → ", summary);
}
/// <summary>
/// 명령어 텍스트 반환
/// </summary>
private string GetCommandText(AgvDirection command)
{
switch (command)
{
case AgvDirection.Forward: return "전진";
case AgvDirection.Backward: return "후진";
case AgvDirection.Left: return "좌회전";
case AgvDirection.Right: return "우회전";
case AgvDirection.Stop: return "정지";
default: return command.ToString();
}
}
/// <summary>
/// RFID 경로 요약 생성
/// </summary>
/// <returns>RFID 경로 요약 문자열</returns>
public string GetRfidPathSummary()
{
if (!Success || RfidPath.Count == 0) return "경로 없음";
if (RfidPath.Count <= 3)
{
return string.Join(" → ", RfidPath);
}
else
{
return $"{RfidPath[0]} → ... ({RfidPath.Count - 2}개 경유) → {RfidPath[RfidPath.Count - 1]}";
}
}
/// <summary>
/// 상세 경로 정보 반환
/// </summary>
/// <returns>상세 정보 문자열</returns>
public string GetDetailedInfo()
{
if (!Success)
{
return $"RFID 경로 계산 실패: {ErrorMessage} (계산시간: {CalculationTimeMs}ms)";
}
return $"RFID 경로: {RfidPath.Count}개 지점, 거리: {TotalDistance:F1}px, " +
$"회전: {RotationCount}회, 예상시간: {EstimatedTimeSeconds:F1}초, " +
$"계산시간: {CalculationTimeMs}ms";
}
/// <summary>
/// AGV 운영자용 실행 정보 반환
/// </summary>
/// <returns>실행 정보 문자열</returns>
public string GetExecutionInfo()
{
if (!Success) return $"실행 불가: {ErrorMessage}";
return $"[실행준비] {GetRfidPathSummary()}\n" +
$"[명령어] {GetCommandSummary()}\n" +
$"[예상시간] {EstimatedTimeSeconds:F1}초";
}
/// <summary>
/// 문자열 표현
/// </summary>
public override string ToString()
{
if (Success)
{
return $"Success: {RfidPath.Count} RFIDs, {TotalDistance:F1}px, {RotationCount} rotations, {EstimatedTimeSeconds:F1}s";
}
else
{
return $"Failed: {ErrorMessage}";
}
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("AGVNavigationCore")]
[assembly: AssemblyDescription("AGV Navigation and Pathfinding Core Library")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("ENIG")]
[assembly: AssemblyProduct("AGV Navigation System")]
[assembly: AssemblyCopyright("Copyright © ENIG 2024")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("c5f7a8b2-8d3e-4a1b-9c6e-7f4d5e2a9b1c")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -0,0 +1,225 @@
# AGVNavigationCore 프로젝트 기능 설명
## 📋 개요
AGVNavigationCore는 AGV(Automated Guided Vehicle) 시스템을 위한 전문적인 경로 계산 및 네비게이션 라이브러리입니다. 실제 AGV의 물리적 제약사항과 운영 요구사항을 고려한 지능형 경로 탐색 기능을 제공합니다.
## 🏗️ 핵심 구조
### **Models 패키지**
- **MapNode**: 맵의 논리적 노드 정보 (위치, 타입, 연결 정보, RFID 매핑)
- **RfidMapping**: RFID와 논리적 노드 ID 간의 매핑 정보
- **Enums**: 노드 타입, AGV 방향, 도킹 방향, 장비 타입 등 열거형 정의
### **PathFinding 패키지**
경로 계산의 핵심 엔진들이 포함된 패키지
## 🎯 주요 기능
### 1. **기본 경로 탐색 (A* 알고리즘)**
**AStarPathfinder 클래스**:
- 표준 A* 알고리즘 구현으로 최적 경로 탐색
- 유클리드 거리 기반 휴리스틱 사용
- 설정 가능한 파라미터:
- `HeuristicWeight`: 휴리스틱 가중치 (기본 1.0)
- `MaxSearchNodes`: 최대 탐색 노드 수 (기본 1000개)
**제공 기능**:
```csharp
// 단일 경로 탐색
PathResult FindPath(string startNodeId, string endNodeId)
// 다중 목표 중 최단 경로 탐색
PathResult FindNearestPath(string startNodeId, List<string> targetNodeIds)
// 노드 연결 상태 확인
bool AreNodesConnected(string nodeId1, string nodeId2)
```
### 2. **AGV 전용 지능형 경로 계산**
**AGVPathfinder 클래스**:
AGV의 실제 움직임 제약사항을 고려한 전문 경로 계산기
**AGV 제약사항 고려**:
- **방향성 제약**: 전진/후진만 가능, 좌우 이동 불가
- **회전 제약**: 특정 노드(회전 가능 지점)에서만 180도 회전 가능
- **도킹 방향**:
- 충전기: 전진 도킹만 가능
- 장비 (로더, 클리너 등): 후진 도킹만 가능
**전용 기능**:
```csharp
// AGV 경로 계산 (방향성 고려)
AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection)
// 가장 가까운 충전소 경로
AGVPathResult FindPathToChargingStation(string startNodeId)
// 특정 장비 타입 도킹 스테이션 경로
AGVPathResult FindPathToDockingStation(string startNodeId, StationType stationType)
```
### 3. **RFID 기반 경로 계산 (실제 운영용)**
**RfidBasedPathfinder 클래스**:
실제 AGV가 RFID를 읽어서 위치를 파악하는 현장 운영 방식에 최적화
**RFID 기반 제약사항**:
- **물리적 RFID**: 의미 없는 고유 식별자 (현장 유지보수 편의성)
- **논리적 매핑**: RFID ↔ NodeId 분리로 맵 변경 시 유연성 확보
- **실시간 변환**: RFID 입력을 내부적으로 NodeId로 변환하여 처리
**RFID 전용 기능**:
```csharp
// RFID 기반 AGV 경로 계산
RfidPathResult FindAGVPath(string startRfidId, string endRfidId, AgvDirection? targetDirection)
// RFID 기반 충전소 경로
RfidPathResult FindPathToChargingStation(string startRfidId)
// RFID 기반 도킹 스테이션 경로
RfidPathResult FindPathToDockingStation(string startRfidId, StationType stationType)
// RFID 매핑 관리
RfidMapping GetRfidMapping(string rfidId)
string GetNodeIdByRfid(string rfidId)
string GetRfidByNodeId(string nodeId)
```
### 4. **상세한 결과 분석**
**PathResult (기본 결과)**:
- 성공/실패 여부
- 노드 ID 시퀀스
- 총 거리 및 계산 시간
- 탐색한 노드 수
**AGVPathResult (AGV 전용 결과)**:
- **실행 가능한 명령어 시퀀스**: `[전진, 전진, 우회전, 후진, 정지]`
- **상세 메트릭**:
- 회전 횟수 계산
- 예상 소요 시간 (이동 + 회전 시간)
- 명령어 요약 (`전진×3 → 우회전×1 → 후진×2`)
**RfidPathResult (RFID 기반 결과)**:
- **RFID 경로 시퀀스**: `[RFID001, RFID045, RFID067, RFID123]`
- **AGV 명령어**: NodeId 기반 결과와 동일한 명령어 시퀀스
- **현장 친화적 정보**:
- RFID 경로 요약 (`RFID001 → ... (2개 경유) → RFID123`)
- 실행 정보 (`[실행준비] → [명령어] → [예상시간]`)
- 운영자용 상세 정보
### 5. **실시간 검증 및 최적화**
**경로 검증**:
```csharp
// 경로 유효성 실시간 검증
bool ValidatePath(List<string> path)
// 네비게이션 가능 노드 필터링 (라벨/이미지 노드 제외)
List<string> GetNavigationNodes()
```
**성능 최적화**:
- 메모리 효율적인 노드 관리
- 조기 종료 조건으로 불필요한 탐색 방지
- 캐시된 거리 계산
## 🔧 설정 가능한 파라미터
**AGV 동작 파라미터**:
- `CurrentDirection`: AGV 현재 방향
- `RotationCostWeight`: 회전 비용 가중치 (기본 50.0)
- `DockingApproachDistance`: 도킹 접근 거리 (기본 100픽셀)
**알고리즘 파라미터**:
- `HeuristicWeight`: A* 휴리스틱 강도
- `MaxSearchNodes`: 탐색 제한으로 무한루프 방지
**RFID 매핑 파라미터**:
- `RfidMappings`: RFID ↔ NodeId 매핑 테이블
- `IsActive`: 매핑 활성화 상태
- `Status`: RFID 상태 (정상, 손상, 교체예정)
## 🎯 실제 활용 시나리오
### **시나리오 1: 일반 이동 (NodeId 기반)**
```
현재위치(N001) → 목적지(N010)
결과: [N001, N003, N007, N010] + [전진, 전진, 전진]
```
### **시나리오 2: 일반 이동 (RFID 기반)**
```
현재위치(RFID123) → 목적지(RFID789)
결과: [RFID123, RFID456, RFID789] + [전진, 우회전, 전진]
AGV는 RFID를 읽으면서 실제 위치 확인 후 이동
```
### **시나리오 3: 충전 필요**
```
배터리 부족 → 가장 가까운 충전소 자동 탐색
결과: 충전소까지 최단경로 + 전진 도킹 명령어
```
### **시나리오 4: 화물 적재**
```
로더 스테이션 접근 → 후진 도킹 필수
결과: 로더까지 경로 + 후진 도킹 시퀀스
```
## 🌟 차별화 포인트
1. **실제 AGV 제약사항 반영**: 이론적 경로가 아닌 실행 가능한 경로 제공
2. **명령어 레벨 출력**: 경로뿐만 아니라 실제 AGV 제어 명령어 생성
3. **RFID 기반 현장 운영**: 물리적 RFID와 논리적 노드 분리로 현장 유지보수성 향상
4. **다양한 장비 지원**: 충전기, 로더, 클리너, 버퍼 등 각각의 도킹 요구사항 처리
5. **이중 API 제공**: NodeId 기반(개발용) + RFID 기반(운영용) 동시 지원
6. **확장성**: 새로운 AGV 타입이나 제약사항 쉽게 추가 가능
7. **성능 최적화**: 실시간 운영에 적합한 빠른 응답속도
## 🚀 사용 방법
### 기본 사용법
```csharp
// 1. 경로 탐색기 초기화
var pathfinder = new AGVPathfinder();
pathfinder.SetMapNodes(mapNodes);
// 2. AGV 경로 계산
var result = pathfinder.FindAGVPath("N001", "N010");
// 3. 결과 확인
if (result.Success)
{
Console.WriteLine($"경로: {string.Join(" ", result.Path)}");
Console.WriteLine($"명령어: {result.GetCommandSummary()}");
Console.WriteLine($"예상시간: {result.EstimatedTimeSeconds}초");
}
```
### 충전소 경로 탐색
```csharp
var chargingResult = pathfinder.FindPathToChargingStation("N001");
if (chargingResult.Success)
{
// 충전소까지 자동 이동
ExecuteAGVCommands(chargingResult.Commands);
}
```
## 📦 의존성
- .NET Framework 4.8
- Newtonsoft.Json 13.0.3
- System.Drawing
## 🔗 통합 프로젝트
이 라이브러리는 다음 프로젝트에서 사용됩니다:
- **AGVMapEditor**: 맵 편집 및 경로 시뮬레이션
- **AGV4**: 메인 AGV 제어 시스템
- **AGVSimulator**: AGV 동작 시뮬레이터
---
*AGVNavigationCore는 ENIG AGV 시스템의 핵심 네비게이션 엔진입니다.*

View File

@@ -0,0 +1,29 @@
@echo off
echo Building V2GDecoder VC++ Project...
REM Check if Visual Studio 2022 is installed (Professional or Community)
set MSBUILD_PRO="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
set MSBUILD_COM="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
set MSBUILD_BT="F:\(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\MSBuild.exe"
if exist %MSBUILD_PRO% (
echo "Found Visual Studio 2022 Professional"
set MSBUILD=%MSBUILD_PRO%
) else if exist %MSBUILD_COM% (
echo "Found Visual Studio 2022 Community"
set MSBUILD=%MSBUILD_COM%
) else if exist %MSBUILD_BT% (
echo "Found Visual Studio 2022 BuildTools"
set MSBUILD=%MSBUILD_BT%
) else (
echo "Visual Studio 2022 (Professional or Community) not found!"
echo "Please install Visual Studio 2022 or update the MSBuild path."
pause
exit /b 1
)
REM Build Debug x64 configuration
echo Building Debug x64 configuration...
%MSBUILD% AGVNavigationCore.csproj
pause

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
</packages>

View File

@@ -4,7 +4,7 @@
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{B2C3D4E5-F6G7-8901-BCDE-F23456789012}</ProjectGuid> <ProjectGuid>{B2C3D4E5-0000-0000-0000-000000000000}</ProjectGuid>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<RootNamespace>AGVSimulator</RootNamespace> <RootNamespace>AGVSimulator</RootNamespace>
<AssemblyName>AGVSimulator</AssemblyName> <AssemblyName>AGVSimulator</AssemblyName>
@@ -42,14 +42,9 @@
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Models\SimulatorConfig.cs" />
<Compile Include="Models\VirtualAGV.cs" /> <Compile Include="Models\VirtualAGV.cs" />
<Compile Include="Models\SimulationState.cs" /> <Compile Include="Models\SimulationState.cs" />
<Compile Include="Controls\SimulatorCanvas.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Controls\SimulatorCanvas.Designer.cs">
<DependentUpon>SimulatorCanvas.cs</DependentUpon>
</Compile>
<Compile Include="Forms\SimulatorForm.cs"> <Compile Include="Forms\SimulatorForm.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
@@ -60,17 +55,19 @@
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Controls\SimulatorCanvas.resx">
<DependentUpon>SimulatorCanvas.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Forms\SimulatorForm.resx"> <EmbeddedResource Include="Forms\SimulatorForm.resx">
<DependentUpon>SimulatorForm.cs</DependentUpon> <DependentUpon>SimulatorForm.cs</DependentUpon>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="build.bat" />
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\AGVNavigationCore\AGVNavigationCore.csproj">
<Project>{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}</Project>
<Name>AGVNavigationCore</Name>
</ProjectReference>
<ProjectReference Include="..\AGVMapEditor\AGVMapEditor.csproj"> <ProjectReference Include="..\AGVMapEditor\AGVMapEditor.csproj">
<Project>{a1b2c3d4-e5f6-7890-abcd-ef1234567890}</Project> <Project>{a1b2c3d4-e5f6-7890-abcd-ef1234567890}</Project>
<Name>AGVMapEditor</Name> <Name>AGVMapEditor</Name>

View File

@@ -4,6 +4,8 @@ using System.Drawing;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using AGVMapEditor.Models; using AGVMapEditor.Models;
using AGVNavigationCore.Models;
using AGVNavigationCore.PathFinding;
using AGVSimulator.Models; using AGVSimulator.Models;
namespace AGVSimulator.Controls namespace AGVSimulator.Controls
@@ -388,13 +390,13 @@ namespace AGVSimulator.Controls
private void DrawPath(Graphics g) private void DrawPath(Graphics g)
{ {
if (_currentPath?.NodeSequence == null || _currentPath.NodeSequence.Count < 2) if (_currentPath?.Path == null || _currentPath.Path.Count < 2)
return; return;
for (int i = 0; i < _currentPath.NodeSequence.Count - 1; i++) for (int i = 0; i < _currentPath.Path.Count - 1; i++)
{ {
var currentNodeId = _currentPath.NodeSequence[i]; var currentNodeId = _currentPath.Path[i];
var nextNodeId = _currentPath.NodeSequence[i + 1]; var nextNodeId = _currentPath.Path[i + 1];
var currentNode = _mapNodes?.FirstOrDefault(n => n.NodeId == currentNodeId); var currentNode = _mapNodes?.FirstOrDefault(n => n.NodeId == currentNodeId);
var nextNode = _mapNodes?.FirstOrDefault(n => n.NodeId == nextNodeId); var nextNode = _mapNodes?.FirstOrDefault(n => n.NodeId == nextNodeId);
@@ -583,11 +585,11 @@ namespace AGVSimulator.Controls
if (_currentPath != null && _currentPath.Success) if (_currentPath != null && _currentPath.Success)
{ {
y += 10; y += 10;
g.DrawString($"경로: {_currentPath.NodeSequence.Count}개 노드", font, brush, new PointF(10, y)); g.DrawString($"경로: {_currentPath.Path.Count}개 노드", font, brush, new PointF(10, y));
y += 15; y += 15;
g.DrawString($"거리: {_currentPath.TotalDistance:F1}", font, brush, new PointF(10, y)); g.DrawString($"거리: {_currentPath.TotalDistance:F1}", font, brush, new PointF(10, y));
y += 15; y += 15;
g.DrawString($"계산시간: {_currentPath.CalculationTime}ms", font, brush, new PointF(10, y)); g.DrawString($"계산시간: {_currentPath.CalculationTimeMs}ms", font, brush, new PointF(10, y));
} }
font.Dispose(); font.Dispose();

View File

@@ -45,20 +45,666 @@ namespace AGVSimulator.Forms
/// </summary> /// </summary>
private void InitializeComponent() private void InitializeComponent()
{ {
this._menuStrip = new System.Windows.Forms.MenuStrip();
this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.openMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.reloadMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.launchMapEditorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator();
this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.simulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.startSimulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.stopSimulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.resetToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.viewToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.fitToMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.resetZoomToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.helpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this._toolStrip = new System.Windows.Forms.ToolStrip();
this.openMapToolStripButton = new System.Windows.Forms.ToolStripButton();
this.reloadMapToolStripButton = new System.Windows.Forms.ToolStripButton();
this.launchMapEditorToolStripButton = new System.Windows.Forms.ToolStripButton();
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
this.startSimulationToolStripButton = new System.Windows.Forms.ToolStripButton();
this.stopSimulationToolStripButton = new System.Windows.Forms.ToolStripButton();
this.resetToolStripButton = new System.Windows.Forms.ToolStripButton();
this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
this.fitToMapToolStripButton = new System.Windows.Forms.ToolStripButton();
this.resetZoomToolStripButton = new System.Windows.Forms.ToolStripButton();
this._statusStrip = new System.Windows.Forms.StatusStrip();
this._statusLabel = new System.Windows.Forms.ToolStripStatusLabel();
this._coordLabel = new System.Windows.Forms.ToolStripStatusLabel();
this._controlPanel = new System.Windows.Forms.Panel();
this._statusGroup = new System.Windows.Forms.GroupBox();
this._pathLengthLabel = new System.Windows.Forms.Label();
this._agvCountLabel = new System.Windows.Forms.Label();
this._simulationStatusLabel = new System.Windows.Forms.Label();
this._pathGroup = new System.Windows.Forms.GroupBox();
this._clearPathButton = new System.Windows.Forms.Button();
this._startPathButton = new System.Windows.Forms.Button();
this._calculatePathButton = new System.Windows.Forms.Button();
this._targetNodeCombo = new System.Windows.Forms.ComboBox();
this.targetNodeLabel = new System.Windows.Forms.Label();
this._startNodeCombo = new System.Windows.Forms.ComboBox();
this.startNodeLabel = new System.Windows.Forms.Label();
this._agvControlGroup = new System.Windows.Forms.GroupBox();
this._setPositionButton = new System.Windows.Forms.Button();
this._rfidTextBox = new System.Windows.Forms.TextBox();
this._rfidLabel = new System.Windows.Forms.Label();
this._stopSimulationButton = new System.Windows.Forms.Button();
this._startSimulationButton = new System.Windows.Forms.Button();
this._removeAgvButton = new System.Windows.Forms.Button();
this._addAgvButton = new System.Windows.Forms.Button();
this._agvListCombo = new System.Windows.Forms.ComboBox();
this._canvasPanel = new System.Windows.Forms.Panel();
this.btAllReset = new System.Windows.Forms.ToolStripButton();
this._menuStrip.SuspendLayout();
this._toolStrip.SuspendLayout();
this._statusStrip.SuspendLayout();
this._controlPanel.SuspendLayout();
this._statusGroup.SuspendLayout();
this._pathGroup.SuspendLayout();
this._agvControlGroup.SuspendLayout();
this.SuspendLayout(); this.SuspendLayout();
// //
// _menuStrip
//
this._menuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.fileToolStripMenuItem,
this.simulationToolStripMenuItem,
this.viewToolStripMenuItem,
this.helpToolStripMenuItem});
this._menuStrip.Location = new System.Drawing.Point(0, 0);
this._menuStrip.Name = "_menuStrip";
this._menuStrip.Size = new System.Drawing.Size(1200, 24);
this._menuStrip.TabIndex = 0;
this._menuStrip.Text = "menuStrip";
//
// fileToolStripMenuItem
//
this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.openMapToolStripMenuItem,
this.reloadMapToolStripMenuItem,
this.toolStripSeparator1,
this.launchMapEditorToolStripMenuItem,
this.toolStripSeparator4,
this.exitToolStripMenuItem});
this.fileToolStripMenuItem.Name = "fileToolStripMenuItem";
this.fileToolStripMenuItem.Size = new System.Drawing.Size(57, 20);
this.fileToolStripMenuItem.Text = "파일(&F)";
//
// openMapToolStripMenuItem
//
this.openMapToolStripMenuItem.Name = "openMapToolStripMenuItem";
this.openMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O)));
this.openMapToolStripMenuItem.Size = new System.Drawing.Size(183, 22);
this.openMapToolStripMenuItem.Text = "맵 열기(&O)...";
this.openMapToolStripMenuItem.Click += new System.EventHandler(this.OnOpenMap_Click);
//
// reloadMapToolStripMenuItem
//
this.reloadMapToolStripMenuItem.Name = "reloadMapToolStripMenuItem";
this.reloadMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.R)));
this.reloadMapToolStripMenuItem.Size = new System.Drawing.Size(183, 22);
this.reloadMapToolStripMenuItem.Text = "맵 다시열기(&R)";
this.reloadMapToolStripMenuItem.Click += new System.EventHandler(this.OnReloadMap_Click);
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(180, 6);
//
// launchMapEditorToolStripMenuItem
//
this.launchMapEditorToolStripMenuItem.Name = "launchMapEditorToolStripMenuItem";
this.launchMapEditorToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.M)));
this.launchMapEditorToolStripMenuItem.Size = new System.Drawing.Size(183, 22);
this.launchMapEditorToolStripMenuItem.Text = "MapEditor 실행(&M)";
this.launchMapEditorToolStripMenuItem.Click += new System.EventHandler(this.OnLaunchMapEditor_Click);
//
// toolStripSeparator4
//
this.toolStripSeparator4.Name = "toolStripSeparator4";
this.toolStripSeparator4.Size = new System.Drawing.Size(180, 6);
//
// exitToolStripMenuItem
//
this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
this.exitToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Alt | System.Windows.Forms.Keys.F4)));
this.exitToolStripMenuItem.Size = new System.Drawing.Size(183, 22);
this.exitToolStripMenuItem.Text = "종료(&X)";
this.exitToolStripMenuItem.Click += new System.EventHandler(this.OnExit_Click);
//
// simulationToolStripMenuItem
//
this.simulationToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.startSimulationToolStripMenuItem,
this.stopSimulationToolStripMenuItem,
this.resetToolStripMenuItem});
this.simulationToolStripMenuItem.Name = "simulationToolStripMenuItem";
this.simulationToolStripMenuItem.Size = new System.Drawing.Size(94, 20);
this.simulationToolStripMenuItem.Text = "시뮬레이션(&S)";
//
// startSimulationToolStripMenuItem
//
this.startSimulationToolStripMenuItem.Name = "startSimulationToolStripMenuItem";
this.startSimulationToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F5;
this.startSimulationToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
this.startSimulationToolStripMenuItem.Text = "시작(&S)";
this.startSimulationToolStripMenuItem.Click += new System.EventHandler(this.OnStartSimulation_Click);
//
// stopSimulationToolStripMenuItem
//
this.stopSimulationToolStripMenuItem.Name = "stopSimulationToolStripMenuItem";
this.stopSimulationToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F6;
this.stopSimulationToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
this.stopSimulationToolStripMenuItem.Text = "정지(&T)";
this.stopSimulationToolStripMenuItem.Click += new System.EventHandler(this.OnStopSimulation_Click);
//
// resetToolStripMenuItem
//
this.resetToolStripMenuItem.Name = "resetToolStripMenuItem";
this.resetToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F7;
this.resetToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
this.resetToolStripMenuItem.Text = "초기화(&R)";
this.resetToolStripMenuItem.Click += new System.EventHandler(this.OnReset_Click);
//
// viewToolStripMenuItem
//
this.viewToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.fitToMapToolStripMenuItem,
this.resetZoomToolStripMenuItem});
this.viewToolStripMenuItem.Name = "viewToolStripMenuItem";
this.viewToolStripMenuItem.Size = new System.Drawing.Size(59, 20);
this.viewToolStripMenuItem.Text = "보기(&V)";
//
// fitToMapToolStripMenuItem
//
this.fitToMapToolStripMenuItem.Name = "fitToMapToolStripMenuItem";
this.fitToMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.F)));
this.fitToMapToolStripMenuItem.Size = new System.Drawing.Size(182, 22);
this.fitToMapToolStripMenuItem.Text = "맵 맞춤(&F)";
this.fitToMapToolStripMenuItem.Click += new System.EventHandler(this.OnFitToMap_Click);
//
// resetZoomToolStripMenuItem
//
this.resetZoomToolStripMenuItem.Name = "resetZoomToolStripMenuItem";
this.resetZoomToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.D0)));
this.resetZoomToolStripMenuItem.Size = new System.Drawing.Size(182, 22);
this.resetZoomToolStripMenuItem.Text = "줌 초기화(&Z)";
this.resetZoomToolStripMenuItem.Click += new System.EventHandler(this.OnResetZoom_Click);
//
// helpToolStripMenuItem
//
this.helpToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.aboutToolStripMenuItem});
this.helpToolStripMenuItem.Name = "helpToolStripMenuItem";
this.helpToolStripMenuItem.Size = new System.Drawing.Size(72, 20);
this.helpToolStripMenuItem.Text = "도움말(&H)";
//
// aboutToolStripMenuItem
//
this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem";
this.aboutToolStripMenuItem.Size = new System.Drawing.Size(123, 22);
this.aboutToolStripMenuItem.Text = "정보(&A)...";
this.aboutToolStripMenuItem.Click += new System.EventHandler(this.OnAbout_Click);
//
// _toolStrip
//
this._toolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.openMapToolStripButton,
this.reloadMapToolStripButton,
this.launchMapEditorToolStripButton,
this.toolStripSeparator2,
this.startSimulationToolStripButton,
this.stopSimulationToolStripButton,
this.resetToolStripButton,
this.btAllReset,
this.toolStripSeparator3,
this.fitToMapToolStripButton,
this.resetZoomToolStripButton});
this._toolStrip.Location = new System.Drawing.Point(0, 24);
this._toolStrip.Name = "_toolStrip";
this._toolStrip.Size = new System.Drawing.Size(1200, 25);
this._toolStrip.TabIndex = 1;
this._toolStrip.Text = "toolStrip";
//
// openMapToolStripButton
//
this.openMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.openMapToolStripButton.Name = "openMapToolStripButton";
this.openMapToolStripButton.Size = new System.Drawing.Size(51, 22);
this.openMapToolStripButton.Text = "맵 열기";
this.openMapToolStripButton.ToolTipText = "맵 파일을 엽니다";
this.openMapToolStripButton.Click += new System.EventHandler(this.OnOpenMap_Click);
//
// reloadMapToolStripButton
//
this.reloadMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.reloadMapToolStripButton.Name = "reloadMapToolStripButton";
this.reloadMapToolStripButton.Size = new System.Drawing.Size(63, 22);
this.reloadMapToolStripButton.Text = "다시열기";
this.reloadMapToolStripButton.ToolTipText = "현재 맵을 다시 로드합니다";
this.reloadMapToolStripButton.Click += new System.EventHandler(this.OnReloadMap_Click);
//
// launchMapEditorToolStripButton
//
this.launchMapEditorToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.launchMapEditorToolStripButton.Name = "launchMapEditorToolStripButton";
this.launchMapEditorToolStripButton.Size = new System.Drawing.Size(71, 22);
this.launchMapEditorToolStripButton.Text = "MapEditor";
this.launchMapEditorToolStripButton.ToolTipText = "MapEditor를 실행합니다";
this.launchMapEditorToolStripButton.Click += new System.EventHandler(this.OnLaunchMapEditor_Click);
//
// toolStripSeparator2
//
this.toolStripSeparator2.Name = "toolStripSeparator2";
this.toolStripSeparator2.Size = new System.Drawing.Size(6, 25);
//
// startSimulationToolStripButton
//
this.startSimulationToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.startSimulationToolStripButton.Name = "startSimulationToolStripButton";
this.startSimulationToolStripButton.Size = new System.Drawing.Size(99, 22);
this.startSimulationToolStripButton.Text = "시뮬레이션 시작";
this.startSimulationToolStripButton.ToolTipText = "시뮬레이션을 시작합니다";
this.startSimulationToolStripButton.Click += new System.EventHandler(this.OnStartSimulation_Click);
//
// stopSimulationToolStripButton
//
this.stopSimulationToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.stopSimulationToolStripButton.Name = "stopSimulationToolStripButton";
this.stopSimulationToolStripButton.Size = new System.Drawing.Size(99, 22);
this.stopSimulationToolStripButton.Text = "시뮬레이션 정지";
this.stopSimulationToolStripButton.ToolTipText = "시뮬레이션을 정지합니다";
this.stopSimulationToolStripButton.Click += new System.EventHandler(this.OnStopSimulation_Click);
//
// resetToolStripButton
//
this.resetToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.resetToolStripButton.Name = "resetToolStripButton";
this.resetToolStripButton.Size = new System.Drawing.Size(47, 22);
this.resetToolStripButton.Text = "초기화";
this.resetToolStripButton.ToolTipText = "시뮬레이션을 초기화합니다";
this.resetToolStripButton.Click += new System.EventHandler(this.OnReset_Click);
//
// toolStripSeparator3
//
this.toolStripSeparator3.Name = "toolStripSeparator3";
this.toolStripSeparator3.Size = new System.Drawing.Size(6, 25);
//
// fitToMapToolStripButton
//
this.fitToMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.fitToMapToolStripButton.Name = "fitToMapToolStripButton";
this.fitToMapToolStripButton.Size = new System.Drawing.Size(51, 22);
this.fitToMapToolStripButton.Text = "맵 맞춤";
this.fitToMapToolStripButton.ToolTipText = "맵 전체를 화면에 맞춥니다";
this.fitToMapToolStripButton.Click += new System.EventHandler(this.OnFitToMap_Click);
//
// resetZoomToolStripButton
//
this.resetZoomToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.resetZoomToolStripButton.Name = "resetZoomToolStripButton";
this.resetZoomToolStripButton.Size = new System.Drawing.Size(63, 22);
this.resetZoomToolStripButton.Text = "줌 초기화";
this.resetZoomToolStripButton.ToolTipText = "줌을 초기화합니다";
this.resetZoomToolStripButton.Click += new System.EventHandler(this.OnResetZoom_Click);
//
// _statusStrip
//
this._statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this._statusLabel,
this._coordLabel});
this._statusStrip.Location = new System.Drawing.Point(0, 778);
this._statusStrip.Name = "_statusStrip";
this._statusStrip.Size = new System.Drawing.Size(1200, 22);
this._statusStrip.TabIndex = 2;
this._statusStrip.Text = "statusStrip";
//
// _statusLabel
//
this._statusLabel.Name = "_statusLabel";
this._statusLabel.Size = new System.Drawing.Size(31, 17);
this._statusLabel.Text = "준비";
//
// _coordLabel
//
this._coordLabel.Name = "_coordLabel";
this._coordLabel.Size = new System.Drawing.Size(0, 17);
//
// _controlPanel
//
this._controlPanel.BackColor = System.Drawing.SystemColors.Control;
this._controlPanel.Controls.Add(this._statusGroup);
this._controlPanel.Controls.Add(this._pathGroup);
this._controlPanel.Controls.Add(this._agvControlGroup);
this._controlPanel.Dock = System.Windows.Forms.DockStyle.Right;
this._controlPanel.Location = new System.Drawing.Point(950, 49);
this._controlPanel.Name = "_controlPanel";
this._controlPanel.Size = new System.Drawing.Size(250, 729);
this._controlPanel.TabIndex = 3;
//
// _statusGroup
//
this._statusGroup.Controls.Add(this._pathLengthLabel);
this._statusGroup.Controls.Add(this._agvCountLabel);
this._statusGroup.Controls.Add(this._simulationStatusLabel);
this._statusGroup.Location = new System.Drawing.Point(10, 356);
this._statusGroup.Name = "_statusGroup";
this._statusGroup.Size = new System.Drawing.Size(230, 100);
this._statusGroup.TabIndex = 3;
this._statusGroup.TabStop = false;
this._statusGroup.Text = "상태 정보";
//
// _pathLengthLabel
//
this._pathLengthLabel.AutoSize = true;
this._pathLengthLabel.Location = new System.Drawing.Point(10, 65);
this._pathLengthLabel.Name = "_pathLengthLabel";
this._pathLengthLabel.Size = new System.Drawing.Size(71, 12);
this._pathLengthLabel.TabIndex = 2;
this._pathLengthLabel.Text = "경로 길이: -";
//
// _agvCountLabel
//
this._agvCountLabel.AutoSize = true;
this._agvCountLabel.Location = new System.Drawing.Point(10, 45);
this._agvCountLabel.Name = "_agvCountLabel";
this._agvCountLabel.Size = new System.Drawing.Size(60, 12);
this._agvCountLabel.TabIndex = 1;
this._agvCountLabel.Text = "AGV 수: 0";
//
// _simulationStatusLabel
//
this._simulationStatusLabel.AutoSize = true;
this._simulationStatusLabel.Location = new System.Drawing.Point(10, 25);
this._simulationStatusLabel.Name = "_simulationStatusLabel";
this._simulationStatusLabel.Size = new System.Drawing.Size(97, 12);
this._simulationStatusLabel.TabIndex = 0;
this._simulationStatusLabel.Text = "시뮬레이션: 정지";
//
// _pathGroup
//
this._pathGroup.Controls.Add(this._clearPathButton);
this._pathGroup.Controls.Add(this._startPathButton);
this._pathGroup.Controls.Add(this._calculatePathButton);
this._pathGroup.Controls.Add(this._targetNodeCombo);
this._pathGroup.Controls.Add(this.targetNodeLabel);
this._pathGroup.Controls.Add(this._startNodeCombo);
this._pathGroup.Controls.Add(this.startNodeLabel);
this._pathGroup.Location = new System.Drawing.Point(10, 200);
this._pathGroup.Name = "_pathGroup";
this._pathGroup.Size = new System.Drawing.Size(230, 150);
this._pathGroup.TabIndex = 1;
this._pathGroup.TabStop = false;
this._pathGroup.Text = "경로 제어";
//
// _clearPathButton
//
this._clearPathButton.Location = new System.Drawing.Point(150, 120);
this._clearPathButton.Name = "_clearPathButton";
this._clearPathButton.Size = new System.Drawing.Size(70, 25);
this._clearPathButton.TabIndex = 6;
this._clearPathButton.Text = "경로 지우기";
this._clearPathButton.UseVisualStyleBackColor = true;
this._clearPathButton.Click += new System.EventHandler(this.OnClearPath_Click);
//
// _startPathButton
//
this._startPathButton.Location = new System.Drawing.Point(80, 120);
this._startPathButton.Name = "_startPathButton";
this._startPathButton.Size = new System.Drawing.Size(65, 25);
this._startPathButton.TabIndex = 5;
this._startPathButton.Text = "경로 시작";
this._startPathButton.UseVisualStyleBackColor = true;
this._startPathButton.Click += new System.EventHandler(this.OnStartPath_Click);
//
// _calculatePathButton
//
this._calculatePathButton.Location = new System.Drawing.Point(10, 120);
this._calculatePathButton.Name = "_calculatePathButton";
this._calculatePathButton.Size = new System.Drawing.Size(65, 25);
this._calculatePathButton.TabIndex = 4;
this._calculatePathButton.Text = "경로 계산";
this._calculatePathButton.UseVisualStyleBackColor = true;
this._calculatePathButton.Click += new System.EventHandler(this.OnCalculatePath_Click);
//
// _targetNodeCombo
//
this._targetNodeCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this._targetNodeCombo.Location = new System.Drawing.Point(10, 95);
this._targetNodeCombo.Name = "_targetNodeCombo";
this._targetNodeCombo.Size = new System.Drawing.Size(210, 20);
this._targetNodeCombo.TabIndex = 3;
//
// targetNodeLabel
//
this.targetNodeLabel.AutoSize = true;
this.targetNodeLabel.Location = new System.Drawing.Point(10, 75);
this.targetNodeLabel.Name = "targetNodeLabel";
this.targetNodeLabel.Size = new System.Drawing.Size(63, 12);
this.targetNodeLabel.TabIndex = 2;
this.targetNodeLabel.Text = "목표 RFID:";
//
// _startNodeCombo
//
this._startNodeCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this._startNodeCombo.Location = new System.Drawing.Point(10, 45);
this._startNodeCombo.Name = "_startNodeCombo";
this._startNodeCombo.Size = new System.Drawing.Size(210, 20);
this._startNodeCombo.TabIndex = 1;
//
// startNodeLabel
//
this.startNodeLabel.AutoSize = true;
this.startNodeLabel.Location = new System.Drawing.Point(10, 25);
this.startNodeLabel.Name = "startNodeLabel";
this.startNodeLabel.Size = new System.Drawing.Size(63, 12);
this.startNodeLabel.TabIndex = 0;
this.startNodeLabel.Text = "시작 RFID:";
//
// _agvControlGroup
//
this._agvControlGroup.Controls.Add(this._setPositionButton);
this._agvControlGroup.Controls.Add(this._rfidTextBox);
this._agvControlGroup.Controls.Add(this._rfidLabel);
this._agvControlGroup.Controls.Add(this._stopSimulationButton);
this._agvControlGroup.Controls.Add(this._startSimulationButton);
this._agvControlGroup.Controls.Add(this._removeAgvButton);
this._agvControlGroup.Controls.Add(this._addAgvButton);
this._agvControlGroup.Controls.Add(this._agvListCombo);
this._agvControlGroup.Location = new System.Drawing.Point(10, 10);
this._agvControlGroup.Name = "_agvControlGroup";
this._agvControlGroup.Size = new System.Drawing.Size(230, 180);
this._agvControlGroup.TabIndex = 0;
this._agvControlGroup.TabStop = false;
this._agvControlGroup.Text = "AGV 제어";
//
// _setPositionButton
//
this._setPositionButton.Location = new System.Drawing.Point(160, 138);
this._setPositionButton.Name = "_setPositionButton";
this._setPositionButton.Size = new System.Drawing.Size(60, 25);
this._setPositionButton.TabIndex = 7;
this._setPositionButton.Text = "위치설정";
this._setPositionButton.UseVisualStyleBackColor = true;
this._setPositionButton.Click += new System.EventHandler(this.OnSetPosition_Click);
//
// _rfidTextBox
//
this._rfidTextBox.Location = new System.Drawing.Point(10, 140);
this._rfidTextBox.Name = "_rfidTextBox";
this._rfidTextBox.Size = new System.Drawing.Size(140, 21);
this._rfidTextBox.TabIndex = 6;
this._rfidTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.OnRfidTextBox_KeyPress);
//
// _rfidLabel
//
this._rfidLabel.AutoSize = true;
this._rfidLabel.Location = new System.Drawing.Point(10, 120);
this._rfidLabel.Name = "_rfidLabel";
this._rfidLabel.Size = new System.Drawing.Size(87, 12);
this._rfidLabel.TabIndex = 5;
this._rfidLabel.Text = "RFID 현재위치:";
//
// _stopSimulationButton
//
this._stopSimulationButton.Location = new System.Drawing.Point(120, 85);
this._stopSimulationButton.Name = "_stopSimulationButton";
this._stopSimulationButton.Size = new System.Drawing.Size(100, 25);
this._stopSimulationButton.TabIndex = 4;
this._stopSimulationButton.Text = "시뮬레이션 정지";
this._stopSimulationButton.UseVisualStyleBackColor = true;
this._stopSimulationButton.Click += new System.EventHandler(this.OnStopSimulation_Click);
//
// _startSimulationButton
//
this._startSimulationButton.Location = new System.Drawing.Point(10, 85);
this._startSimulationButton.Name = "_startSimulationButton";
this._startSimulationButton.Size = new System.Drawing.Size(100, 25);
this._startSimulationButton.TabIndex = 3;
this._startSimulationButton.Text = "시뮬레이션 시작";
this._startSimulationButton.UseVisualStyleBackColor = true;
this._startSimulationButton.Click += new System.EventHandler(this.OnStartSimulation_Click);
//
// _removeAgvButton
//
this._removeAgvButton.Location = new System.Drawing.Point(120, 55);
this._removeAgvButton.Name = "_removeAgvButton";
this._removeAgvButton.Size = new System.Drawing.Size(100, 25);
this._removeAgvButton.TabIndex = 2;
this._removeAgvButton.Text = "AGV 제거";
this._removeAgvButton.UseVisualStyleBackColor = true;
this._removeAgvButton.Click += new System.EventHandler(this.OnRemoveAGV_Click);
//
// _addAgvButton
//
this._addAgvButton.Location = new System.Drawing.Point(10, 55);
this._addAgvButton.Name = "_addAgvButton";
this._addAgvButton.Size = new System.Drawing.Size(100, 25);
this._addAgvButton.TabIndex = 1;
this._addAgvButton.Text = "AGV 추가";
this._addAgvButton.UseVisualStyleBackColor = true;
this._addAgvButton.Click += new System.EventHandler(this.OnAddAGV_Click);
//
// _agvListCombo
//
this._agvListCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this._agvListCombo.Location = new System.Drawing.Point(10, 25);
this._agvListCombo.Name = "_agvListCombo";
this._agvListCombo.Size = new System.Drawing.Size(210, 20);
this._agvListCombo.TabIndex = 0;
this._agvListCombo.SelectedIndexChanged += new System.EventHandler(this.OnAGVList_SelectedIndexChanged);
//
// _canvasPanel
//
this._canvasPanel.Dock = System.Windows.Forms.DockStyle.Fill;
this._canvasPanel.Location = new System.Drawing.Point(0, 49);
this._canvasPanel.Name = "_canvasPanel";
this._canvasPanel.Size = new System.Drawing.Size(950, 729);
this._canvasPanel.TabIndex = 4;
//
// btAllReset
//
this.btAllReset.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.btAllReset.Name = "btAllReset";
this.btAllReset.Size = new System.Drawing.Size(71, 22);
this.btAllReset.Text = "전체초기화";
this.btAllReset.ToolTipText = "시뮬레이션을 초기화합니다";
this.btAllReset.Click += new System.EventHandler(this.btAllReset_Click);
//
// SimulatorForm // SimulatorForm
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F); this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1200, 800); this.ClientSize = new System.Drawing.Size(1200, 800);
this.Controls.Add(this._canvasPanel);
this.Controls.Add(this._controlPanel);
this.Controls.Add(this._statusStrip);
this.Controls.Add(this._toolStrip);
this.Controls.Add(this._menuStrip);
this.MainMenuStrip = this._menuStrip;
this.Name = "SimulatorForm"; this.Name = "SimulatorForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "AGV 시뮬레이터"; this.Text = "AGV 시뮬레이터";
this.WindowState = System.Windows.Forms.FormWindowState.Maximized; this.WindowState = System.Windows.Forms.FormWindowState.Maximized;
this._menuStrip.ResumeLayout(false);
this._menuStrip.PerformLayout();
this._toolStrip.ResumeLayout(false);
this._toolStrip.PerformLayout();
this._statusStrip.ResumeLayout(false);
this._statusStrip.PerformLayout();
this._controlPanel.ResumeLayout(false);
this._statusGroup.ResumeLayout(false);
this._statusGroup.PerformLayout();
this._pathGroup.ResumeLayout(false);
this._pathGroup.PerformLayout();
this._agvControlGroup.ResumeLayout(false);
this._agvControlGroup.PerformLayout();
this.ResumeLayout(false); this.ResumeLayout(false);
this.PerformLayout();
} }
#endregion #endregion
private System.Windows.Forms.MenuStrip _menuStrip;
private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem openMapToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem simulationToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem startSimulationToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem stopSimulationToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem resetToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem viewToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem fitToMapToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem resetZoomToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem helpToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem aboutToolStripMenuItem;
private System.Windows.Forms.ToolStrip _toolStrip;
private System.Windows.Forms.ToolStripButton openMapToolStripButton;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
private System.Windows.Forms.ToolStripButton startSimulationToolStripButton;
private System.Windows.Forms.ToolStripButton stopSimulationToolStripButton;
private System.Windows.Forms.ToolStripButton resetToolStripButton;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
private System.Windows.Forms.ToolStripButton fitToMapToolStripButton;
private System.Windows.Forms.ToolStripButton resetZoomToolStripButton;
private System.Windows.Forms.StatusStrip _statusStrip;
private System.Windows.Forms.ToolStripStatusLabel _statusLabel;
private System.Windows.Forms.ToolStripStatusLabel _coordLabel;
private System.Windows.Forms.Panel _controlPanel;
private System.Windows.Forms.GroupBox _agvControlGroup;
private System.Windows.Forms.ComboBox _agvListCombo;
private System.Windows.Forms.Button _addAgvButton;
private System.Windows.Forms.Button _removeAgvButton;
private System.Windows.Forms.Button _startSimulationButton;
private System.Windows.Forms.Button _stopSimulationButton;
private System.Windows.Forms.GroupBox _pathGroup;
private System.Windows.Forms.Label startNodeLabel;
private System.Windows.Forms.ComboBox _startNodeCombo;
private System.Windows.Forms.Label targetNodeLabel;
private System.Windows.Forms.ComboBox _targetNodeCombo;
private System.Windows.Forms.Button _calculatePathButton;
private System.Windows.Forms.Button _startPathButton;
private System.Windows.Forms.Button _clearPathButton;
private System.Windows.Forms.GroupBox _statusGroup;
private System.Windows.Forms.Label _simulationStatusLabel;
private System.Windows.Forms.Label _agvCountLabel;
private System.Windows.Forms.Label _pathLengthLabel;
private System.Windows.Forms.Panel _canvasPanel;
private System.Windows.Forms.Label _rfidLabel;
private System.Windows.Forms.TextBox _rfidTextBox;
private System.Windows.Forms.Button _setPositionButton;
private System.Windows.Forms.ToolStripButton btAllReset;
private System.Windows.Forms.ToolStripMenuItem reloadMapToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem launchMapEditorToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
private System.Windows.Forms.ToolStripButton reloadMapToolStripButton;
private System.Windows.Forms.ToolStripButton launchMapEditorToolStripButton;
} }
} }

View File

@@ -1,11 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using AGVMapEditor.Models; using AGVMapEditor.Models;
using AGVSimulator.Controls; using AGVNavigationCore.Models;
using AGVNavigationCore.Controls;
using AGVSimulator.Models; using AGVSimulator.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -18,50 +20,17 @@ namespace AGVSimulator.Forms
{ {
#region Fields #region Fields
private SimulatorCanvas _simulatorCanvas; private UnifiedAGVCanvas _simulatorCanvas;
private List<MapNode> _mapNodes; private List<MapNode> _mapNodes;
private List<RfidMapping> _rfidMappings;
private NodeResolver _nodeResolver; private NodeResolver _nodeResolver;
private PathCalculator _pathCalculator; private PathCalculator _pathCalculator;
private List<VirtualAGV> _agvList; private List<VirtualAGV> _agvList;
private SimulationState _simulationState; private SimulationState _simulationState;
private Timer _simulationTimer; private Timer _simulationTimer;
private SimulatorConfig _config;
private string _currentMapFilePath;
// UI Controls // UI Controls - Designer에서 생성됨
private MenuStrip _menuStrip;
private ToolStrip _toolStrip;
private StatusStrip _statusStrip;
private Panel _controlPanel;
private Panel _canvasPanel;
// Control Panel Controls
private GroupBox _agvControlGroup;
private ComboBox _agvListCombo;
private Button _addAgvButton;
private Button _removeAgvButton;
private Button _startSimulationButton;
private Button _stopSimulationButton;
private Button _resetButton;
private GroupBox _pathGroup;
private ComboBox _startNodeCombo;
private ComboBox _targetNodeCombo;
private Button _calculatePathButton;
private Button _startPathButton;
private Button _clearPathButton;
private GroupBox _viewGroup;
private Button _fitToMapButton;
private Button _resetZoomButton;
private GroupBox _statusGroup;
private Label _simulationStatusLabel;
private Label _agvCountLabel;
private Label _pathLengthLabel;
// Status Labels
private ToolStripStatusLabel _statusLabel;
private ToolStripStatusLabel _coordLabel;
#endregion #endregion
@@ -88,27 +57,18 @@ namespace AGVSimulator.Forms
private void InitializeForm() private void InitializeForm()
{ {
// 설정 // 설정 로드
Text = "AGV 시뮬레이터"; _config = SimulatorConfig.Load();
Size = new Size(1200, 800);
StartPosition = FormStartPosition.CenterScreen;
// 데이터 초기화 // 데이터 초기화
_mapNodes = new List<MapNode>(); _mapNodes = new List<MapNode>();
_rfidMappings = new List<RfidMapping>();
_agvList = new List<VirtualAGV>(); _agvList = new List<VirtualAGV>();
_simulationState = new SimulationState(); _simulationState = new SimulationState();
_currentMapFilePath = string.Empty;
// UI 컨트롤 생성 // 시뮬레이터 캔버스 생성 (중앙 패널에만)
CreateMenuStrip();
CreateToolStrip();
CreateStatusStrip();
CreateControlPanel();
CreateSimulatorCanvas(); CreateSimulatorCanvas();
// 레이아웃 설정
SetupLayout();
// 타이머 초기화 // 타이머 초기화
_simulationTimer = new Timer(); _simulationTimer = new Timer();
_simulationTimer.Interval = 100; // 100ms 간격 _simulationTimer.Interval = 100; // 100ms 간격
@@ -118,260 +78,21 @@ namespace AGVSimulator.Forms
UpdateUI(); UpdateUI();
} }
private void CreateMenuStrip()
{
_menuStrip = new MenuStrip();
// 파일 메뉴
var fileMenu = new ToolStripMenuItem("파일(&F)");
fileMenu.DropDownItems.Add(new ToolStripMenuItem("맵 열기(&O)...", null, OnOpenMap_Click) { ShortcutKeys = Keys.Control | Keys.O });
fileMenu.DropDownItems.Add(new ToolStripSeparator());
fileMenu.DropDownItems.Add(new ToolStripMenuItem("종료(&X)", null, OnExit_Click) { ShortcutKeys = Keys.Alt | Keys.F4 });
// 시뮬레이션 메뉴
var simMenu = new ToolStripMenuItem("시뮬레이션(&S)");
simMenu.DropDownItems.Add(new ToolStripMenuItem("시작(&S)", null, OnStartSimulation_Click) { ShortcutKeys = Keys.F5 });
simMenu.DropDownItems.Add(new ToolStripMenuItem("정지(&T)", null, OnStopSimulation_Click) { ShortcutKeys = Keys.F6 });
simMenu.DropDownItems.Add(new ToolStripMenuItem("초기화(&R)", null, OnReset_Click) { ShortcutKeys = Keys.F7 });
// 보기 메뉴
var viewMenu = new ToolStripMenuItem("보기(&V)");
viewMenu.DropDownItems.Add(new ToolStripMenuItem("맵 맞춤(&F)", null, OnFitToMap_Click) { ShortcutKeys = Keys.Control | Keys.F });
viewMenu.DropDownItems.Add(new ToolStripMenuItem("줌 초기화(&Z)", null, OnResetZoom_Click) { ShortcutKeys = Keys.Control | Keys.D0 });
// 도움말 메뉴
var helpMenu = new ToolStripMenuItem("도움말(&H)");
helpMenu.DropDownItems.Add(new ToolStripMenuItem("정보(&A)...", null, OnAbout_Click));
_menuStrip.Items.AddRange(new ToolStripItem[] { fileMenu, simMenu, viewMenu, helpMenu });
Controls.Add(_menuStrip);
MainMenuStrip = _menuStrip;
}
private void CreateToolStrip()
{
_toolStrip = new ToolStrip();
_toolStrip.Items.Add(new ToolStripButton("맵 열기", null, OnOpenMap_Click) { ToolTipText = "맵 파일을 엽니다" });
_toolStrip.Items.Add(new ToolStripSeparator());
_toolStrip.Items.Add(new ToolStripButton("시뮬레이션 시작", null, OnStartSimulation_Click) { ToolTipText = "시뮬레이션을 시작합니다" });
_toolStrip.Items.Add(new ToolStripButton("시뮬레이션 정지", null, OnStopSimulation_Click) { ToolTipText = "시뮬레이션을 정지합니다" });
_toolStrip.Items.Add(new ToolStripButton("초기화", null, OnReset_Click) { ToolTipText = "시뮬레이션을 초기화합니다" });
_toolStrip.Items.Add(new ToolStripSeparator());
_toolStrip.Items.Add(new ToolStripButton("맵 맞춤", null, OnFitToMap_Click) { ToolTipText = "맵 전체를 화면에 맞춥니다" });
_toolStrip.Items.Add(new ToolStripButton("줌 초기화", null, OnResetZoom_Click) { ToolTipText = "줌을 초기화합니다" });
Controls.Add(_toolStrip);
}
private void CreateStatusStrip()
{
_statusStrip = new StatusStrip();
_statusLabel = new ToolStripStatusLabel("준비");
_coordLabel = new ToolStripStatusLabel();
_statusStrip.Items.AddRange(new ToolStripItem[] { _statusLabel, _coordLabel });
Controls.Add(_statusStrip);
}
private void CreateControlPanel()
{
_controlPanel = new Panel();
_controlPanel.Width = 250;
_controlPanel.Dock = DockStyle.Right;
_controlPanel.BackColor = SystemColors.Control;
// AGV 제어 그룹
CreateAGVControlGroup();
// 경로 제어 그룹
CreatePathControlGroup();
// 뷰 제어 그룹
CreateViewControlGroup();
// 상태 그룹
CreateStatusGroup();
Controls.Add(_controlPanel);
}
private void CreateAGVControlGroup()
{
_agvControlGroup = new GroupBox();
_agvControlGroup.Text = "AGV 제어";
_agvControlGroup.Location = new Point(10, 10);
_agvControlGroup.Size = new Size(230, 120);
_agvListCombo = new ComboBox();
_agvListCombo.DropDownStyle = ComboBoxStyle.DropDownList;
_agvListCombo.Location = new Point(10, 25);
_agvListCombo.Size = new Size(210, 21);
_agvListCombo.SelectedIndexChanged += OnAGVList_SelectedIndexChanged;
_addAgvButton = new Button();
_addAgvButton.Text = "AGV 추가";
_addAgvButton.Location = new Point(10, 55);
_addAgvButton.Size = new Size(100, 25);
_addAgvButton.Click += OnAddAGV_Click;
_removeAgvButton = new Button();
_removeAgvButton.Text = "AGV 제거";
_removeAgvButton.Location = new Point(120, 55);
_removeAgvButton.Size = new Size(100, 25);
_removeAgvButton.Click += OnRemoveAGV_Click;
_startSimulationButton = new Button();
_startSimulationButton.Text = "시뮬레이션 시작";
_startSimulationButton.Location = new Point(10, 85);
_startSimulationButton.Size = new Size(100, 25);
_startSimulationButton.Click += OnStartSimulation_Click;
_stopSimulationButton = new Button();
_stopSimulationButton.Text = "시뮬레이션 정지";
_stopSimulationButton.Location = new Point(120, 85);
_stopSimulationButton.Size = new Size(100, 25);
_stopSimulationButton.Click += OnStopSimulation_Click;
_agvControlGroup.Controls.AddRange(new Control[] {
_agvListCombo, _addAgvButton, _removeAgvButton, _startSimulationButton, _stopSimulationButton
});
_controlPanel.Controls.Add(_agvControlGroup);
}
private void CreatePathControlGroup()
{
_pathGroup = new GroupBox();
_pathGroup.Text = "경로 제어";
_pathGroup.Location = new Point(10, 140);
_pathGroup.Size = new Size(230, 150);
var startLabel = new Label();
startLabel.Text = "시작 노드:";
startLabel.Location = new Point(10, 25);
startLabel.Size = new Size(70, 15);
_startNodeCombo = new ComboBox();
_startNodeCombo.DropDownStyle = ComboBoxStyle.DropDownList;
_startNodeCombo.Location = new Point(10, 45);
_startNodeCombo.Size = new Size(210, 21);
var targetLabel = new Label();
targetLabel.Text = "목표 노드:";
targetLabel.Location = new Point(10, 75);
targetLabel.Size = new Size(70, 15);
_targetNodeCombo = new ComboBox();
_targetNodeCombo.DropDownStyle = ComboBoxStyle.DropDownList;
_targetNodeCombo.Location = new Point(10, 95);
_targetNodeCombo.Size = new Size(210, 21);
_calculatePathButton = new Button();
_calculatePathButton.Text = "경로 계산";
_calculatePathButton.Location = new Point(10, 120);
_calculatePathButton.Size = new Size(65, 25);
_calculatePathButton.Click += OnCalculatePath_Click;
_startPathButton = new Button();
_startPathButton.Text = "경로 시작";
_startPathButton.Location = new Point(80, 120);
_startPathButton.Size = new Size(65, 25);
_startPathButton.Click += OnStartPath_Click;
_clearPathButton = new Button();
_clearPathButton.Text = "경로 지우기";
_clearPathButton.Location = new Point(150, 120);
_clearPathButton.Size = new Size(70, 25);
_clearPathButton.Click += OnClearPath_Click;
_pathGroup.Controls.AddRange(new Control[] {
startLabel, _startNodeCombo, targetLabel, _targetNodeCombo,
_calculatePathButton, _startPathButton, _clearPathButton
});
_controlPanel.Controls.Add(_pathGroup);
}
private void CreateViewControlGroup()
{
_viewGroup = new GroupBox();
_viewGroup.Text = "화면 제어";
_viewGroup.Location = new Point(10, 300);
_viewGroup.Size = new Size(230, 60);
_fitToMapButton = new Button();
_fitToMapButton.Text = "맵 맞춤";
_fitToMapButton.Location = new Point(10, 25);
_fitToMapButton.Size = new Size(100, 25);
_fitToMapButton.Click += OnFitToMap_Click;
_resetZoomButton = new Button();
_resetZoomButton.Text = "줌 초기화";
_resetZoomButton.Location = new Point(120, 25);
_resetZoomButton.Size = new Size(100, 25);
_resetZoomButton.Click += OnResetZoom_Click;
_resetButton = new Button();
_resetButton.Text = "전체 초기화";
_resetButton.Location = new Point(65, 55);
_resetButton.Size = new Size(100, 25);
_resetButton.Click += OnReset_Click;
_viewGroup.Controls.AddRange(new Control[] { _fitToMapButton, _resetZoomButton });
_controlPanel.Controls.Add(_viewGroup);
}
private void CreateStatusGroup()
{
_statusGroup = new GroupBox();
_statusGroup.Text = "상태 정보";
_statusGroup.Location = new Point(10, 370);
_statusGroup.Size = new Size(230, 100);
_simulationStatusLabel = new Label();
_simulationStatusLabel.Text = "시뮬레이션: 정지";
_simulationStatusLabel.Location = new Point(10, 25);
_simulationStatusLabel.Size = new Size(210, 15);
_agvCountLabel = new Label();
_agvCountLabel.Text = "AGV 수: 0";
_agvCountLabel.Location = new Point(10, 45);
_agvCountLabel.Size = new Size(210, 15);
_pathLengthLabel = new Label();
_pathLengthLabel.Text = "경로 길이: -";
_pathLengthLabel.Location = new Point(10, 65);
_pathLengthLabel.Size = new Size(210, 15);
_statusGroup.Controls.AddRange(new Control[] {
_simulationStatusLabel, _agvCountLabel, _pathLengthLabel
});
_controlPanel.Controls.Add(_statusGroup);
}
private void CreateSimulatorCanvas() private void CreateSimulatorCanvas()
{ {
_canvasPanel = new Panel(); _simulatorCanvas = new UnifiedAGVCanvas();
_canvasPanel.Dock = DockStyle.Fill;
_simulatorCanvas = new SimulatorCanvas();
_simulatorCanvas.Dock = DockStyle.Fill; _simulatorCanvas.Dock = DockStyle.Fill;
_simulatorCanvas.Mode = UnifiedAGVCanvas.CanvasMode.ViewOnly;
_canvasPanel.Controls.Add(_simulatorCanvas); _canvasPanel.Controls.Add(_simulatorCanvas);
Controls.Add(_canvasPanel);
} }
private void SetupLayout() private void SetupLayout()
{ {
// Z-Order 설정 // Z-Order 설정 - 모든 컨트롤이 디자이너에 구현되어 자동 관리됨
_canvasPanel.BringToFront(); _canvasPanel.BringToFront();
_controlPanel.BringToFront();
_toolStrip.BringToFront();
_menuStrip.BringToFront();
} }
#endregion #endregion
@@ -382,7 +103,7 @@ namespace AGVSimulator.Forms
{ {
using (var openDialog = new OpenFileDialog()) using (var openDialog = new OpenFileDialog())
{ {
openDialog.Filter = "맵 파일 (*.json)|*.json|모든 파일 (*.*)|*.*"; openDialog.Filter = "AGV Map Files (*.agvmap)|*.agvmap|모든 파일 (*.*)|*.*";
openDialog.Title = "맵 파일 열기"; openDialog.Title = "맵 파일 열기";
if (openDialog.ShowDialog() == DialogResult.OK) if (openDialog.ShowDialog() == DialogResult.OK)
@@ -430,30 +151,12 @@ namespace AGVSimulator.Forms
private void OnReset_Click(object sender, EventArgs e) private void OnReset_Click(object sender, EventArgs e)
{ {
// 시뮬레이션 정지
if (_simulationState.IsRunning)
{
OnStopSimulation_Click(sender, e);
}
// AGV 초기화
_simulatorCanvas.ClearAGVs();
_agvList.Clear();
// 경로 초기화
_simulatorCanvas.CurrentPath = null;
// UI 업데이트
UpdateAGVComboBox();
UpdateNodeComboBoxes();
UpdateUI();
_statusLabel.Text = "초기화 완료";
} }
private void OnFitToMap_Click(object sender, EventArgs e) private void OnFitToMap_Click(object sender, EventArgs e)
{ {
_simulatorCanvas.FitToMap(); _simulatorCanvas.FitToNodes();
} }
private void OnResetZoom_Click(object sender, EventArgs e) private void OnResetZoom_Click(object sender, EventArgs e)
@@ -480,7 +183,7 @@ namespace AGVSimulator.Forms
var newAGV = new VirtualAGV(agvId, startPosition); var newAGV = new VirtualAGV(agvId, startPosition);
_agvList.Add(newAGV); _agvList.Add(newAGV);
_simulatorCanvas.AddAGV(newAGV); _simulatorCanvas.AGVList = new List<IAGV>(_agvList.Cast<IAGV>());
UpdateAGVComboBox(); UpdateAGVComboBox();
UpdateUI(); UpdateUI();
@@ -496,8 +199,8 @@ namespace AGVSimulator.Forms
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV; var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
if (selectedAGV != null) if (selectedAGV != null)
{ {
_simulatorCanvas.RemoveAGV(selectedAGV.AgvId);
_agvList.Remove(selectedAGV); _agvList.Remove(selectedAGV);
_simulatorCanvas.AGVList = new List<IAGV>(_agvList.Cast<IAGV>());
UpdateAGVComboBox(); UpdateAGVComboBox();
UpdateUI(); UpdateUI();
@@ -515,7 +218,7 @@ namespace AGVSimulator.Forms
{ {
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null) if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
{ {
MessageBox.Show("시작 노드와 목표 노드를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Show("시작 RFID와 목표 RFID를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return; return;
} }
@@ -524,20 +227,21 @@ namespace AGVSimulator.Forms
if (_pathCalculator == null) if (_pathCalculator == null)
{ {
_pathCalculator = new PathCalculator(_mapNodes, _nodeResolver); _pathCalculator = new PathCalculator();
_pathCalculator.SetMapData(_mapNodes);
} }
var result = _pathCalculator.CalculatePath(startNode.NodeId, targetNode.NodeId, AgvDirection.Forward); var agvResult = _pathCalculator.FindAGVPath(startNode.NodeId, targetNode.NodeId);
if (result.Success) if (agvResult.Success)
{ {
_simulatorCanvas.CurrentPath = result; _simulatorCanvas.CurrentPath = agvResult.ToPathResult();
_pathLengthLabel.Text = $"경로 길이: {result.TotalDistance:F1}"; _pathLengthLabel.Text = $"경로 길이: {agvResult.TotalDistance:F1}";
_statusLabel.Text = $"경로 계산 완료 ({result.CalculationTime}ms)"; _statusLabel.Text = $"경로 계산 완료 ({agvResult.CalculationTimeMs}ms)";
} }
else else
{ {
MessageBox.Show($"경로를 찾을 수 없습니다:\n{result.ErrorMessage}", "경로 계산 실패", MessageBox.Show($"경로를 찾을 수 없습니다:\n{agvResult.ErrorMessage}", "경로 계산 실패",
MessageBoxButtons.OK, MessageBoxIcon.Warning); MessageBoxButtons.OK, MessageBoxIcon.Warning);
} }
} }
@@ -568,6 +272,20 @@ namespace AGVSimulator.Forms
_statusLabel.Text = "경로 지움"; _statusLabel.Text = "경로 지움";
} }
private void OnSetPosition_Click(object sender, EventArgs e)
{
SetAGVPositionByRfid();
}
private void OnRfidTextBox_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)Keys.Enter)
{
SetAGVPositionByRfid();
e.Handled = true;
}
}
private void OnSimulationTimer_Tick(object sender, EventArgs e) private void OnSimulationTimer_Tick(object sender, EventArgs e)
{ {
// 시뮬레이션 업데이트는 각 AGV의 내부 타이머에서 처리됨 // 시뮬레이션 업데이트는 각 AGV의 내부 타이머에서 처리됨
@@ -578,38 +296,97 @@ namespace AGVSimulator.Forms
#region Private Methods #region Private Methods
private void SetAGVPositionByRfid()
{
// 선택된 AGV 확인
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
if (selectedAGV == null)
{
MessageBox.Show("먼저 AGV를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// RFID 값 확인
var rfidId = _rfidTextBox.Text.Trim();
if (string.IsNullOrEmpty(rfidId))
{
MessageBox.Show("RFID 값을 입력해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// RFID에 해당하는 노드 직접 찾기
var targetNode = _mapNodes?.FirstOrDefault(n => n.RfidId.Equals(rfidId, StringComparison.OrdinalIgnoreCase));
if (targetNode == null)
{
MessageBox.Show($"RFID '{rfidId}'에 해당하는 노드를 찾을 수 없습니다.\n\n사용 가능한 RFID 목록:\n{GetAvailableRfidList()}",
"RFID 찾기 실패", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// AGV 위치 설정
_simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, targetNode.Position);
_statusLabel.Text = $"{selectedAGV.AgvId} 위치를 RFID '{rfidId}' (노드: {targetNode.NodeId})로 설정했습니다.";
_rfidTextBox.Text = ""; // 입력 필드 초기화
// 시뮬레이터 캔버스의 해당 노드로 이동
_simulatorCanvas.PanToNode(targetNode.NodeId);
}
private string GetAvailableRfidList()
{
if (_mapNodes == null || _mapNodes.Count == 0)
return "매핑된 RFID가 없습니다.";
var nodesWithRfid = _mapNodes.Where(n => n.HasRfid()).ToList();
if (nodesWithRfid.Count == 0)
return "RFID가 할당된 노드가 없습니다.";
// 처음 10개의 RFID만 표시
var rfidList = nodesWithRfid.Take(10).Select(n => $"- {n.RfidId} → {n.NodeId}");
var result = string.Join("\n", rfidList);
if (nodesWithRfid.Count > 10)
result += $"\n... 외 {nodesWithRfid.Count - 10}개";
return result;
}
private void LoadMapFile(string filePath) private void LoadMapFile(string filePath)
{ {
try try
{ {
var json = File.ReadAllText(filePath); var result = MapLoader.LoadMapFromFile(filePath);
// 구조체로 직접 역직렬화 if (result.Success)
var mapData = JsonConvert.DeserializeObject<MapFileData>(json);
if (mapData != null)
{ {
_mapNodes = mapData.MapNodes ?? new List<MapNode>(); _mapNodes = result.Nodes;
_rfidMappings = mapData.RfidMappings ?? new List<RfidMapping>(); _currentMapFilePath = filePath;
// RFID가 없는 노드들에 자동 할당
MapLoader.AssignAutoRfidIds(_mapNodes);
// 시뮬레이터 캔버스에 맵 설정
_simulatorCanvas.Nodes = _mapNodes;
// 설정에 마지막 맵 파일 경로 저장
_config.LastMapFilePath = filePath;
if (_config.AutoSave)
{
_config.Save();
}
// UI 업데이트
UpdateNodeComboBoxes();
UpdateUI();
// 맵에 맞춤
_simulatorCanvas.FitToNodes();
} }
else else
{ {
_mapNodes = new List<MapNode>(); throw new InvalidOperationException($"맵 파일 로드 실패: {result.ErrorMessage}");
_rfidMappings = new List<RfidMapping>();
} }
// NodeResolver 초기화
_nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
// 시뮬레이터 캔버스에 맵 설정
_simulatorCanvas.MapNodes = _mapNodes;
// UI 업데이트
UpdateNodeComboBoxes();
UpdateUI();
// 맵에 맞춤
_simulatorCanvas.FitToMap();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -617,13 +394,6 @@ namespace AGVSimulator.Forms
} }
} }
// 맵 파일 데이터 구조체
private class MapFileData
{
public List<MapNode> MapNodes { get; set; }
public List<RfidMapping> RfidMappings { get; set; }
}
private void UpdateNodeComboBoxes() private void UpdateNodeComboBoxes()
{ {
_startNodeCombo.Items.Clear(); _startNodeCombo.Items.Clear();
@@ -633,13 +403,16 @@ namespace AGVSimulator.Forms
{ {
foreach (var node in _mapNodes) foreach (var node in _mapNodes)
{ {
_startNodeCombo.Items.Add(node); if (node.IsActive && node.HasRfid())
_targetNodeCombo.Items.Add(node); {
_startNodeCombo.Items.Add(node);
_targetNodeCombo.Items.Add(node);
}
} }
} }
_startNodeCombo.DisplayMember = "NodeId"; _startNodeCombo.DisplayMember = "RfidId";
_targetNodeCombo.DisplayMember = "NodeId"; _targetNodeCombo.DisplayMember = "RfidId";
} }
private void UpdateAGVComboBox() private void UpdateAGVComboBox()
@@ -681,8 +454,128 @@ namespace AGVSimulator.Forms
_calculatePathButton.Enabled = _startNodeCombo.SelectedItem != null && _calculatePathButton.Enabled = _startNodeCombo.SelectedItem != null &&
_targetNodeCombo.SelectedItem != null; _targetNodeCombo.SelectedItem != null;
// RFID 위치 설정 관련
var hasSelectedAGV = _agvListCombo.SelectedItem != null;
var hasRfidNodes = _mapNodes != null && _mapNodes.Any(n => n.HasRfid());
_setPositionButton.Enabled = hasSelectedAGV && hasRfidNodes;
_rfidTextBox.Enabled = hasSelectedAGV && hasRfidNodes;
// 맵 다시열기 버튼
var hasCurrentMap = !string.IsNullOrEmpty(_currentMapFilePath);
reloadMapToolStripMenuItem.Enabled = hasCurrentMap;
reloadMapToolStripButton.Enabled = hasCurrentMap;
}
private void OnReloadMap_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(_currentMapFilePath))
{
MessageBox.Show("다시 로드할 맵 파일이 없습니다. 먼저 맵을 열어주세요.", "알림",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (!File.Exists(_currentMapFilePath))
{
MessageBox.Show($"맵 파일을 찾을 수 없습니다:\n{_currentMapFilePath}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
try
{
LoadMapFile(_currentMapFilePath);
_statusLabel.Text = $"맵 다시 로드 완료: {Path.GetFileName(_currentMapFilePath)}";
}
catch (Exception ex)
{
MessageBox.Show($"맵 파일을 다시 로드할 수 없습니다:\n{ex.Message}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void OnLaunchMapEditor_Click(object sender, EventArgs e)
{
try
{
// MapEditor 실행 파일 경로 확인
string mapEditorPath = _config.MapEditorExecutablePath;
// 경로가 설정되지 않았거나 파일이 없는 경우 사용자에게 선택을 요청
if (string.IsNullOrEmpty(mapEditorPath) || !File.Exists(mapEditorPath))
{
using (var openDialog = new OpenFileDialog())
{
openDialog.Filter = "실행 파일 (*.exe)|*.exe|모든 파일 (*.*)|*.*";
openDialog.Title = "AGV MapEditor 실행 파일 선택";
openDialog.InitialDirectory = Application.StartupPath;
if (openDialog.ShowDialog() == DialogResult.OK)
{
mapEditorPath = openDialog.FileName;
// 설정에 저장
_config.MapEditorExecutablePath = mapEditorPath;
if (_config.AutoSave)
{
_config.Save();
}
}
else
{
return; // 사용자가 취소함
}
}
}
// MapEditor 실행
var startInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = mapEditorPath,
UseShellExecute = true
};
// 현재 로드된 맵 파일이 있으면 파라미터로 전달
if (!string.IsNullOrEmpty(_currentMapFilePath) && File.Exists(_currentMapFilePath))
{
startInfo.Arguments = $"\"{_currentMapFilePath}\"";
}
System.Diagnostics.Process.Start(startInfo);
_statusLabel.Text = "MapEditor 실행됨";
}
catch (Exception ex)
{
MessageBox.Show($"MapEditor를 실행할 수 없습니다:\n{ex.Message}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
} }
#endregion #endregion
private void btAllReset_Click(object sender, EventArgs e)
{
// 시뮬레이션 정지
if (_simulationState.IsRunning)
{
OnStopSimulation_Click(sender, e);
}
// AGV 초기화
_agvList.Clear();
_simulatorCanvas.AGVList = new List<IAGV>();
// 경로 초기화
_simulatorCanvas.CurrentPath = null;
// UI 업데이트
UpdateAGVComboBox();
UpdateNodeComboBoxes();
UpdateUI();
_statusLabel.Text = "초기화 완료";
}
} }
} }

View File

@@ -1,5 +1,64 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
@@ -28,9 +87,9 @@
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space" />
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
@@ -39,7 +98,7 @@
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -58,4 +117,13 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<metadata name="_menuStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="_toolStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>132, 17</value>
</metadata>
<metadata name="_statusStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>237, 17</value>
</metadata>
</root> </root>

View File

@@ -0,0 +1,114 @@
using System;
using System.IO;
using Newtonsoft.Json;
namespace AGVSimulator.Models
{
/// <summary>
/// 시뮬레이터 환경 설정 클래스
/// </summary>
public class SimulatorConfig
{
#region Properties
/// <summary>
/// MapEditor 실행 파일 경로
/// </summary>
public string MapEditorExecutablePath { get; set; } = string.Empty;
/// <summary>
/// 마지막으로 로드한 맵 파일 경로
/// </summary>
public string LastMapFilePath { get; set; } = string.Empty;
/// <summary>
/// 설정 파일 자동 저장 여부
/// </summary>
public bool AutoSave { get; set; } = true;
#endregion
#region Static Methods
/// <summary>
/// 설정 파일 기본 경로
/// </summary>
private static string ConfigFilePath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AGVSimulator",
"config.json");
/// <summary>
/// 설정을 파일에서 로드
/// </summary>
/// <returns>로드된 설정 객체</returns>
public static SimulatorConfig Load()
{
try
{
if (File.Exists(ConfigFilePath))
{
var json = File.ReadAllText(ConfigFilePath);
return JsonConvert.DeserializeObject<SimulatorConfig>(json) ?? new SimulatorConfig();
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"설정 로드 실패: {ex.Message}");
}
return new SimulatorConfig();
}
/// <summary>
/// 설정을 파일에 저장
/// </summary>
/// <param name="config">저장할 설정 객체</param>
/// <returns>저장 성공 여부</returns>
public static bool Save(SimulatorConfig config)
{
try
{
var directory = Path.GetDirectoryName(ConfigFilePath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonConvert.SerializeObject(config, Formatting.Indented);
File.WriteAllText(ConfigFilePath, json);
return true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"설정 저장 실패: {ex.Message}");
return false;
}
}
#endregion
#region Instance Methods
/// <summary>
/// 현재 설정을 저장
/// </summary>
/// <returns>저장 성공 여부</returns>
public bool Save()
{
return Save(this);
}
/// <summary>
/// MapEditor 실행 파일 경로 유효성 확인
/// </summary>
/// <returns>유효한 경로인지 여부</returns>
public bool IsMapEditorPathValid()
{
return !string.IsNullOrEmpty(MapEditorExecutablePath) &&
File.Exists(MapEditorExecutablePath);
}
#endregion
}
}

View File

@@ -3,27 +3,18 @@ using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using AGVMapEditor.Models; using AGVMapEditor.Models;
using AGVNavigationCore.Models;
using AGVNavigationCore.PathFinding;
using AGVNavigationCore.Controls;
namespace AGVSimulator.Models namespace AGVSimulator.Models
{ {
/// <summary>
/// 가상 AGV 상태
/// </summary>
public enum AGVState
{
Idle, // 대기
Moving, // 이동 중
Rotating, // 회전 중
Docking, // 도킹 중
Charging, // 충전 중
Error // 오류
}
/// <summary> /// <summary>
/// 가상 AGV 클래스 /// 가상 AGV 클래스
/// 실제 AGV의 동작을 시뮬레이션 /// 실제 AGV의 동작을 시뮬레이션
/// </summary> /// </summary>
public class VirtualAGV public class VirtualAGV : IAGV
{ {
#region Events #region Events
@@ -181,7 +172,7 @@ namespace AGVSimulator.Models
} }
_currentPath = path; _currentPath = path;
_remainingNodes = new List<string>(path.NodeSequence); _remainingNodes = new List<string>(path.Path);
_currentNodeIndex = 0; _currentNodeIndex = 0;
// 시작 노드 위치로 이동 // 시작 노드 위치로 이동

View File

@@ -0,0 +1,29 @@
@echo off
echo Building V2GDecoder VC++ Project...
REM Check if Visual Studio 2022 is installed (Professional or Community)
set MSBUILD_PRO="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
set MSBUILD_COM="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
set MSBUILD_BT="F:\(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\MSBuild.exe"
if exist %MSBUILD_PRO% (
echo "Found Visual Studio 2022 Professional"
set MSBUILD=%MSBUILD_PRO%
) else if exist %MSBUILD_COM% (
echo "Found Visual Studio 2022 Community"
set MSBUILD=%MSBUILD_COM%
) else if exist %MSBUILD_BT% (
echo "Found Visual Studio 2022 BuildTools"
set MSBUILD=%MSBUILD_BT%
) else (
echo "Visual Studio 2022 (Professional or Community) not found!"
echo "Please install Visual Studio 2022 or update the MSBuild path."
pause
exit /b 1
)
REM Build Debug x64 configuration
echo Building Debug x64 configuration...
%MSBUILD% AGVSimulator.csproj
pause

View File

@@ -1,6 +1,7 @@
# CLAUDE.md # CLAUDE.md
이 파일은 이 저장소의 코드로 작업할 때 Claude Code (claude.ai/code)를 위한 지침을 제공합니다. 이 파일은 이 저장소의 코드로 작업할 때 Claude Code (claude.ai/code)를 위한 지침을 제공합니다.
맵데이터는 C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap 파일을 기준으로 사용
## 빌드 및 개발 명령어 ## 빌드 및 개발 명령어

648
Cs_HMI/Data/NewMap.agvmap Normal file
View File

@@ -0,0 +1,648 @@
{
"Nodes": [
{
"NodeId": "N001",
"Name": "N001",
"Position": "80, 160",
"Type": 2,
"DockDirection": null,
"ConnectedNodes": [
"N002"
],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:34:44.9548285+09:00",
"ModifiedDate": "2025-09-11T11:46:18.1785633+09:00",
"Description": "TOPS-2",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "001",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N001 - TOPS-2 - [001]"
},
{
"NodeId": "N002",
"Name": "N002",
"Position": "220, 220",
"Type": 0,
"DockDirection": null,
"ConnectedNodes": [
"N003"
],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:34:48.2957516+09:00",
"ModifiedDate": "2025-09-11T11:46:20.6016371+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "002",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N002 - [002]"
},
{
"NodeId": "N003",
"Name": "N003",
"Position": "300, 280",
"Type": 0,
"DockDirection": null,
"ConnectedNodes": [
"N004"
],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:34:49.2226656+09:00",
"ModifiedDate": "2025-09-11T11:46:23.2433989+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "003",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N003 - [003]"
},
{
"NodeId": "N004",
"Name": "N004",
"Position": "380, 340",
"Type": 1,
"DockDirection": null,
"ConnectedNodes": [
"N008",
"N011",
"N022"
],
"CanRotate": true,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:34:50.1681027+09:00",
"ModifiedDate": "2025-09-11T11:46:24.8122488+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "004",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N004 - [004]"
},
{
"NodeId": "N006",
"Name": "N006",
"Position": "520, 220",
"Type": 0,
"DockDirection": null,
"ConnectedNodes": [
"N007"
],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:34:51.1111368+09:00",
"ModifiedDate": "2025-09-11T11:46:41.6764551+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "013",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N006 - [013]"
},
{
"NodeId": "N007",
"Name": "N007",
"Position": "600, 180",
"Type": 0,
"DockDirection": null,
"ConnectedNodes": [],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:34:51.9266982+09:00",
"ModifiedDate": "2025-09-11T11:46:43.5813583+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "014",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N007 - [014]"
},
{
"NodeId": "N008",
"Name": "N008",
"Position": "340, 420",
"Type": 0,
"DockDirection": null,
"ConnectedNodes": [
"N009"
],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:34:53.9595825+09:00",
"ModifiedDate": "2025-09-11T11:46:33.4490347+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "009",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N008 - [009]"
},
{
"NodeId": "N009",
"Name": "N009",
"Position": "280, 480",
"Type": 0,
"DockDirection": null,
"ConnectedNodes": [
"N010"
],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:34:54.5035702+09:00",
"ModifiedDate": "2025-09-11T11:46:35.2267268+09:00",
"Description": "SSTRON",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "010",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N009 - SSTRON - [010]"
},
{
"NodeId": "N010",
"Name": "N010",
"Position": "180, 540",
"Type": 2,
"DockDirection": null,
"ConnectedNodes": [],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:34:55.0563237+09:00",
"ModifiedDate": "2025-09-11T11:46:37.2974468+09:00",
"Description": "TOPS-1",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "011",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N010 - TOPS-1 - [011]"
},
{
"NodeId": "N011",
"Name": "N011",
"Position": "460, 420",
"Type": 0,
"DockDirection": null,
"ConnectedNodes": [
"N012"
],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:34:55.8875335+09:00",
"ModifiedDate": "2025-09-11T11:46:26.5275006+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "005",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N011 - [005]"
},
{
"NodeId": "N012",
"Name": "N012",
"Position": "540, 480",
"Type": 0,
"DockDirection": null,
"ConnectedNodes": [
"N013"
],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:34:56.3678144+09:00",
"ModifiedDate": "2025-09-11T11:46:27.9224943+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "006",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N012 - [006]"
},
{
"NodeId": "N013",
"Name": "N013",
"Position": "620, 520",
"Type": 0,
"DockDirection": null,
"ConnectedNodes": [
"N014"
],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:34:56.8390845+09:00",
"ModifiedDate": "2025-09-11T11:46:29.5788308+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "007",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N013 - [007]"
},
{
"NodeId": "N014",
"Name": "N014",
"Position": "720, 580",
"Type": 2,
"DockDirection": null,
"ConnectedNodes": [],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:34:57.2549726+09:00",
"ModifiedDate": "2025-09-11T11:46:31.1919274+09:00",
"Description": "SS-TRON",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "008",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N014 - SS-TRON - [008]"
},
{
"NodeId": "N019",
"Name": "N019",
"Position": "679, 199",
"Type": 3,
"DockDirection": null,
"ConnectedNodes": [
"N007"
],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:35:56.5359098+09:00",
"ModifiedDate": "2025-09-11T11:46:45.6967709+09:00",
"Description": "Charger",
"IsActive": true,
"DisplayColor": "Red",
"RfidId": "015",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N019 - Charger - [015]"
},
{
"NodeId": "N022",
"Name": "N022",
"Position": "459, 279",
"Type": 0,
"DockDirection": null,
"ConnectedNodes": [
"N006"
],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T08:36:48.0311551+09:00",
"ModifiedDate": "2025-09-11T11:46:39.7262145+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "012",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N022 - [012]"
},
{
"NodeId": "N023",
"Name": "N023",
"Position": "440, 220",
"Type": 0,
"DockDirection": null,
"ConnectedNodes": [
"N024",
"N004"
],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T09:41:36.8738794+09:00",
"ModifiedDate": "2025-09-11T11:46:47.8868788+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "016",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N023 - [016]"
},
{
"NodeId": "N024",
"Name": "N024",
"Position": "500, 160",
"Type": 0,
"DockDirection": null,
"ConnectedNodes": [
"N025"
],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T09:41:37.4551853+09:00",
"ModifiedDate": "2025-09-11T11:46:51.7183934+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "017",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N024 - [017]"
},
{
"NodeId": "N025",
"Name": "N025",
"Position": "600, 120",
"Type": 0,
"DockDirection": null,
"ConnectedNodes": [
"N026"
],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T09:41:38.0142374+09:00",
"ModifiedDate": "2025-09-11T11:46:54.3289018+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "018",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N025 - [018]"
},
{
"NodeId": "N026",
"Name": "N026",
"Position": "660, 100",
"Type": 3,
"DockDirection": null,
"ConnectedNodes": [],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T09:41:38.5834487+09:00",
"ModifiedDate": "2025-09-11T11:46:57.0288799+09:00",
"Description": "Charger",
"IsActive": true,
"DisplayColor": "Blue",
"RfidId": "019",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "N026 - Charger - [019]"
},
{
"NodeId": "LBL001",
"Name": "Amkor Technology Korea",
"Position": "160, 80",
"Type": 4,
"DockDirection": null,
"ConnectedNodes": [],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T11:08:22.4048927+09:00",
"ModifiedDate": "2025-09-11T11:08:22.4048927+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Purple",
"RfidId": "",
"LabelText": "Amkor Technology Korea",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "LBL001"
},
{
"NodeId": "IMG001",
"Name": "logo",
"Position": "700, 320",
"Type": 5,
"DockDirection": null,
"ConnectedNodes": [],
"CanRotate": false,
"StationId": "",
"StationType": null,
"CreatedDate": "2025-09-11T11:08:44.7897541+09:00",
"ModifiedDate": "2025-09-11T11:08:44.7897541+09:00",
"Description": "",
"IsActive": true,
"DisplayColor": "Brown",
"RfidId": "",
"LabelText": "",
"FontFamily": "Arial",
"FontSize": 12.0,
"FontStyle": 0,
"ForeColor": "Black",
"BackColor": "Transparent",
"ShowBackground": false,
"ImagePath": "C:\\Data\\Users\\Pictures\\logo.png",
"Scale": "1, 1",
"Opacity": 1.0,
"Rotation": 0.0,
"DisplayText": "IMG001"
}
],
"CreatedDate": "2025-09-11T11:46:57.8091998+09:00",
"Version": "1.0"
}