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" />
</ItemGroup>
<ItemGroup>
<Compile Include="Models\MapNode.cs" />
<Compile Include="Models\RfidMapping.cs" />
<ProjectReference Include="..\AGVNavigationCore\AGVNavigationCore.csproj">
<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\Enums.cs" />
<Compile Include="Models\PathNode.cs" />
<Compile Include="Models\PathResult.cs" />
<Compile Include="Models\PathCalculator.cs" />
<Compile Include="Forms\MainForm.cs">
<SubType>Form</SubType>
@@ -56,12 +61,6 @@
<Compile Include="Forms\MainForm.Designer.cs">
<DependentUpon>MainForm.cs</DependentUpon>
</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="Properties\AssemblyInfo.cs" />
</ItemGroup>
@@ -69,11 +68,9 @@
<EmbeddedResource Include="Forms\MainForm.resx">
<DependentUpon>MainForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Controls\MapCanvas.resx">
<DependentUpon>MapCanvas.cs</DependentUpon>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Include="build.bat" />
<None Include="packages.config" />
</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.saveToolStripMenuItem = 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.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
@@ -42,20 +43,9 @@ namespace AGVMapEditor.Forms
this.splitContainer1 = new System.Windows.Forms.SplitContainer();
this.tabControl1 = new System.Windows.Forms.TabControl();
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._propertyGrid = new System.Windows.Forms.PropertyGrid();
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.statusStrip1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
@@ -63,8 +53,6 @@ namespace AGVMapEditor.Forms
this.splitContainer1.SuspendLayout();
this.tabControl1.SuspendLayout();
this.tabPageNodes.SuspendLayout();
this.tabPageRfid.SuspendLayout();
this.tabPageProperties.SuspendLayout();
this.SuspendLayout();
//
// menuStrip1
@@ -82,6 +70,7 @@ namespace AGVMapEditor.Forms
this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.newToolStripMenuItem,
this.openToolStripMenuItem,
this.closeToolStripMenuItem,
this.toolStripSeparator1,
this.saveToolStripMenuItem,
this.saveAsToolStripMenuItem,
@@ -95,7 +84,7 @@ namespace AGVMapEditor.Forms
//
this.newToolStripMenuItem.Name = "newToolStripMenuItem";
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.Click += new System.EventHandler(this.newToolStripMenuItem_Click);
//
@@ -103,39 +92,46 @@ namespace AGVMapEditor.Forms
//
this.openToolStripMenuItem.Name = "openToolStripMenuItem";
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.Click += new System.EventHandler(this.openToolStripMenuItem_Click);
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(177, 6);
this.toolStripSeparator1.Size = new System.Drawing.Size(195, 6);
//
// saveToolStripMenuItem
//
this.saveToolStripMenuItem.Name = "saveToolStripMenuItem";
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.Click += new System.EventHandler(this.saveToolStripMenuItem_Click);
//
// 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.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
//
this.toolStripSeparator2.Name = "toolStripSeparator2";
this.toolStripSeparator2.Size = new System.Drawing.Size(177, 6);
this.toolStripSeparator2.Size = new System.Drawing.Size(195, 6);
//
// 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.Click += new System.EventHandler(this.exitToolStripMenuItem_Click);
//
@@ -172,8 +168,6 @@ namespace AGVMapEditor.Forms
// tabControl1
//
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.Location = new System.Drawing.Point(0, 0);
this.tabControl1.Name = "tabControl1";
@@ -183,11 +177,8 @@ namespace AGVMapEditor.Forms
//
// 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._propertyGrid);
this.tabPageNodes.Controls.Add(this.label1);
this.tabPageNodes.Location = new System.Drawing.Point(4, 22);
this.tabPageNodes.Name = "tabPageNodes";
@@ -197,163 +188,33 @@ namespace AGVMapEditor.Forms
this.tabPageNodes.Text = "노드 관리";
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
//
this.listBoxNodes.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.listBoxNodes.Dock = System.Windows.Forms.DockStyle.Fill;
this.listBoxNodes.FormattingEnabled = true;
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.Size = new System.Drawing.Size(280, 568);
this.listBoxNodes.Size = new System.Drawing.Size(286, 245);
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
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(6, 6);
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.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
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
@@ -378,11 +239,6 @@ namespace AGVMapEditor.Forms
this.tabControl1.ResumeLayout(false);
this.tabPageNodes.ResumeLayout(false);
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.PerformLayout();
@@ -397,6 +253,7 @@ namespace AGVMapEditor.Forms
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripMenuItem saveToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem saveAsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem closeToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
private System.Windows.Forms.StatusStrip statusStrip1;
@@ -404,19 +261,8 @@ namespace AGVMapEditor.Forms
private System.Windows.Forms.SplitContainer splitContainer1;
private System.Windows.Forms.TabControl tabControl1;
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.Label label1;
private System.Windows.Forms.Button btnDeleteRfidMapping;
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;
private System.Windows.Forms.PropertyGrid _propertyGrid;
}
}

View File

@@ -5,7 +5,8 @@ using System.IO;
using System.Linq;
using System.Windows.Forms;
using AGVMapEditor.Models;
using AGVMapEditor.Controls;
using AGVNavigationCore.Controls;
using AGVNavigationCore.Models;
using Newtonsoft.Json;
namespace AGVMapEditor.Forms
@@ -17,28 +18,46 @@ namespace AGVMapEditor.Forms
{
#region Fields
private NodeResolver _nodeResolver;
private List<MapNode> _mapNodes;
private List<RfidMapping> _rfidMappings;
private MapCanvas _mapCanvas;
private UnifiedAGVCanvas _mapCanvas;
// 현재 선택된 노드
private MapNode _selectedNode;
// 파일 경로
private string _currentMapFile = string.Empty;
private bool _hasChanges = false;
#endregion
#region Constructor
public MainForm()
public MainForm() : this(null)
{
}
public MainForm(string[] args)
{
InitializeComponent();
InitializeData();
InitializeMapCanvas();
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
@@ -48,20 +67,117 @@ namespace AGVMapEditor.Forms
private void InitializeData()
{
_mapNodes = new List<MapNode>();
_rfidMappings = new List<RfidMapping>();
_nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
}
private void InitializeMapCanvas()
{
_mapCanvas = new MapCanvas(_mapNodes);
_mapCanvas = new UnifiedAGVCanvas();
_mapCanvas.Dock = DockStyle.Fill;
_mapCanvas.Mode = UnifiedAGVCanvas.CanvasMode.Edit;
_mapCanvas.Nodes = _mapNodes;
// RfidMappings 제거 - MapNode에 통합됨
// 이벤트 연결
_mapCanvas.NodeAdded += OnNodeAdded;
_mapCanvas.NodeSelected += OnNodeSelected;
_mapCanvas.NodeMoved += OnNodeMoved;
_mapCanvas.BackgroundClicked += OnBackgroundClicked;
_mapCanvas.NodeDeleted += OnNodeDeleted;
_mapCanvas.MapChanged += OnMapChanged;
// 스플리터 패널에 맵 캔버스 추가
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
@@ -71,7 +187,16 @@ namespace AGVMapEditor.Forms
private void MainForm_Load(object sender, EventArgs e)
{
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)
@@ -87,6 +212,28 @@ namespace AGVMapEditor.Forms
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)
{
_selectedNode = null;
@@ -123,6 +270,11 @@ namespace AGVMapEditor.Forms
SaveAsMap();
}
private void closeToolStripMenuItem_Click(object sender, EventArgs e)
{
CloseMap();
}
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{
this.Close();
@@ -152,16 +304,6 @@ namespace AGVMapEditor.Forms
RemoveConnectionFromSelectedNode();
}
private void btnAddRfidMapping_Click(object sender, EventArgs e)
{
AddNewRfidMapping();
}
private void btnDeleteRfidMapping_Click(object sender, EventArgs e)
{
DeleteSelectedRfidMapping();
}
#endregion
#region Node Management
@@ -171,12 +313,12 @@ namespace AGVMapEditor.Forms
var nodeId = GenerateNodeId();
var nodeName = $"노드{_mapNodes.Count + 1}";
var position = new Point(100 + _mapNodes.Count * 50, 100 + _mapNodes.Count * 50);
var node = new MapNode(nodeId, nodeName, position, NodeType.Normal);
_mapNodes.Add(node);
_hasChanges = true;
RefreshNodeList();
RefreshMapCanvas();
UpdateTitle();
@@ -190,17 +332,17 @@ namespace AGVMapEditor.Forms
return;
}
var result = MessageBox.Show($"노드 '{_selectedNode.Name}'를 삭제하시겠습니까?\n연결된 RFID 매핑도 함께 삭제됩니다.",
var result = MessageBox.Show($"노드 '{_selectedNode.Name}'를 삭제하시겠습니까?\n연결된 RFID 매핑도 함께 삭제됩니다.",
"삭제 확인", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
_nodeResolver.RemoveMapNode(_selectedNode.NodeId);
// 노드 제거
_mapNodes.Remove(_selectedNode);
_selectedNode = null;
_hasChanges = true;
RefreshNodeList();
RefreshRfidMappingList();
RefreshMapCanvas();
ClearNodeProperties();
UpdateTitle();
@@ -216,9 +358,9 @@ namespace AGVMapEditor.Forms
}
// 다른 노드들 중에서 선택
var availableNodes = _mapNodes.Where(n => n.NodeId != _selectedNode.NodeId &&
var availableNodes = _mapNodes.Where(n => n.NodeId != _selectedNode.NodeId &&
!_selectedNode.ConnectedNodes.Contains(n.NodeId)).ToList();
if (availableNodes.Count == 0)
{
MessageBox.Show("연결 가능한 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
@@ -228,7 +370,7 @@ namespace AGVMapEditor.Forms
// 간단한 선택 다이얼로그 (실제로는 별도 폼을 만들어야 함)
var nodeNames = availableNodes.Select(n => $"{n.NodeId}: {n.Name}").ToArray();
var input = Microsoft.VisualBasic.Interaction.InputBox("연결할 노드를 선택하세요:", "노드 연결", nodeNames[0]);
var targetNode = availableNodes.FirstOrDefault(n => input.StartsWith(n.NodeId));
if (targetNode != null)
{
@@ -249,14 +391,14 @@ namespace AGVMapEditor.Forms
}
// 연결된 노드들 중에서 선택
var connectedNodeNames = _selectedNode.ConnectedNodes.Select(connectedNodeId =>
var connectedNodeNames = _selectedNode.ConnectedNodes.Select(connectedNodeId =>
{
var node = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
return node != null ? $"{node.NodeId}: {node.Name}" : connectedNodeId;
}).ToArray();
var input = Microsoft.VisualBasic.Interaction.InputBox("제거할 연결을 선택하세요:", "연결 제거", connectedNodeNames[0]);
var targetNodeId = input.Split(':')[0];
if (_selectedNode.ConnectedNodes.Contains(targetNodeId))
{
@@ -272,110 +414,50 @@ namespace AGVMapEditor.Forms
{
int counter = 1;
string nodeId;
do
{
nodeId = $"N{counter:D3}";
counter++;
} while (_mapNodes.Any(n => n.NodeId == nodeId));
return nodeId;
}
#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
private void NewMap()
{
_mapNodes.Clear();
_rfidMappings.Clear();
_nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
_selectedNode = null;
_currentMapFile = string.Empty;
_hasChanges = false;
RefreshAll();
UpdateTitle();
}
private void CloseMap()
{
if (CheckSaveChanges())
{
_mapNodes.Clear();
_selectedNode = null;
_currentMapFile = string.Empty;
_hasChanges = false;
RefreshAll();
UpdateTitle();
}
}
private void OpenMap()
{
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"
};
@@ -388,7 +470,6 @@ namespace AGVMapEditor.Forms
_hasChanges = false;
RefreshAll();
UpdateTitle();
MessageBox.Show("맵이 성공적으로 로드되었습니다.", "성공", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
@@ -423,7 +504,7 @@ namespace AGVMapEditor.Forms
{
var saveFileDialog = new SaveFileDialog
{
Filter = "AGV Map Files (*.agvmap)|*.agvmap|JSON Files (*.json)|*.json",
Filter = "AGV Map Files (*.agvmap)|*.agvmap",
DefaultExt = "agvmap",
FileName = "NewMap.agvmap"
};
@@ -447,35 +528,48 @@ namespace AGVMapEditor.Forms
private void LoadMapFromFile(string filePath)
{
var json = File.ReadAllText(filePath);
var mapData = JsonConvert.DeserializeObject<MapData>(json);
var result = MapLoader.LoadMapFromFile(filePath);
_mapNodes = mapData.Nodes ?? new List<MapNode>();
_rfidMappings = mapData.RfidMappings ?? new List<RfidMapping>();
_nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
if (result.Success)
{
_mapNodes = result.Nodes;
// 맵 캔버스에 데이터 설정
_mapCanvas.Nodes = _mapNodes;
// RfidMappings 제거됨 - MapNode에 통합
}
else
{
MessageBox.Show($"맵 파일 로딩 실패: {result.ErrorMessage}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void SaveMapToFile(string filePath)
{
var mapData = new MapData
if (!MapLoader.SaveMapToFile(filePath, _mapNodes))
{
Nodes = _mapNodes,
RfidMappings = _rfidMappings,
CreatedDate = DateTime.Now,
Version = "1.0"
};
var json = JsonConvert.SerializeObject(mapData, Formatting.Indented);
File.WriteAllText(filePath, json);
MessageBox.Show("맵 파일 저장 실패", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// RFID 매핑 업데이트 (공용 MapLoader 사용)
/// </summary>
private void UpdateRfidMappings()
{
// 네비게이션 노드들에 RFID 자동 할당
MapLoader.AssignAutoRfidIds(_mapNodes);
}
private bool CheckSaveChanges()
{
if (_hasChanges)
{
var result = MessageBox.Show("변경사항이 있습니다. 저장하시겠습니까?", "변경사항 저장",
var result = MessageBox.Show("변경사항이 있습니다. 저장하시겠습니까?", "변경사항 저장",
MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
SaveMap();
@@ -486,7 +580,7 @@ namespace AGVMapEditor.Forms
return false;
}
}
return true;
}
@@ -497,7 +591,6 @@ namespace AGVMapEditor.Forms
private void RefreshAll()
{
RefreshNodeList();
RefreshRfidMappingList();
RefreshMapCanvas();
ClearNodeProperties();
}
@@ -506,15 +599,99 @@ namespace AGVMapEditor.Forms
{
listBoxNodes.DataSource = null;
listBoxNodes.DataSource = _mapNodes;
listBoxNodes.DisplayMember = "Name";
listBoxNodes.DisplayMember = "DisplayText";
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;
listBoxRfidMappings.DataSource = _rfidMappings;
listBoxRfidMappings.DisplayMember = "ToString";
if (listBoxNodes.SelectedItem is MapNode selectedNode)
{
_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()
@@ -530,30 +707,31 @@ namespace AGVMapEditor.Forms
return;
}
// 선택된 노드의 속성을 프로퍼티 패널에 표시
// (실제로는 PropertyGrid나 별도 컨트롤 사용)
labelSelectedNode.Text = $"선택된 노드: {_selectedNode.Name} ({_selectedNode.NodeId})";
// 노드 래퍼 객체 생성 (타입에 따라 다른 래퍼 사용)
var nodeWrapper = NodePropertyWrapperFactory.CreateWrapper(_selectedNode, _mapNodes);
_propertyGrid.SelectedObject = nodeWrapper;
_propertyGrid.Focus();
}
private void ClearNodeProperties()
{
labelSelectedNode.Text = "선택된 노드: 없음";
_propertyGrid.SelectedObject = null;
}
private void UpdateTitle()
{
var title = "AGV Map Editor";
if (!string.IsNullOrEmpty(_currentMapFile))
{
title += $" - {Path.GetFileName(_currentMapFile)}";
}
if (_hasChanges)
{
title += " *";
}
this.Text = title;
}
@@ -571,16 +749,38 @@ namespace AGVMapEditor.Forms
#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>();
public DateTime CreatedDate { get; set; }
public string Version { get; set; } = "1.0";
// 속성이 변경되었을 때 자동으로 변경사항 표시
_hasChanges = true;
UpdateTitle();
// 현재 선택된 노드를 기억
var currentSelectedNode = _selectedNode;
RefreshNodeList();
RefreshMapCanvas();
// 선택된 노드를 다시 선택
if (currentSelectedNode != null)
{
var nodeIndex = _mapNodes.IndexOf(currentSelectedNode);
if (nodeIndex >= 0)
{
listBoxNodes.SelectedIndex = nodeIndex;
}
}
}
#endregion
#region Data Model for Serialization
#endregion
}
}

View File

@@ -1,5 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<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:import namespace="http://www.w3.org/XML/1998/namespace" />
<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="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 name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
@@ -39,7 +98,7 @@
<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:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>

View File

@@ -14,7 +14,11 @@ namespace AGVMapEditor.Models
/// <summary>도킹 스테이션</summary>
Docking,
/// <summary>충전 스테이션</summary>
Charging
Charging,
/// <summary>라벨 (UI 요소)</summary>
Label,
/// <summary>이미지 (UI 요소)</summary>
Image
}
/// <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>
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>
@@ -130,6 +196,12 @@ namespace AGVMapEditor.Models
case NodeType.Charging:
DisplayColor = Color.Red;
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})";
}
/// <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>
@@ -217,9 +312,111 @@ namespace AGVMapEditor.Models
ModifiedDate = ModifiedDate,
Description = Description,
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;
}
/// <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.Collections.Generic;
using System.Linq;
using AGVNavigationCore.Models;
namespace AGVMapEditor.Models
{

View File

@@ -3,467 +3,266 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using AGVNavigationCore.Models;
using AGVNavigationCore.PathFinding;
namespace AGVMapEditor.Models
{
/// <summary>
/// AGV 전용 경로 계산기 (A* 알고리즘 기반)
/// AGV의 방향성, 도킹 제약, 회전 제약을 고려한 경로 계산
/// AGV 전용 경로 계산기 (AGVNavigationCore 래퍼)
/// AGVMapEditor와 AGVNavigationCore 간의 호환성 제공
/// RFID 기반 경로 계산을 우선 사용
/// </summary>
public class PathCalculator
{
#region Constants
private const float BASE_MOVE_COST = 1.0f; // 기본 이동 비용
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
private AGVPathfinder _agvPathfinder;
private AStarPathfinder _astarPathfinder;
private RfidBasedPathfinder _rfidPathfinder;
/// <summary>
/// 생성자
/// </summary>
/// <param name="mapNodes">맵 노드 목록</param>
/// <param name="nodeResolver">노드 해결기</param>
public PathCalculator(List<MapNode> mapNodes, NodeResolver nodeResolver)
public PathCalculator()
{
_mapNodes = mapNodes ?? throw new ArgumentNullException(nameof(mapNodes));
_nodeResolver = nodeResolver ?? throw new ArgumentNullException(nameof(nodeResolver));
_agvPathfinder = new AGVPathfinder();
_astarPathfinder = new AStarPathfinder();
_rfidPathfinder = new RfidBasedPathfinder();
}
#endregion
#region Public Methods
/// <summary>
/// 맵 노드 설정
/// </summary>
/// <param name="mapNodes">맵 노드 목록</param>
public void SetMapNodes(List<MapNode> mapNodes)
{
_agvPathfinder.SetMapNodes(mapNodes);
_astarPathfinder.SetMapNodes(mapNodes);
}
/// <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>
/// <param name="startNodeId">시작 노드 ID</param>
/// <param name="targetNodeId">목 노드 ID</param>
/// <param name="currentDirection">현재 AGV 방향</param>
/// <param name="endNodeId">목적지 노드 ID</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>
public PathResult CalculatePath(string startNodeId, string targetNodeId, AgvDirection currentDirection)
public PathResult FindNearestPath(string startNodeId, List<string> targetNodeIds)
{
var stopwatch = Stopwatch.StartNew();
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
};
}
return _astarPathfinder.FindNearestPath(startNodeId, targetNodeIds);
}
/// <summary>
/// 경로 유효성 검증 (RFID 이탈 감지시 사용)
/// 두 노드가 연결되어 있는지 확인
/// </summary>
/// <param name="currentPath">현재 경로</param>
/// <param name="currentRfidId">현재 감지된 RFID</param>
/// <returns>경로 유효성 여부</returns>
public bool ValidateCurrentPath(PathResult currentPath, string currentRfidId)
/// <param name="nodeId1">노드 1 ID</param>
/// <param name="nodeId2">노드 2 ID</param>
/// <returns>연결 여부</returns>
public bool AreNodesConnected(string nodeId1, string nodeId2)
{
if (currentPath == null || !currentPath.Success)
return false;
var currentNode = _nodeResolver.GetNodeByRfid(currentRfidId);
if (currentNode == null)
return false;
// 현재 노드가 계획된 경로에 포함되어 있는지 확인
return currentPath.NodeSequence.Contains(currentNode.NodeId);
return _astarPathfinder.AreNodesConnected(nodeId1, nodeId2);
}
/// <summary>
/// 동적 경로 재계산 (경로 이탈시 사용)
/// 경로 유효성 검증
/// </summary>
/// <param name="currentRfidId">현재 RFID 위치</param>
/// <param name="targetNodeId">목표 노드 ID</param>
/// <param name="currentDirection">현재 방향</param>
/// <param name="originalPath">원래 경로 (참고용)</param>
/// <returns>새로운 경로</returns>
public PathResult RecalculatePath(string currentRfidId, string targetNodeId,
AgvDirection currentDirection, PathResult originalPath = null)
/// <param name="path">검증할 경로</param>
/// <returns>유효성 검증 결과</returns>
public bool ValidatePath(List<string> path)
{
var currentNode = _nodeResolver.GetNodeByRfid(currentRfidId);
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("경로를 찾을 수 없습니다.");
return _agvPathfinder.ValidatePath(path);
}
/// <summary>
/// 인접 노드들 처리
/// 네비게이션 가능한 노드 목록 반환
/// </summary>
private void ProcessNeighbors(PathNode current, string targetNodeId,
SortedSet<PathNode> openSet, HashSet<string> closedSet,
Dictionary<string, float> gScore)
/// <returns>노드 ID 목록</returns>
public List<string> GetNavigationNodes()
{
var currentMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == current.NodeId);
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);
}
}
}
return _astarPathfinder.GetNavigationNodes();
}
/// <summary>
/// 가능한 방향들 계산
/// AGV 현재 방향 설정
/// </summary>
private List<AgvDirection> GetPossibleDirections(PathNode current, MapNode neighborNode)
/// <param name="direction">현재 방향</param>
public void SetCurrentDirection(AgvDirection direction)
{
var directions = new List<AgvDirection>();
// 기본적으로 전진/후진 가능
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;
_agvPathfinder.CurrentDirection = direction;
}
/// <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;
// 방향 전환 비용
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;
_agvPathfinder.RotationCostWeight = weight;
}
/// <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);
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; // 좌표 단위 조정
_astarPathfinder.HeuristicWeight = weight;
}
/// <summary>
/// 두 점 사이의 거리 계산
/// 최대 탐색 노드 수 설정
/// </summary>
private float CalculateDistance(Point from, Point to)
/// <param name="maxNodes">최대 탐색 노드 수</param>
public void SetMaxSearchNodes(int maxNodes)
{
var dx = to.X - from.X;
var dy = to.Y - from.Y;
return (float)Math.Sqrt(dx * dx + dy * dy);
_astarPathfinder.MaxSearchNodes = maxNodes;
}
// ==================== 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>
/// 회전 가능한 위치인지 확인
/// RFID 기반 충전소 경로 찾기
/// </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 node != null && (node.CanRotate || node.Type == NodeType.Rotation);
return _rfidPathfinder.FindPathToChargingStation(startRfidId);
}
/// <summary>
/// 도킹 접근 방향이 유효한지 확인
/// RFID 기반 도킹 스테이션 경로 찾기
/// </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);
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;
return _rfidPathfinder.FindPathToDockingStation(startRfidId, stationType);
}
/// <summary>
/// 이동 명령 시퀀스 생성
/// 여러 RFID 목적지 중 가장 가까운 곳으로의 경로 찾기
/// </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>();
// 방향 전환이 필요한 경우
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;
return _rfidPathfinder.FindNearestPath(startRfidId, targetRfidIds);
}
/// <summary>
/// 경로 재구성
/// RFID 매핑 정보 조회 (MapNode 반환)
/// </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>();
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);
return _rfidPathfinder.GetRfidMapping(rfidId);
}
/// <summary>
/// 특정 노드에서 가능한 다음 노드들 조회
/// RFID로 NodeId 조회
/// </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);
if (currentNode == null)
return new List<string>();
return currentNode.ConnectedNodes.ToList();
return _rfidPathfinder.GetNodeIdByRfid(rfidId);
}
/// <summary>
/// 경로 최적화 (선택적 기능)
/// NodeId로 RFID 조회
/// </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 originalPath;
// TODO: 경로 최적화 로직 구현
// - 불필요한 중간 정지점 제거
// - 회전 최소화
// - 경로 단순화
return originalPath;
return _rfidPathfinder.GetRfidByNodeId(nodeId);
}
#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 AGVNavigationCore.Models;
namespace AGVMapEditor.Models
{

View File

@@ -13,14 +13,14 @@ namespace AGVMapEditor
/// 애플리케이션의 기본 진입점입니다.
/// </summary>
[STAThread]
static void Main()
static void Main(string[] args)
{
// Windows Forms 애플리케이션 초기화
Application.EnableVisualStyles();
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