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:
@@ -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>
|
||||
|
||||
59
Cs_HMI/AGVMapEditor/Controls/MapCanvas.Designer.cs
generated
59
Cs_HMI/AGVMapEditor/Controls/MapCanvas.Designer.cs
generated
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
34
Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.Designer.cs
generated
Normal file
34
Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
1358
Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.cs
Normal file
1358
Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.cs
Normal file
File diff suppressed because it is too large
Load Diff
218
Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs
generated
218
Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs
generated
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -14,7 +14,11 @@ namespace AGVMapEditor.Models
|
||||
/// <summary>도킹 스테이션</summary>
|
||||
Docking,
|
||||
/// <summary>충전 스테이션</summary>
|
||||
Charging
|
||||
Charging,
|
||||
/// <summary>라벨 (UI 요소)</summary>
|
||||
Label,
|
||||
/// <summary>이미지 (UI 요소)</summary>
|
||||
Image
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
15
Cs_HMI/AGVMapEditor/Models/MapData.cs
Normal file
15
Cs_HMI/AGVMapEditor/Models/MapData.cs
Normal 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";
|
||||
}
|
||||
|
||||
}
|
||||
210
Cs_HMI/AGVMapEditor/Models/MapImage.cs
Normal file
210
Cs_HMI/AGVMapEditor/Models/MapImage.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
125
Cs_HMI/AGVMapEditor/Models/MapLabel.cs
Normal file
125
Cs_HMI/AGVMapEditor/Models/MapLabel.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
499
Cs_HMI/AGVMapEditor/Models/NodePropertyWrapper.cs
Normal file
499
Cs_HMI/AGVMapEditor/Models/NodePropertyWrapper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Cs_HMI/AGVMapEditor/build.bat
Normal file
29
Cs_HMI/AGVMapEditor/build.bat
Normal 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
|
||||
101
Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj
Normal file
101
Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj
Normal file
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>AGVNavigationCore</RootNamespace>
|
||||
<AssemblyName>AGVNavigationCore</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\x86\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<DebugType>full</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
|
||||
<OutputPath>bin\x86\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Models\Enums.cs" />
|
||||
<Compile Include="Models\MapLoader.cs" />
|
||||
<Compile Include="Models\MapNode.cs" />
|
||||
<Compile Include="Models\RfidMapping.cs" />
|
||||
<Compile Include="PathFinding\PathNode.cs" />
|
||||
<Compile Include="PathFinding\PathResult.cs" />
|
||||
<Compile Include="PathFinding\AStarPathfinder.cs" />
|
||||
<Compile Include="PathFinding\AGVPathfinder.cs" />
|
||||
<Compile Include="PathFinding\AGVPathResult.cs" />
|
||||
<Compile Include="PathFinding\RfidBasedPathfinder.cs" />
|
||||
<Compile Include="PathFinding\RfidPathResult.cs" />
|
||||
<Compile Include="Controls\UnifiedAGVCanvas.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Controls\UnifiedAGVCanvas.Designer.cs">
|
||||
<DependentUpon>UnifiedAGVCanvas.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\UnifiedAGVCanvas.Events.cs">
|
||||
<DependentUpon>UnifiedAGVCanvas.cs</DependentUpon>
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Controls\UnifiedAGVCanvas.Mouse.cs">
|
||||
<DependentUpon>UnifiedAGVCanvas.cs</DependentUpon>
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Utils\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="build.bat" />
|
||||
<None Include="packages.config" />
|
||||
<None Include="README.md" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
41
Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Designer.cs
generated
Normal file
41
Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Designer.cs
generated
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace AGVNavigationCore.Controls
|
||||
{
|
||||
partial class UnifiedAGVCanvas
|
||||
{
|
||||
/// <summary>
|
||||
/// 필수 디자이너 변수입니다.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
|
||||
#region 구성 요소 디자이너에서 생성한 코드
|
||||
|
||||
/// <summary>
|
||||
/// 디자이너 지원에 필요한 메서드입니다.
|
||||
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// UnifiedAGVCanvas
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.BackColor = System.Drawing.Color.White;
|
||||
this.Name = "UnifiedAGVCanvas";
|
||||
this.Size = new System.Drawing.Size(800, 600);
|
||||
this.Paint += new System.Windows.Forms.PaintEventHandler(this.UnifiedAGVCanvas_Paint);
|
||||
this.MouseClick += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseClick);
|
||||
this.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseDoubleClick);
|
||||
this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseDown);
|
||||
this.MouseMove += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseMove);
|
||||
this.MouseUp += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseUp);
|
||||
this.MouseWheel += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseWheel);
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
801
Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs
Normal file
801
Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs
Normal file
@@ -0,0 +1,801 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding;
|
||||
|
||||
namespace AGVNavigationCore.Controls
|
||||
{
|
||||
public partial class UnifiedAGVCanvas
|
||||
{
|
||||
#region Paint Events
|
||||
|
||||
private void UnifiedAGVCanvas_Paint(object sender, PaintEventArgs e)
|
||||
{
|
||||
var g = e.Graphics;
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
g.InterpolationMode = InterpolationMode.High;
|
||||
|
||||
// 변환 행렬 설정 (줌 및 팬)
|
||||
var transform = new Matrix();
|
||||
transform.Scale(_zoomFactor, _zoomFactor);
|
||||
transform.Translate(_panOffset.X, _panOffset.Y);
|
||||
g.Transform = transform;
|
||||
|
||||
try
|
||||
{
|
||||
// 그리드 그리기
|
||||
if (_showGrid)
|
||||
{
|
||||
DrawGrid(g);
|
||||
}
|
||||
|
||||
// 노드 연결선 그리기
|
||||
DrawConnections(g);
|
||||
|
||||
// 경로 그리기
|
||||
DrawPaths(g);
|
||||
|
||||
// 노드 그리기
|
||||
DrawNodes(g);
|
||||
|
||||
// AGV 그리기
|
||||
DrawAGVs(g);
|
||||
|
||||
// 임시 연결선 그리기 (편집 모드)
|
||||
if (_canvasMode == CanvasMode.Edit && _isConnectionMode)
|
||||
{
|
||||
DrawTemporaryConnection(g);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
g.Transform = new Matrix(); // 변환 행렬 리셋
|
||||
}
|
||||
|
||||
// UI 정보 그리기 (변환 없이)
|
||||
DrawUIInfo(g);
|
||||
}
|
||||
|
||||
private void DrawGrid(Graphics g)
|
||||
{
|
||||
if (!_showGrid) return;
|
||||
|
||||
var bounds = GetVisibleBounds();
|
||||
var gridSize = (int)(GRID_SIZE * _zoomFactor);
|
||||
|
||||
if (gridSize < 5) return; // 너무 작으면 그리지 않음
|
||||
|
||||
for (int x = bounds.Left; x < bounds.Right; x += GRID_SIZE)
|
||||
{
|
||||
if (x % (GRID_SIZE * 5) == 0)
|
||||
g.DrawLine(new Pen(Color.Gray, 1), x, bounds.Top, x, bounds.Bottom);
|
||||
else
|
||||
g.DrawLine(_gridPen, x, bounds.Top, x, bounds.Bottom);
|
||||
}
|
||||
|
||||
for (int y = bounds.Top; y < bounds.Bottom; y += GRID_SIZE)
|
||||
{
|
||||
if (y % (GRID_SIZE * 5) == 0)
|
||||
g.DrawLine(new Pen(Color.Gray, 1), bounds.Left, y, bounds.Right, y);
|
||||
else
|
||||
g.DrawLine(_gridPen, bounds.Left, y, bounds.Right, y);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawConnections(Graphics g)
|
||||
{
|
||||
if (_nodes == null) return;
|
||||
|
||||
foreach (var node in _nodes)
|
||||
{
|
||||
if (node.ConnectedNodes == null) continue;
|
||||
|
||||
foreach (var connectedNodeId in node.ConnectedNodes)
|
||||
{
|
||||
var targetNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
|
||||
if (targetNode == null) continue;
|
||||
|
||||
DrawConnection(g, node, targetNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawConnection(Graphics g, MapNode fromNode, MapNode toNode)
|
||||
{
|
||||
var startPoint = fromNode.Position;
|
||||
var endPoint = toNode.Position;
|
||||
|
||||
// 연결선만 그리기 (단순한 도로 연결, 방향성 없음)
|
||||
g.DrawLine(_connectionPen, startPoint, endPoint);
|
||||
}
|
||||
|
||||
private void DrawDirectionArrow(Graphics g, Point point, double angle, AgvDirection direction)
|
||||
{
|
||||
var arrowSize = CONNECTION_ARROW_SIZE;
|
||||
var arrowAngle = Math.PI / 6; // 30도
|
||||
|
||||
var cos = Math.Cos(angle);
|
||||
var sin = Math.Sin(angle);
|
||||
|
||||
var arrowPoint1 = new Point(
|
||||
(int)(point.X - arrowSize * Math.Cos(angle - arrowAngle)),
|
||||
(int)(point.Y - arrowSize * Math.Sin(angle - arrowAngle))
|
||||
);
|
||||
|
||||
var arrowPoint2 = new Point(
|
||||
(int)(point.X - arrowSize * Math.Cos(angle + arrowAngle)),
|
||||
(int)(point.Y - arrowSize * Math.Sin(angle + arrowAngle))
|
||||
);
|
||||
|
||||
var arrowColor = direction == AgvDirection.Forward ? Color.Blue : Color.Red;
|
||||
var arrowPen = new Pen(arrowColor, 2);
|
||||
|
||||
g.DrawLine(arrowPen, point, arrowPoint1);
|
||||
g.DrawLine(arrowPen, point, arrowPoint2);
|
||||
|
||||
arrowPen.Dispose();
|
||||
}
|
||||
|
||||
private void DrawPaths(Graphics g)
|
||||
{
|
||||
// 모든 경로 그리기
|
||||
if (_allPaths != null)
|
||||
{
|
||||
foreach (var path in _allPaths)
|
||||
{
|
||||
DrawPath(g, path, Color.LightBlue);
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 선택된 경로 그리기
|
||||
if (_currentPath != null)
|
||||
{
|
||||
DrawPath(g, _currentPath, Color.Purple);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPath(Graphics g, PathResult path, Color color)
|
||||
{
|
||||
if (path?.Path == null || path.Path.Count < 2) return;
|
||||
|
||||
var pathPen = new Pen(color, 4) { DashStyle = DashStyle.Dash };
|
||||
|
||||
for (int i = 0; i < path.Path.Count - 1; i++)
|
||||
{
|
||||
var currentNodeId = path.Path[i];
|
||||
var nextNodeId = path.Path[i + 1];
|
||||
|
||||
var currentNode = _nodes?.FirstOrDefault(n => n.NodeId == currentNodeId);
|
||||
var nextNode = _nodes?.FirstOrDefault(n => n.NodeId == nextNodeId);
|
||||
|
||||
if (currentNode != null && nextNode != null)
|
||||
{
|
||||
// 경로 선 그리기
|
||||
g.DrawLine(pathPen, currentNode.Position, nextNode.Position);
|
||||
|
||||
// 경로 방향 표시 (계산된 경로의 경우에만 방향 화살표 표시)
|
||||
var midPoint = new Point(
|
||||
(currentNode.Position.X + nextNode.Position.X) / 2,
|
||||
(currentNode.Position.Y + nextNode.Position.Y) / 2
|
||||
);
|
||||
|
||||
var angle = Math.Atan2(nextNode.Position.Y - currentNode.Position.Y,
|
||||
nextNode.Position.X - currentNode.Position.X);
|
||||
DrawDirectionArrow(g, midPoint, angle, AgvDirection.Forward);
|
||||
}
|
||||
}
|
||||
|
||||
pathPen.Dispose();
|
||||
}
|
||||
|
||||
private void DrawNodes(Graphics g)
|
||||
{
|
||||
if (_nodes == null) return;
|
||||
|
||||
foreach (var node in _nodes)
|
||||
{
|
||||
DrawNode(g, node);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawNode(Graphics g, MapNode node)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case NodeType.Label:
|
||||
DrawLabelNode(g, node);
|
||||
break;
|
||||
case NodeType.Image:
|
||||
DrawImageNode(g, node);
|
||||
break;
|
||||
default:
|
||||
DrawCircularNode(g, node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCircularNode(Graphics g, MapNode node)
|
||||
{
|
||||
var brush = GetNodeBrush(node);
|
||||
|
||||
switch (node.Type)
|
||||
{
|
||||
case NodeType.Docking:
|
||||
DrawPentagonNode(g, node, brush);
|
||||
break;
|
||||
case NodeType.Charging:
|
||||
DrawTriangleNode(g, node, brush);
|
||||
break;
|
||||
default:
|
||||
DrawCircleNode(g, node, brush);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCircleNode(Graphics g, MapNode node, Brush brush)
|
||||
{
|
||||
var rect = new Rectangle(
|
||||
node.Position.X - NODE_RADIUS,
|
||||
node.Position.Y - NODE_RADIUS,
|
||||
NODE_SIZE,
|
||||
NODE_SIZE
|
||||
);
|
||||
|
||||
// 노드 그리기
|
||||
g.FillEllipse(brush, rect);
|
||||
g.DrawEllipse(Pens.Black, rect);
|
||||
|
||||
// 선택된 노드 강조
|
||||
if (node == _selectedNode)
|
||||
{
|
||||
g.DrawEllipse(_selectedNodePen, rect);
|
||||
}
|
||||
|
||||
// 호버된 노드 강조
|
||||
if (node == _hoveredNode)
|
||||
{
|
||||
var hoverRect = new Rectangle(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4);
|
||||
g.DrawEllipse(new Pen(Color.Orange, 2), hoverRect);
|
||||
}
|
||||
|
||||
DrawNodeLabel(g, node);
|
||||
}
|
||||
|
||||
private void DrawPentagonNode(Graphics g, MapNode node, Brush brush)
|
||||
{
|
||||
var radius = NODE_RADIUS;
|
||||
var center = node.Position;
|
||||
|
||||
// 5각형 꼭짓점 계산 (위쪽부터 시계방향)
|
||||
var points = new Point[5];
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var angle = (Math.PI * 2 * i / 5) - Math.PI / 2; // -90도부터 시작 (위쪽)
|
||||
points[i] = new Point(
|
||||
(int)(center.X + radius * Math.Cos(angle)),
|
||||
(int)(center.Y + radius * Math.Sin(angle))
|
||||
);
|
||||
}
|
||||
|
||||
// 5각형 그리기
|
||||
g.FillPolygon(brush, points);
|
||||
g.DrawPolygon(Pens.Black, points);
|
||||
|
||||
// 선택된 노드 강조
|
||||
if (node == _selectedNode)
|
||||
{
|
||||
g.DrawPolygon(_selectedNodePen, points);
|
||||
}
|
||||
|
||||
// 호버된 노드 강조
|
||||
if (node == _hoveredNode)
|
||||
{
|
||||
// 확장된 5각형 계산
|
||||
var hoverPoints = new Point[5];
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var angle = (Math.PI * 2 * i / 5) - Math.PI / 2;
|
||||
hoverPoints[i] = new Point(
|
||||
(int)(center.X + (radius + 3) * Math.Cos(angle)),
|
||||
(int)(center.Y + (radius + 3) * Math.Sin(angle))
|
||||
);
|
||||
}
|
||||
g.DrawPolygon(new Pen(Color.Orange, 2), hoverPoints);
|
||||
}
|
||||
|
||||
DrawNodeLabel(g, node);
|
||||
}
|
||||
|
||||
private void DrawTriangleNode(Graphics g, MapNode node, Brush brush)
|
||||
{
|
||||
var radius = NODE_RADIUS;
|
||||
var center = node.Position;
|
||||
|
||||
// 삼각형 꼭짓점 계산 (위쪽 꼭짓점부터 시계방향)
|
||||
var points = new Point[3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var angle = (Math.PI * 2 * i / 3) - Math.PI / 2; // -90도부터 시작 (위쪽)
|
||||
points[i] = new Point(
|
||||
(int)(center.X + radius * Math.Cos(angle)),
|
||||
(int)(center.Y + radius * Math.Sin(angle))
|
||||
);
|
||||
}
|
||||
|
||||
// 삼각형 그리기
|
||||
g.FillPolygon(brush, points);
|
||||
g.DrawPolygon(Pens.Black, points);
|
||||
|
||||
// 선택된 노드 강조
|
||||
if (node == _selectedNode)
|
||||
{
|
||||
g.DrawPolygon(_selectedNodePen, points);
|
||||
}
|
||||
|
||||
// 호버된 노드 강조
|
||||
if (node == _hoveredNode)
|
||||
{
|
||||
// 확장된 삼각형 계산
|
||||
var hoverPoints = new Point[3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var angle = (Math.PI * 2 * i / 3) - Math.PI / 2;
|
||||
hoverPoints[i] = new Point(
|
||||
(int)(center.X + (radius + 3) * Math.Cos(angle)),
|
||||
(int)(center.Y + (radius + 3) * Math.Sin(angle))
|
||||
);
|
||||
}
|
||||
g.DrawPolygon(new Pen(Color.Orange, 2), hoverPoints);
|
||||
}
|
||||
|
||||
DrawNodeLabel(g, node);
|
||||
}
|
||||
|
||||
private void DrawNodeLabel(Graphics g, MapNode node)
|
||||
{
|
||||
string displayText;
|
||||
Color textColor;
|
||||
string descriptionText;
|
||||
|
||||
// 위쪽에 표시할 설명 (노드의 Description 속성)
|
||||
descriptionText = string.IsNullOrEmpty(node.Description) ? "" : node.Description;
|
||||
|
||||
// 아래쪽에 표시할 값 (RFID 우선, 없으면 노드ID)
|
||||
if (node.HasRfid())
|
||||
{
|
||||
// RFID가 있는 경우: 순수 RFID 값만 표시 (진한 색상)
|
||||
displayText = node.RfidId;
|
||||
textColor = Color.Black;
|
||||
}
|
||||
else
|
||||
{
|
||||
// RFID가 없는 경우: 노드 ID 표시 (연한 색상)
|
||||
displayText = node.NodeId;
|
||||
textColor = Color.Gray;
|
||||
}
|
||||
|
||||
var font = new Font("Arial", 8, FontStyle.Bold);
|
||||
var descFont = new Font("Arial", 6, FontStyle.Regular);
|
||||
|
||||
// 메인 텍스트 크기 측정
|
||||
var textSize = g.MeasureString(displayText, font);
|
||||
var descSize = g.MeasureString(descriptionText, descFont);
|
||||
|
||||
// 설명 텍스트 위치 (노드 위쪽)
|
||||
var descPoint = new Point(
|
||||
(int)(node.Position.X - descSize.Width / 2),
|
||||
(int)(node.Position.Y - NODE_RADIUS - descSize.Height - 2)
|
||||
);
|
||||
|
||||
// 메인 텍스트 위치 (노드 아래쪽)
|
||||
var textPoint = new Point(
|
||||
(int)(node.Position.X - textSize.Width / 2),
|
||||
(int)(node.Position.Y + NODE_RADIUS + 2)
|
||||
);
|
||||
|
||||
// 설명 텍스트 그리기 (설명이 있는 경우에만)
|
||||
if (!string.IsNullOrEmpty(descriptionText))
|
||||
{
|
||||
using (var descBrush = new SolidBrush(Color.FromArgb(120, Color.Black)))
|
||||
{
|
||||
g.DrawString(descriptionText, descFont, descBrush, descPoint);
|
||||
}
|
||||
}
|
||||
|
||||
// 메인 텍스트 그리기
|
||||
using (var textBrush = new SolidBrush(textColor))
|
||||
{
|
||||
g.DrawString(displayText, font, textBrush, textPoint);
|
||||
}
|
||||
|
||||
font.Dispose();
|
||||
descFont.Dispose();
|
||||
}
|
||||
|
||||
private void DrawLabelNode(Graphics g, MapNode node)
|
||||
{
|
||||
var text = string.IsNullOrEmpty(node.LabelText) ? node.NodeId : node.LabelText;
|
||||
|
||||
// 폰트 설정
|
||||
var font = new Font(node.FontFamily, node.FontSize, node.FontStyle);
|
||||
var textBrush = new SolidBrush(node.ForeColor);
|
||||
|
||||
// 텍스트 크기 측정
|
||||
var textSize = g.MeasureString(text, font);
|
||||
var textPoint = new Point(
|
||||
(int)(node.Position.X - textSize.Width / 2),
|
||||
(int)(node.Position.Y - textSize.Height / 2)
|
||||
);
|
||||
|
||||
// 배경 그리기 (설정된 경우)
|
||||
if (node.ShowBackground)
|
||||
{
|
||||
var backgroundBrush = new SolidBrush(node.BackColor);
|
||||
var backgroundRect = new Rectangle(
|
||||
textPoint.X - 2,
|
||||
textPoint.Y - 2,
|
||||
(int)textSize.Width + 4,
|
||||
(int)textSize.Height + 4
|
||||
);
|
||||
g.FillRectangle(backgroundBrush, backgroundRect);
|
||||
g.DrawRectangle(Pens.Black, backgroundRect);
|
||||
backgroundBrush.Dispose();
|
||||
}
|
||||
|
||||
// 텍스트 그리기
|
||||
g.DrawString(text, font, textBrush, textPoint);
|
||||
|
||||
// 선택된 노드 강조
|
||||
if (node == _selectedNode)
|
||||
{
|
||||
var selectionRect = new Rectangle(
|
||||
textPoint.X - 4,
|
||||
textPoint.Y - 4,
|
||||
(int)textSize.Width + 8,
|
||||
(int)textSize.Height + 8
|
||||
);
|
||||
g.DrawRectangle(_selectedNodePen, selectionRect);
|
||||
}
|
||||
|
||||
// 호버된 노드 강조
|
||||
if (node == _hoveredNode)
|
||||
{
|
||||
var hoverRect = new Rectangle(
|
||||
textPoint.X - 6,
|
||||
textPoint.Y - 6,
|
||||
(int)textSize.Width + 12,
|
||||
(int)textSize.Height + 12
|
||||
);
|
||||
g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect);
|
||||
}
|
||||
|
||||
font.Dispose();
|
||||
textBrush.Dispose();
|
||||
}
|
||||
|
||||
private void DrawImageNode(Graphics g, MapNode node)
|
||||
{
|
||||
// 이미지 로드 (필요시)
|
||||
if (node.LoadedImage == null && !string.IsNullOrEmpty(node.ImagePath))
|
||||
{
|
||||
node.LoadImage();
|
||||
}
|
||||
|
||||
if (node.LoadedImage != null)
|
||||
{
|
||||
// 실제 표시 크기 계산
|
||||
var displaySize = node.GetDisplaySize();
|
||||
if (displaySize.IsEmpty)
|
||||
displaySize = new Size(50, 50); // 기본 크기
|
||||
|
||||
var imageRect = new Rectangle(
|
||||
node.Position.X - displaySize.Width / 2,
|
||||
node.Position.Y - displaySize.Height / 2,
|
||||
displaySize.Width,
|
||||
displaySize.Height
|
||||
);
|
||||
|
||||
// 회전이 있는 경우
|
||||
if (node.Rotation != 0)
|
||||
{
|
||||
var oldTransform = g.Transform;
|
||||
g.TranslateTransform(node.Position.X, node.Position.Y);
|
||||
g.RotateTransform(node.Rotation);
|
||||
g.TranslateTransform(-node.Position.X, -node.Position.Y);
|
||||
|
||||
// 투명도 적용하여 이미지 그리기
|
||||
if (node.Opacity < 1.0f)
|
||||
{
|
||||
var imageAttributes = new System.Drawing.Imaging.ImageAttributes();
|
||||
var colorMatrix = new System.Drawing.Imaging.ColorMatrix();
|
||||
colorMatrix.Matrix33 = node.Opacity;
|
||||
imageAttributes.SetColorMatrix(colorMatrix, System.Drawing.Imaging.ColorMatrixFlag.Default,
|
||||
System.Drawing.Imaging.ColorAdjustType.Bitmap);
|
||||
g.DrawImage(node.LoadedImage, imageRect, 0, 0, node.LoadedImage.Width, node.LoadedImage.Height,
|
||||
GraphicsUnit.Pixel, imageAttributes);
|
||||
imageAttributes.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
g.DrawImage(node.LoadedImage, imageRect);
|
||||
}
|
||||
|
||||
g.Transform = oldTransform;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 투명도 적용하여 이미지 그리기
|
||||
if (node.Opacity < 1.0f)
|
||||
{
|
||||
var imageAttributes = new System.Drawing.Imaging.ImageAttributes();
|
||||
var colorMatrix = new System.Drawing.Imaging.ColorMatrix();
|
||||
colorMatrix.Matrix33 = node.Opacity;
|
||||
imageAttributes.SetColorMatrix(colorMatrix, System.Drawing.Imaging.ColorMatrixFlag.Default,
|
||||
System.Drawing.Imaging.ColorAdjustType.Bitmap);
|
||||
g.DrawImage(node.LoadedImage, imageRect, 0, 0, node.LoadedImage.Width, node.LoadedImage.Height,
|
||||
GraphicsUnit.Pixel, imageAttributes);
|
||||
imageAttributes.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
g.DrawImage(node.LoadedImage, imageRect);
|
||||
}
|
||||
}
|
||||
|
||||
// 선택된 노드 강조
|
||||
if (node == _selectedNode)
|
||||
{
|
||||
g.DrawRectangle(_selectedNodePen, imageRect);
|
||||
}
|
||||
|
||||
// 호버된 노드 강조
|
||||
if (node == _hoveredNode)
|
||||
{
|
||||
var hoverRect = new Rectangle(imageRect.X - 2, imageRect.Y - 2, imageRect.Width + 4, imageRect.Height + 4);
|
||||
g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect);
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// 이미지가 없는 경우 기본 사각형으로 표시
|
||||
var rect = new Rectangle(
|
||||
node.Position.X - 25,
|
||||
node.Position.Y - 25,
|
||||
50,
|
||||
50
|
||||
);
|
||||
|
||||
g.FillRectangle(Brushes.LightGray, rect);
|
||||
g.DrawRectangle(Pens.Black, rect);
|
||||
|
||||
// "이미지 없음" 텍스트
|
||||
var font = new Font("Arial", 8);
|
||||
var text = "No Image";
|
||||
var textSize = g.MeasureString(text, font);
|
||||
var textPoint = new Point(
|
||||
(int)(node.Position.X - textSize.Width / 2),
|
||||
(int)(node.Position.Y - textSize.Height / 2)
|
||||
);
|
||||
g.DrawString(text, font, Brushes.Black, textPoint);
|
||||
font.Dispose();
|
||||
|
||||
// 선택된 노드 강조
|
||||
if (node == _selectedNode)
|
||||
{
|
||||
g.DrawRectangle(_selectedNodePen, rect);
|
||||
}
|
||||
|
||||
// 호버된 노드 강조
|
||||
if (node == _hoveredNode)
|
||||
{
|
||||
var hoverRect = new Rectangle(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4);
|
||||
g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Brush GetNodeBrush(MapNode node)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case NodeType.Normal:
|
||||
return _normalNodeBrush;
|
||||
case NodeType.Rotation:
|
||||
return _rotationNodeBrush;
|
||||
case NodeType.Docking:
|
||||
return _dockingNodeBrush;
|
||||
case NodeType.Charging:
|
||||
return _chargingNodeBrush;
|
||||
case NodeType.Label:
|
||||
return new SolidBrush(Color.Purple);
|
||||
case NodeType.Image:
|
||||
return new SolidBrush(Color.Brown);
|
||||
default:
|
||||
return _normalNodeBrush;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAGVs(Graphics g)
|
||||
{
|
||||
if (_agvList == null) return;
|
||||
|
||||
foreach (var agv in _agvList)
|
||||
{
|
||||
if (_agvPositions.ContainsKey(agv.AgvId))
|
||||
{
|
||||
DrawAGV(g, agv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAGV(Graphics g, IAGV agv)
|
||||
{
|
||||
if (!_agvPositions.ContainsKey(agv.AgvId)) return;
|
||||
|
||||
var position = _agvPositions[agv.AgvId];
|
||||
var direction = _agvDirections.ContainsKey(agv.AgvId) ? _agvDirections[agv.AgvId] : AgvDirection.Forward;
|
||||
var state = _agvStates.ContainsKey(agv.AgvId) ? _agvStates[agv.AgvId] : AGVState.Idle;
|
||||
|
||||
// AGV 색상 결정
|
||||
var brush = GetAGVBrush(state);
|
||||
|
||||
// AGV 사각형 그리기
|
||||
var rect = new Rectangle(
|
||||
position.X - AGV_SIZE / 2,
|
||||
position.Y - AGV_SIZE / 2,
|
||||
AGV_SIZE,
|
||||
AGV_SIZE
|
||||
);
|
||||
|
||||
g.FillRectangle(brush, rect);
|
||||
g.DrawRectangle(_agvPen, rect);
|
||||
|
||||
// 방향 표시 (화살표)
|
||||
DrawAGVDirection(g, position, direction);
|
||||
|
||||
// AGV ID 표시
|
||||
var font = new Font("Arial", 10, FontStyle.Bold);
|
||||
var textSize = g.MeasureString(agv.AgvId, font);
|
||||
var textPoint = new Point(
|
||||
(int)(position.X - textSize.Width / 2),
|
||||
(int)(position.Y - AGV_SIZE / 2 - textSize.Height - 2)
|
||||
);
|
||||
|
||||
g.DrawString(agv.AgvId, font, Brushes.Black, textPoint);
|
||||
|
||||
// 배터리 레벨 표시
|
||||
var batteryText = $"{agv.BatteryLevel:F0}%";
|
||||
var batterySize = g.MeasureString(batteryText, font);
|
||||
var batteryPoint = new Point(
|
||||
(int)(position.X - batterySize.Width / 2),
|
||||
(int)(position.Y + AGV_SIZE / 2 + 2)
|
||||
);
|
||||
|
||||
g.DrawString(batteryText, font, Brushes.Black, batteryPoint);
|
||||
font.Dispose();
|
||||
}
|
||||
|
||||
private Brush GetAGVBrush(AGVState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case AGVState.Idle:
|
||||
return Brushes.LightGray;
|
||||
case AGVState.Moving:
|
||||
return Brushes.LightGreen;
|
||||
case AGVState.Rotating:
|
||||
return Brushes.Yellow;
|
||||
case AGVState.Docking:
|
||||
return Brushes.Orange;
|
||||
case AGVState.Charging:
|
||||
return Brushes.Blue;
|
||||
case AGVState.Error:
|
||||
return Brushes.Red;
|
||||
default:
|
||||
return Brushes.LightGray;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAGVDirection(Graphics g, Point position, AgvDirection direction)
|
||||
{
|
||||
var arrowSize = 10;
|
||||
Point[] arrowPoints = null;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case AgvDirection.Forward:
|
||||
arrowPoints = new Point[]
|
||||
{
|
||||
new Point(position.X + arrowSize, position.Y),
|
||||
new Point(position.X - arrowSize/2, position.Y - arrowSize/2),
|
||||
new Point(position.X - arrowSize/2, position.Y + arrowSize/2)
|
||||
};
|
||||
break;
|
||||
case AgvDirection.Backward:
|
||||
arrowPoints = new Point[]
|
||||
{
|
||||
new Point(position.X - arrowSize, position.Y),
|
||||
new Point(position.X + arrowSize/2, position.Y - arrowSize/2),
|
||||
new Point(position.X + arrowSize/2, position.Y + arrowSize/2)
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
if (arrowPoints != null)
|
||||
{
|
||||
g.FillPolygon(Brushes.White, arrowPoints);
|
||||
g.DrawPolygon(Pens.Black, arrowPoints);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTemporaryConnection(Graphics g)
|
||||
{
|
||||
if (_connectionStartNode != null && _connectionEndPoint != Point.Empty)
|
||||
{
|
||||
g.DrawLine(_tempConnectionPen, _connectionStartNode.Position, _connectionEndPoint);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawUIInfo(Graphics g)
|
||||
{
|
||||
// 회사 로고
|
||||
if (_companyLogo != null)
|
||||
{
|
||||
var logoRect = new Rectangle(10, 10, 100, 50);
|
||||
g.DrawImage(_companyLogo, logoRect);
|
||||
}
|
||||
|
||||
// 측정 정보
|
||||
if (!string.IsNullOrEmpty(_measurementInfo))
|
||||
{
|
||||
var font = new Font("Arial", 9);
|
||||
var textBrush = new SolidBrush(Color.Black);
|
||||
var backgroundBrush = new SolidBrush(Color.FromArgb(200, Color.White));
|
||||
|
||||
var textSize = g.MeasureString(_measurementInfo, font);
|
||||
var textRect = new Rectangle(
|
||||
Width - (int)textSize.Width - 20,
|
||||
Height - (int)textSize.Height - 20,
|
||||
(int)textSize.Width + 10,
|
||||
(int)textSize.Height + 10
|
||||
);
|
||||
|
||||
g.FillRectangle(backgroundBrush, textRect);
|
||||
g.DrawRectangle(Pens.Gray, textRect);
|
||||
g.DrawString(_measurementInfo, font, textBrush, textRect.X + 5, textRect.Y + 5);
|
||||
|
||||
font.Dispose();
|
||||
textBrush.Dispose();
|
||||
backgroundBrush.Dispose();
|
||||
}
|
||||
|
||||
// 줌 정보
|
||||
var zoomText = $"Zoom: {_zoomFactor:P0}";
|
||||
var zoomFont = new Font("Arial", 10, FontStyle.Bold);
|
||||
var zoomSize = g.MeasureString(zoomText, zoomFont);
|
||||
var zoomPoint = new Point(10, Height - (int)zoomSize.Height - 10);
|
||||
|
||||
g.FillRectangle(new SolidBrush(Color.FromArgb(200, Color.White)),
|
||||
zoomPoint.X - 5, zoomPoint.Y - 5,
|
||||
zoomSize.Width + 10, zoomSize.Height + 10);
|
||||
g.DrawString(zoomText, zoomFont, Brushes.Black, zoomPoint);
|
||||
zoomFont.Dispose();
|
||||
}
|
||||
|
||||
private Rectangle GetVisibleBounds()
|
||||
{
|
||||
var left = (int)(-_panOffset.X / _zoomFactor);
|
||||
var top = (int)(-_panOffset.Y / _zoomFactor);
|
||||
var right = (int)((Width - _panOffset.X) / _zoomFactor);
|
||||
var bottom = (int)((Height - _panOffset.Y) / _zoomFactor);
|
||||
|
||||
return new Rectangle(left, top, right - left, bottom - top);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
605
Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs
Normal file
605
Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs
Normal file
@@ -0,0 +1,605 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.Controls
|
||||
{
|
||||
public partial class UnifiedAGVCanvas
|
||||
{
|
||||
#region Mouse Events
|
||||
|
||||
private void UnifiedAGVCanvas_MouseClick(object sender, MouseEventArgs e)
|
||||
{
|
||||
Focus(); // 포커스 설정
|
||||
|
||||
if (_canvasMode == CanvasMode.ViewOnly) return;
|
||||
|
||||
var worldPoint = ScreenToWorld(e.Location);
|
||||
var hitNode = GetNodeAt(worldPoint);
|
||||
|
||||
switch (_editMode)
|
||||
{
|
||||
case EditMode.Select:
|
||||
HandleSelectClick(hitNode);
|
||||
break;
|
||||
|
||||
case EditMode.AddNode:
|
||||
HandleAddNodeClick(worldPoint);
|
||||
break;
|
||||
|
||||
case EditMode.Connect:
|
||||
HandleConnectClick(hitNode);
|
||||
break;
|
||||
|
||||
case EditMode.Delete:
|
||||
HandleDeleteClick(hitNode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnifiedAGVCanvas_MouseDoubleClick(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (_canvasMode == CanvasMode.ViewOnly) return;
|
||||
|
||||
var worldPoint = ScreenToWorld(e.Location);
|
||||
var hitNode = GetNodeAt(worldPoint);
|
||||
|
||||
if (hitNode != null)
|
||||
{
|
||||
// 노드 속성 편집 (이벤트 발생)
|
||||
NodeSelected?.Invoke(this, hitNode);
|
||||
}
|
||||
}
|
||||
|
||||
private void UnifiedAGVCanvas_MouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
var worldPoint = ScreenToWorld(e.Location);
|
||||
|
||||
if (e.Button == MouseButtons.Left)
|
||||
{
|
||||
if (_canvasMode == CanvasMode.Edit && _editMode == EditMode.Move)
|
||||
{
|
||||
var hitNode = GetNodeAt(worldPoint);
|
||||
if (hitNode != null)
|
||||
{
|
||||
_isDragging = true;
|
||||
_selectedNode = hitNode;
|
||||
_dragOffset = new Point(
|
||||
worldPoint.X - hitNode.Position.X,
|
||||
worldPoint.Y - hitNode.Position.Y
|
||||
);
|
||||
Cursor = Cursors.SizeAll;
|
||||
Invalidate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 팬 시작 (우클릭 또는 중간버튼)
|
||||
_isPanning = true;
|
||||
_lastMousePosition = e.Location;
|
||||
Cursor = Cursors.SizeAll;
|
||||
}
|
||||
else if (e.Button == MouseButtons.Right)
|
||||
{
|
||||
// 컨텍스트 메뉴 (편집 모드에서만)
|
||||
if (_canvasMode == CanvasMode.Edit)
|
||||
{
|
||||
var hitNode = GetNodeAt(worldPoint);
|
||||
ShowContextMenu(e.Location, hitNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UnifiedAGVCanvas_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
var worldPoint = ScreenToWorld(e.Location);
|
||||
|
||||
// 호버 노드 업데이트
|
||||
var newHoveredNode = GetNodeAt(worldPoint);
|
||||
if (newHoveredNode != _hoveredNode)
|
||||
{
|
||||
_hoveredNode = newHoveredNode;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
if (_isPanning)
|
||||
{
|
||||
// 팬 처리
|
||||
var deltaX = e.X - _lastMousePosition.X;
|
||||
var deltaY = e.Y - _lastMousePosition.Y;
|
||||
|
||||
_panOffset.X += deltaX;
|
||||
_panOffset.Y += deltaY;
|
||||
|
||||
_lastMousePosition = e.Location;
|
||||
Invalidate();
|
||||
}
|
||||
else if (_isDragging && _canvasMode == CanvasMode.Edit)
|
||||
{
|
||||
// 노드 드래그
|
||||
if (_selectedNode != null)
|
||||
{
|
||||
var newPosition = new Point(
|
||||
worldPoint.X - _dragOffset.X,
|
||||
worldPoint.Y - _dragOffset.Y
|
||||
);
|
||||
|
||||
// 그리드 스냅
|
||||
if (ModifierKeys.HasFlag(Keys.Control))
|
||||
{
|
||||
newPosition.X = (newPosition.X / GRID_SIZE) * GRID_SIZE;
|
||||
newPosition.Y = (newPosition.Y / GRID_SIZE) * GRID_SIZE;
|
||||
}
|
||||
|
||||
_selectedNode.Position = newPosition;
|
||||
NodeMoved?.Invoke(this, _selectedNode);
|
||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
else if (_isConnectionMode && _canvasMode == CanvasMode.Edit)
|
||||
{
|
||||
// 임시 연결선 업데이트
|
||||
_connectionEndPoint = worldPoint;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
// 툴팁 표시 (호버된 노드/AGV 정보)
|
||||
UpdateTooltip(worldPoint);
|
||||
}
|
||||
|
||||
private void UnifiedAGVCanvas_MouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.Button == MouseButtons.Left)
|
||||
{
|
||||
if (_isDragging && _canvasMode == CanvasMode.Edit)
|
||||
{
|
||||
_isDragging = false;
|
||||
Cursor = GetCursorForMode(_editMode);
|
||||
}
|
||||
|
||||
if (_isPanning)
|
||||
{
|
||||
_isPanning = false;
|
||||
Cursor = Cursors.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UnifiedAGVCanvas_MouseWheel(object sender, MouseEventArgs e)
|
||||
{
|
||||
// 줌 처리
|
||||
var mouseWorldPoint = ScreenToWorld(e.Location);
|
||||
var oldZoom = _zoomFactor;
|
||||
|
||||
if (e.Delta > 0)
|
||||
_zoomFactor = Math.Min(_zoomFactor * 1.2f, 5.0f);
|
||||
else
|
||||
_zoomFactor = Math.Max(_zoomFactor / 1.2f, 0.1f);
|
||||
|
||||
// 마우스 위치를 중심으로 줌
|
||||
var zoomRatio = _zoomFactor / oldZoom;
|
||||
_panOffset.X = (int)(e.X - (e.X - _panOffset.X) * zoomRatio);
|
||||
_panOffset.Y = (int)(e.Y - (e.Y - _panOffset.Y) * zoomRatio);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mouse Helper Methods
|
||||
|
||||
private Point ScreenToWorld(Point screenPoint)
|
||||
{
|
||||
return new Point(
|
||||
(int)((screenPoint.X - _panOffset.X) / _zoomFactor),
|
||||
(int)((screenPoint.Y - _panOffset.Y) / _zoomFactor)
|
||||
);
|
||||
}
|
||||
|
||||
private Point WorldToScreen(Point worldPoint)
|
||||
{
|
||||
return new Point(
|
||||
(int)(worldPoint.X * _zoomFactor + _panOffset.X),
|
||||
(int)(worldPoint.Y * _zoomFactor + _panOffset.Y)
|
||||
);
|
||||
}
|
||||
|
||||
private MapNode GetNodeAt(Point worldPoint)
|
||||
{
|
||||
if (_nodes == null) return null;
|
||||
|
||||
// 역순으로 검사하여 위에 그려진 노드부터 확인
|
||||
for (int i = _nodes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var node = _nodes[i];
|
||||
if (IsPointInNode(worldPoint, node))
|
||||
return node;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsPointInNode(Point point, MapNode node)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case NodeType.Label:
|
||||
return IsPointInLabelNode(point, node);
|
||||
case NodeType.Image:
|
||||
return IsPointInImageNode(point, node);
|
||||
default:
|
||||
return IsPointInCircularNode(point, node);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPointInCircularNode(Point point, MapNode node)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case NodeType.Docking:
|
||||
return IsPointInPentagon(point, node);
|
||||
case NodeType.Charging:
|
||||
return IsPointInTriangle(point, node);
|
||||
default:
|
||||
return IsPointInCircle(point, node);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPointInCircle(Point point, MapNode node)
|
||||
{
|
||||
var hitRadius = Math.Max(NODE_RADIUS, 10 / _zoomFactor);
|
||||
var distance = Math.Sqrt(
|
||||
Math.Pow(node.Position.X - point.X, 2) +
|
||||
Math.Pow(node.Position.Y - point.Y, 2)
|
||||
);
|
||||
return distance <= hitRadius;
|
||||
}
|
||||
|
||||
private bool IsPointInPentagon(Point point, MapNode node)
|
||||
{
|
||||
var radius = NODE_RADIUS;
|
||||
var center = node.Position;
|
||||
|
||||
// 5각형 꼭짓점 계산
|
||||
var points = new Point[5];
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var angle = (Math.PI * 2 * i / 5) - Math.PI / 2;
|
||||
points[i] = new Point(
|
||||
(int)(center.X + radius * Math.Cos(angle)),
|
||||
(int)(center.Y + radius * Math.Sin(angle))
|
||||
);
|
||||
}
|
||||
|
||||
return IsPointInPolygon(point, points);
|
||||
}
|
||||
|
||||
private bool IsPointInTriangle(Point point, MapNode node)
|
||||
{
|
||||
var radius = NODE_RADIUS;
|
||||
var center = node.Position;
|
||||
|
||||
// 삼각형 꼭짓점 계산
|
||||
var points = new Point[3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var angle = (Math.PI * 2 * i / 3) - Math.PI / 2;
|
||||
points[i] = new Point(
|
||||
(int)(center.X + radius * Math.Cos(angle)),
|
||||
(int)(center.Y + radius * Math.Sin(angle))
|
||||
);
|
||||
}
|
||||
|
||||
return IsPointInPolygon(point, points);
|
||||
}
|
||||
|
||||
private bool IsPointInPolygon(Point point, Point[] polygon)
|
||||
{
|
||||
// Ray casting 알고리즘 사용
|
||||
bool inside = false;
|
||||
int j = polygon.Length - 1;
|
||||
|
||||
for (int i = 0; i < polygon.Length; i++)
|
||||
{
|
||||
var xi = polygon[i].X;
|
||||
var yi = polygon[i].Y;
|
||||
var xj = polygon[j].X;
|
||||
var yj = polygon[j].Y;
|
||||
|
||||
if (((yi > point.Y) != (yj > point.Y)) &&
|
||||
(point.X < (xj - xi) * (point.Y - yi) / (yj - yi) + xi))
|
||||
{
|
||||
inside = !inside;
|
||||
}
|
||||
j = i;
|
||||
}
|
||||
|
||||
return inside;
|
||||
}
|
||||
|
||||
private bool IsPointInLabelNode(Point point, MapNode node)
|
||||
{
|
||||
var text = string.IsNullOrEmpty(node.LabelText) ? node.NodeId : node.LabelText;
|
||||
|
||||
// 임시 Graphics로 텍스트 크기 측정
|
||||
using (var tempBitmap = new Bitmap(1, 1))
|
||||
using (var tempGraphics = Graphics.FromImage(tempBitmap))
|
||||
{
|
||||
var font = new Font(node.FontFamily, node.FontSize, node.FontStyle);
|
||||
var textSize = tempGraphics.MeasureString(text, font);
|
||||
|
||||
var textRect = new Rectangle(
|
||||
(int)(node.Position.X - textSize.Width / 2),
|
||||
(int)(node.Position.Y - textSize.Height / 2),
|
||||
(int)textSize.Width,
|
||||
(int)textSize.Height
|
||||
);
|
||||
|
||||
font.Dispose();
|
||||
return textRect.Contains(point);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPointInImageNode(Point point, MapNode node)
|
||||
{
|
||||
var displaySize = node.GetDisplaySize();
|
||||
if (displaySize.IsEmpty)
|
||||
displaySize = new Size(50, 50); // 기본 크기
|
||||
|
||||
var imageRect = new Rectangle(
|
||||
node.Position.X - displaySize.Width / 2,
|
||||
node.Position.Y - displaySize.Height / 2,
|
||||
displaySize.Width,
|
||||
displaySize.Height
|
||||
);
|
||||
|
||||
return imageRect.Contains(point);
|
||||
}
|
||||
|
||||
private IAGV GetAGVAt(Point worldPoint)
|
||||
{
|
||||
if (_agvList == null) return null;
|
||||
|
||||
var hitRadius = Math.Max(AGV_SIZE / 2, 15 / _zoomFactor);
|
||||
|
||||
return _agvList.FirstOrDefault(agv =>
|
||||
{
|
||||
if (!_agvPositions.ContainsKey(agv.AgvId)) return false;
|
||||
|
||||
var agvPos = _agvPositions[agv.AgvId];
|
||||
var distance = Math.Sqrt(
|
||||
Math.Pow(agvPos.X - worldPoint.X, 2) +
|
||||
Math.Pow(agvPos.Y - worldPoint.Y, 2)
|
||||
);
|
||||
return distance <= hitRadius;
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleSelectClick(MapNode hitNode)
|
||||
{
|
||||
if (hitNode != _selectedNode)
|
||||
{
|
||||
_selectedNode = hitNode;
|
||||
NodeSelected?.Invoke(this, hitNode);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleAddNodeClick(Point worldPoint)
|
||||
{
|
||||
// 그리드 스냅
|
||||
if (ModifierKeys.HasFlag(Keys.Control))
|
||||
{
|
||||
worldPoint.X = (worldPoint.X / GRID_SIZE) * GRID_SIZE;
|
||||
worldPoint.Y = (worldPoint.Y / GRID_SIZE) * GRID_SIZE;
|
||||
}
|
||||
|
||||
var newNode = new MapNode
|
||||
{
|
||||
NodeId = $"N{_nodeCounter:D3}",
|
||||
Position = worldPoint,
|
||||
Type = NodeType.Normal
|
||||
};
|
||||
|
||||
_nodeCounter++;
|
||||
_nodes.Add(newNode);
|
||||
|
||||
NodeAdded?.Invoke(this, newNode);
|
||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void HandleConnectClick(MapNode hitNode)
|
||||
{
|
||||
if (hitNode == null) return;
|
||||
|
||||
if (!_isConnectionMode)
|
||||
{
|
||||
// 연결 시작
|
||||
_isConnectionMode = true;
|
||||
_connectionStartNode = hitNode;
|
||||
_selectedNode = hitNode;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 연결 완료
|
||||
if (_connectionStartNode != null && _connectionStartNode != hitNode)
|
||||
{
|
||||
CreateConnection(_connectionStartNode, hitNode);
|
||||
}
|
||||
CancelConnection();
|
||||
}
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void HandleDeleteClick(MapNode hitNode)
|
||||
{
|
||||
if (hitNode == null) return;
|
||||
|
||||
// 연결된 모든 연결선도 제거
|
||||
foreach (var node in _nodes)
|
||||
{
|
||||
node.RemoveConnection(hitNode.NodeId);
|
||||
}
|
||||
|
||||
_nodes.Remove(hitNode);
|
||||
|
||||
if (_selectedNode == hitNode)
|
||||
_selectedNode = null;
|
||||
|
||||
NodeDeleted?.Invoke(this, hitNode);
|
||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void CreateConnection(MapNode fromNode, MapNode toNode)
|
||||
{
|
||||
// 중복 연결 체크
|
||||
if (fromNode.ConnectedNodes.Contains(toNode.NodeId))
|
||||
return;
|
||||
|
||||
fromNode.AddConnection(toNode.NodeId);
|
||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private float CalculateDistance(Point from, Point to)
|
||||
{
|
||||
var dx = to.X - from.X;
|
||||
var dy = to.Y - from.Y;
|
||||
return (float)Math.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
private void ShowContextMenu(Point location, MapNode hitNode)
|
||||
{
|
||||
_contextMenu.Items.Clear();
|
||||
|
||||
if (hitNode != null)
|
||||
{
|
||||
_contextMenu.Items.Add("노드 속성...", null, (s, e) => NodeSelected?.Invoke(this, hitNode));
|
||||
_contextMenu.Items.Add("노드 삭제", null, (s, e) => HandleDeleteClick(hitNode));
|
||||
_contextMenu.Items.Add("-");
|
||||
}
|
||||
|
||||
_contextMenu.Items.Add("노드 추가", null, (s, e) =>
|
||||
{
|
||||
var worldPoint = ScreenToWorld(location);
|
||||
HandleAddNodeClick(worldPoint);
|
||||
});
|
||||
|
||||
_contextMenu.Items.Add("전체 맞춤", null, (s, e) => FitToNodes());
|
||||
_contextMenu.Items.Add("줌 리셋", null, (s, e) => ResetZoom());
|
||||
|
||||
_contextMenu.Show(this, location);
|
||||
}
|
||||
|
||||
private void UpdateTooltip(Point worldPoint)
|
||||
{
|
||||
string tooltipText = "";
|
||||
|
||||
// 노드 툴팁
|
||||
var hitNode = GetNodeAt(worldPoint);
|
||||
if (hitNode != null)
|
||||
{
|
||||
tooltipText = $"노드: {hitNode.NodeId}\n타입: {hitNode.Type}\n위치: ({hitNode.Position.X}, {hitNode.Position.Y})";
|
||||
}
|
||||
else
|
||||
{
|
||||
// AGV 툴팁
|
||||
var hitAGV = GetAGVAt(worldPoint);
|
||||
if (hitAGV != null)
|
||||
{
|
||||
var state = _agvStates.ContainsKey(hitAGV.AgvId) ? _agvStates[hitAGV.AgvId] : AGVState.Idle;
|
||||
tooltipText = $"AGV: {hitAGV.AgvId}\n상태: {state}\n배터리: {hitAGV.BatteryLevel:F1}%\n위치: ({hitAGV.CurrentPosition.X}, {hitAGV.CurrentPosition.Y})";
|
||||
}
|
||||
}
|
||||
|
||||
// 툴팁 업데이트 (기존 ToolTip 컨트롤 사용)
|
||||
if (!string.IsNullOrEmpty(tooltipText))
|
||||
{
|
||||
// ToolTip 설정 (필요시 추가 구현)
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region View Control Methods
|
||||
|
||||
/// <summary>
|
||||
/// 모든 노드가 보이도록 뷰 조정
|
||||
/// </summary>
|
||||
public void FitToNodes()
|
||||
{
|
||||
if (_nodes == null || _nodes.Count == 0) return;
|
||||
|
||||
var minX = _nodes.Min(n => n.Position.X);
|
||||
var maxX = _nodes.Max(n => n.Position.X);
|
||||
var minY = _nodes.Min(n => n.Position.Y);
|
||||
var maxY = _nodes.Max(n => n.Position.Y);
|
||||
|
||||
var margin = 50;
|
||||
var contentWidth = maxX - minX + margin * 2;
|
||||
var contentHeight = maxY - minY + margin * 2;
|
||||
|
||||
var zoomX = (float)Width / contentWidth;
|
||||
var zoomY = (float)Height / contentHeight;
|
||||
_zoomFactor = Math.Min(zoomX, zoomY) * 0.9f;
|
||||
|
||||
var centerX = (minX + maxX) / 2;
|
||||
var centerY = (minY + maxY) / 2;
|
||||
|
||||
_panOffset.X = (int)(Width / 2 - centerX * _zoomFactor);
|
||||
_panOffset.Y = (int)(Height / 2 - centerY * _zoomFactor);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 줌 리셋
|
||||
/// </summary>
|
||||
public void ResetZoom()
|
||||
{
|
||||
_zoomFactor = 1.0f;
|
||||
_panOffset = Point.Empty;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 위치로 이동
|
||||
/// </summary>
|
||||
public void PanTo(Point worldPoint)
|
||||
{
|
||||
_panOffset.X = (int)(Width / 2 - worldPoint.X * _zoomFactor);
|
||||
_panOffset.Y = (int)(Height / 2 - worldPoint.Y * _zoomFactor);
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 노드로 이동
|
||||
/// </summary>
|
||||
public void PanToNode(string nodeId)
|
||||
{
|
||||
var node = _nodes?.FirstOrDefault(n => n.NodeId == nodeId);
|
||||
if (node != null)
|
||||
{
|
||||
PanTo(node.Position);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 AGV로 이동
|
||||
/// </summary>
|
||||
public void PanToAGV(string agvId)
|
||||
{
|
||||
if (_agvPositions.ContainsKey(agvId))
|
||||
{
|
||||
PanTo(_agvPositions[agvId]);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
536
Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs
Normal file
536
Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs
Normal file
@@ -0,0 +1,536 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding;
|
||||
|
||||
namespace AGVNavigationCore.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// 통합 AGV 캔버스 컨트롤
|
||||
/// 맵 편집, AGV 시뮬레이션, 실시간 모니터링을 모두 지원
|
||||
/// </summary>
|
||||
public partial class UnifiedAGVCanvas : UserControl
|
||||
{
|
||||
#region Constants
|
||||
|
||||
private const int NODE_SIZE = 24;
|
||||
private const int NODE_RADIUS = NODE_SIZE / 2;
|
||||
private const int GRID_SIZE = 20;
|
||||
private const float CONNECTION_WIDTH = 2.0f;
|
||||
private const int SNAP_DISTANCE = 10;
|
||||
private const int AGV_SIZE = 30;
|
||||
private const int CONNECTION_ARROW_SIZE = 8;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Enums
|
||||
|
||||
/// <summary>
|
||||
/// 캔버스 모드
|
||||
/// </summary>
|
||||
public enum CanvasMode
|
||||
{
|
||||
ViewOnly, // 읽기 전용 (시뮬레이터, 모니터링)
|
||||
Edit // 편집 가능 (맵 에디터)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 편집 모드 (CanvasMode.Edit일 때만 적용)
|
||||
/// </summary>
|
||||
public enum EditMode
|
||||
{
|
||||
Select, // 선택 모드
|
||||
Move, // 이동 모드
|
||||
AddNode, // 노드 추가 모드
|
||||
Connect, // 연결 모드
|
||||
Delete, // 삭제 모드
|
||||
AddLabel, // 라벨 추가 모드
|
||||
AddImage // 이미지 추가 모드
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
// 캔버스 모드
|
||||
private CanvasMode _canvasMode = CanvasMode.ViewOnly;
|
||||
private EditMode _editMode = EditMode.Select;
|
||||
|
||||
// 맵 데이터
|
||||
private List<MapNode> _nodes;
|
||||
private MapNode _selectedNode;
|
||||
private MapNode _hoveredNode;
|
||||
|
||||
// AGV 관련
|
||||
private List<IAGV> _agvList;
|
||||
private Dictionary<string, Point> _agvPositions;
|
||||
private Dictionary<string, AgvDirection> _agvDirections;
|
||||
private Dictionary<string, AGVState> _agvStates;
|
||||
|
||||
// 경로 관련
|
||||
private PathResult _currentPath;
|
||||
private List<PathResult> _allPaths;
|
||||
|
||||
// UI 요소들
|
||||
private Image _companyLogo;
|
||||
private string _companyLogoPath = string.Empty;
|
||||
private string _measurementInfo = "스케일: 1:100\n면적: 1000㎡\n최종 수정: " + DateTime.Now.ToString("yyyy-MM-dd");
|
||||
|
||||
// 편집 관련 (EditMode에서만 사용)
|
||||
private bool _isDragging;
|
||||
private Point _dragOffset;
|
||||
private Point _lastMousePosition;
|
||||
private bool _isConnectionMode;
|
||||
private MapNode _connectionStartNode;
|
||||
private Point _connectionEndPoint;
|
||||
|
||||
// 그리드 및 줌 관련
|
||||
private bool _showGrid = true;
|
||||
private float _zoomFactor = 1.0f;
|
||||
private Point _panOffset = Point.Empty;
|
||||
private bool _isPanning;
|
||||
|
||||
// 자동 증가 카운터
|
||||
private int _nodeCounter = 1;
|
||||
|
||||
|
||||
// 브러쉬 및 펜
|
||||
private Brush _normalNodeBrush;
|
||||
private Brush _rotationNodeBrush;
|
||||
private Brush _dockingNodeBrush;
|
||||
private Brush _chargingNodeBrush;
|
||||
private Brush _selectedNodeBrush;
|
||||
private Brush _hoveredNodeBrush;
|
||||
private Brush _gridBrush;
|
||||
private Brush _agvBrush;
|
||||
private Brush _pathBrush;
|
||||
|
||||
private Pen _connectionPen;
|
||||
private Pen _gridPen;
|
||||
private Pen _tempConnectionPen;
|
||||
private Pen _selectedNodePen;
|
||||
private Pen _pathPen;
|
||||
private Pen _agvPen;
|
||||
|
||||
// 컨텍스트 메뉴
|
||||
private ContextMenuStrip _contextMenu;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
// 맵 편집 이벤트
|
||||
public event EventHandler<MapNode> NodeAdded;
|
||||
public event EventHandler<MapNode> NodeSelected;
|
||||
public event EventHandler<MapNode> NodeDeleted;
|
||||
public event EventHandler<MapNode> NodeMoved;
|
||||
public event EventHandler MapChanged;
|
||||
|
||||
// AGV 이벤트
|
||||
public event EventHandler<IAGV> AGVSelected;
|
||||
public event EventHandler<IAGV> AGVStateChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// 캔버스 모드
|
||||
/// </summary>
|
||||
public CanvasMode Mode
|
||||
{
|
||||
get => _canvasMode;
|
||||
set
|
||||
{
|
||||
_canvasMode = value;
|
||||
UpdateModeUI();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 편집 모드 (CanvasMode.Edit일 때만 적용)
|
||||
/// </summary>
|
||||
public EditMode CurrentEditMode
|
||||
{
|
||||
get => _editMode;
|
||||
set
|
||||
{
|
||||
if (_canvasMode != CanvasMode.Edit) return;
|
||||
|
||||
_editMode = value;
|
||||
if (_editMode != EditMode.Connect)
|
||||
{
|
||||
CancelConnection();
|
||||
}
|
||||
Cursor = GetCursorForMode(_editMode);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그리드 표시 여부
|
||||
/// </summary>
|
||||
public bool ShowGrid
|
||||
{
|
||||
get => _showGrid;
|
||||
set
|
||||
{
|
||||
_showGrid = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 줌 팩터
|
||||
/// </summary>
|
||||
public float ZoomFactor
|
||||
{
|
||||
get => _zoomFactor;
|
||||
set
|
||||
{
|
||||
_zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value));
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 선택된 노드
|
||||
/// </summary>
|
||||
public MapNode SelectedNode => _selectedNode;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 목록
|
||||
/// </summary>
|
||||
public List<MapNode> Nodes
|
||||
{
|
||||
get => _nodes ?? new List<MapNode>();
|
||||
set
|
||||
{
|
||||
_nodes = value ?? new List<MapNode>();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 목록
|
||||
/// </summary>
|
||||
public List<IAGV> AGVList
|
||||
{
|
||||
get => _agvList ?? new List<IAGV>();
|
||||
set
|
||||
{
|
||||
_agvList = value ?? new List<IAGV>();
|
||||
UpdateAGVData();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 표시할 경로
|
||||
/// </summary>
|
||||
public PathResult CurrentPath
|
||||
{
|
||||
get => _currentPath;
|
||||
set
|
||||
{
|
||||
_currentPath = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 경로 목록 (다중 AGV 경로 표시용)
|
||||
/// </summary>
|
||||
public List<PathResult> AllPaths
|
||||
{
|
||||
get => _allPaths ?? new List<PathResult>();
|
||||
set
|
||||
{
|
||||
_allPaths = value ?? new List<PathResult>();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 회사 로고 이미지
|
||||
/// </summary>
|
||||
public Image CompanyLogo
|
||||
{
|
||||
get => _companyLogo;
|
||||
set
|
||||
{
|
||||
_companyLogo = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 측정 정보 텍스트
|
||||
/// </summary>
|
||||
public string MeasurementInfo
|
||||
{
|
||||
get => _measurementInfo;
|
||||
set
|
||||
{
|
||||
_measurementInfo = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
public UnifiedAGVCanvas()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializeCanvas();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
|
||||
private void InitializeCanvas()
|
||||
{
|
||||
SetStyle(ControlStyles.AllPaintingInWmPaint |
|
||||
ControlStyles.UserPaint |
|
||||
ControlStyles.DoubleBuffer |
|
||||
ControlStyles.ResizeRedraw, true);
|
||||
|
||||
_nodes = new List<MapNode>();
|
||||
_agvList = new List<IAGV>();
|
||||
_agvPositions = new Dictionary<string, Point>();
|
||||
_agvDirections = new Dictionary<string, AgvDirection>();
|
||||
_agvStates = new Dictionary<string, AGVState>();
|
||||
_allPaths = new List<PathResult>();
|
||||
|
||||
InitializeBrushesAndPens();
|
||||
CreateContextMenu();
|
||||
}
|
||||
|
||||
private void InitializeBrushesAndPens()
|
||||
{
|
||||
// 노드 브러쉬
|
||||
_normalNodeBrush = new SolidBrush(Color.LightBlue);
|
||||
_rotationNodeBrush = new SolidBrush(Color.Yellow);
|
||||
_dockingNodeBrush = new SolidBrush(Color.Orange);
|
||||
_chargingNodeBrush = new SolidBrush(Color.Green);
|
||||
_selectedNodeBrush = new SolidBrush(Color.Red);
|
||||
_hoveredNodeBrush = new SolidBrush(Color.LightCyan);
|
||||
|
||||
// AGV 및 경로 브러쉬
|
||||
_agvBrush = new SolidBrush(Color.Red);
|
||||
_pathBrush = new SolidBrush(Color.Purple);
|
||||
|
||||
// 그리드 브러쉬
|
||||
_gridBrush = new SolidBrush(Color.LightGray);
|
||||
|
||||
// 펜
|
||||
_connectionPen = new Pen(Color.DarkBlue, CONNECTION_WIDTH);
|
||||
_connectionPen.EndCap = LineCap.ArrowAnchor;
|
||||
|
||||
_gridPen = new Pen(Color.LightGray, 1);
|
||||
_tempConnectionPen = new Pen(Color.Orange, 2) { DashStyle = DashStyle.Dash };
|
||||
_selectedNodePen = new Pen(Color.Red, 3);
|
||||
_pathPen = new Pen(Color.Purple, 3);
|
||||
_agvPen = new Pen(Color.Red, 3);
|
||||
}
|
||||
|
||||
private void CreateContextMenu()
|
||||
{
|
||||
_contextMenu = new ContextMenuStrip();
|
||||
// 컨텍스트 메뉴는 EditMode에서만 사용
|
||||
}
|
||||
|
||||
private void UpdateModeUI()
|
||||
{
|
||||
// 모드에 따른 UI 업데이트
|
||||
if (_canvasMode == CanvasMode.ViewOnly)
|
||||
{
|
||||
Cursor = Cursors.Default;
|
||||
_contextMenu.Enabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_contextMenu.Enabled = true;
|
||||
Cursor = GetCursorForMode(_editMode);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AGV Management
|
||||
|
||||
/// <summary>
|
||||
/// AGV 위치 업데이트
|
||||
/// </summary>
|
||||
public void UpdateAGVPosition(string agvId, Point position)
|
||||
{
|
||||
if (_agvPositions.ContainsKey(agvId))
|
||||
_agvPositions[agvId] = position;
|
||||
else
|
||||
_agvPositions.Add(agvId, position);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 방향 업데이트
|
||||
/// </summary>
|
||||
public void UpdateAGVDirection(string agvId, AgvDirection direction)
|
||||
{
|
||||
if (_agvDirections.ContainsKey(agvId))
|
||||
_agvDirections[agvId] = direction;
|
||||
else
|
||||
_agvDirections.Add(agvId, direction);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 상태 업데이트
|
||||
/// </summary>
|
||||
public void UpdateAGVState(string agvId, AGVState state)
|
||||
{
|
||||
if (_agvStates.ContainsKey(agvId))
|
||||
_agvStates[agvId] = state;
|
||||
else
|
||||
_agvStates.Add(agvId, state);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 위치 설정 (시뮬레이터용)
|
||||
/// </summary>
|
||||
/// <param name="agvId">AGV ID</param>
|
||||
/// <param name="position">새로운 위치</param>
|
||||
public void SetAGVPosition(string agvId, Point position)
|
||||
{
|
||||
UpdateAGVPosition(agvId, position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 데이터 동기화
|
||||
/// </summary>
|
||||
private void UpdateAGVData()
|
||||
{
|
||||
if (_agvList == null) return;
|
||||
|
||||
foreach (var agv in _agvList)
|
||||
{
|
||||
UpdateAGVPosition(agv.AgvId, agv.CurrentPosition);
|
||||
UpdateAGVDirection(agv.AgvId, agv.CurrentDirection);
|
||||
UpdateAGVState(agv.AgvId, agv.CurrentState);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private Cursor GetCursorForMode(EditMode mode)
|
||||
{
|
||||
if (_canvasMode != CanvasMode.Edit)
|
||||
return Cursors.Default;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case EditMode.AddNode:
|
||||
return Cursors.Cross;
|
||||
case EditMode.Move:
|
||||
return Cursors.SizeAll;
|
||||
case EditMode.Connect:
|
||||
return Cursors.Hand;
|
||||
case EditMode.Delete:
|
||||
return Cursors.No;
|
||||
default:
|
||||
return Cursors.Default;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelConnection()
|
||||
{
|
||||
_isConnectionMode = false;
|
||||
_connectionStartNode = null;
|
||||
_connectionEndPoint = Point.Empty;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cleanup
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
|
||||
// 브러쉬 정리
|
||||
_normalNodeBrush?.Dispose();
|
||||
_rotationNodeBrush?.Dispose();
|
||||
_dockingNodeBrush?.Dispose();
|
||||
_chargingNodeBrush?.Dispose();
|
||||
_selectedNodeBrush?.Dispose();
|
||||
_hoveredNodeBrush?.Dispose();
|
||||
_gridBrush?.Dispose();
|
||||
_agvBrush?.Dispose();
|
||||
_pathBrush?.Dispose();
|
||||
|
||||
// 펜 정리
|
||||
_connectionPen?.Dispose();
|
||||
_gridPen?.Dispose();
|
||||
_tempConnectionPen?.Dispose();
|
||||
_selectedNodePen?.Dispose();
|
||||
_pathPen?.Dispose();
|
||||
_agvPen?.Dispose();
|
||||
|
||||
// 컨텍스트 메뉴 정리
|
||||
_contextMenu?.Dispose();
|
||||
|
||||
// 이미지 정리
|
||||
_companyLogo?.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region Interfaces
|
||||
|
||||
/// <summary>
|
||||
/// AGV 인터페이스 (가상/실제 AGV 통합)
|
||||
/// </summary>
|
||||
public interface IAGV
|
||||
{
|
||||
string AgvId { get; }
|
||||
Point CurrentPosition { get; }
|
||||
AgvDirection CurrentDirection { get; }
|
||||
AGVState CurrentState { get; }
|
||||
float BatteryLevel { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 상태 열거형
|
||||
/// </summary>
|
||||
public enum AGVState
|
||||
{
|
||||
Idle, // 대기
|
||||
Moving, // 이동 중
|
||||
Rotating, // 회전 중
|
||||
Docking, // 도킹 중
|
||||
Charging, // 충전 중
|
||||
Error // 오류
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
68
Cs_HMI/AGVNavigationCore/Models/Enums.cs
Normal file
68
Cs_HMI/AGVNavigationCore/Models/Enums.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 노드 타입 열거형
|
||||
/// </summary>
|
||||
public enum NodeType
|
||||
{
|
||||
/// <summary>일반 경로 노드</summary>
|
||||
Normal,
|
||||
/// <summary>회전 가능 지점</summary>
|
||||
Rotation,
|
||||
/// <summary>도킹 스테이션</summary>
|
||||
Docking,
|
||||
/// <summary>충전 스테이션</summary>
|
||||
Charging,
|
||||
/// <summary>라벨 (UI 요소)</summary>
|
||||
Label,
|
||||
/// <summary>이미지 (UI 요소)</summary>
|
||||
Image
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향 열거형
|
||||
/// </summary>
|
||||
public enum DockingDirection
|
||||
{
|
||||
/// <summary>전진 도킹 (충전기)</summary>
|
||||
Forward,
|
||||
/// <summary>후진 도킹 (로더, 클리너, 오프로더, 버퍼)</summary>
|
||||
Backward
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 이동 방향 열거형
|
||||
/// </summary>
|
||||
public enum AgvDirection
|
||||
{
|
||||
/// <summary>전진 (모니터 방향)</summary>
|
||||
Forward,
|
||||
/// <summary>후진 (리프트 방향)</summary>
|
||||
Backward,
|
||||
/// <summary>좌회전</summary>
|
||||
Left,
|
||||
/// <summary>우회전</summary>
|
||||
Right,
|
||||
/// <summary>정지</summary>
|
||||
Stop
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 장비 타입 열거형
|
||||
/// </summary>
|
||||
public enum StationType
|
||||
{
|
||||
/// <summary>로더</summary>
|
||||
Loader,
|
||||
/// <summary>클리너</summary>
|
||||
Cleaner,
|
||||
/// <summary>오프로더</summary>
|
||||
Offloader,
|
||||
/// <summary>버퍼</summary>
|
||||
Buffer,
|
||||
/// <summary>충전기</summary>
|
||||
Charger
|
||||
}
|
||||
}
|
||||
144
Cs_HMI/AGVNavigationCore/Models/MapLoader.cs
Normal file
144
Cs_HMI/AGVNavigationCore/Models/MapLoader.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 맵 파일 로딩/저장을 위한 공용 유틸리티 클래스
|
||||
/// AGVMapEditor와 AGVSimulator에서 공통으로 사용
|
||||
/// </summary>
|
||||
public static class MapLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 파일 로딩 결과
|
||||
/// </summary>
|
||||
public class MapLoadResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
public string Version { get; set; } = string.Empty;
|
||||
public DateTime CreatedDate { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 파일 저장용 데이터 구조
|
||||
/// </summary>
|
||||
public class MapFileData
|
||||
{
|
||||
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public string Version { get; set; } = "1.0";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 파일을 로드하여 노드를 반환
|
||||
/// </summary>
|
||||
/// <param name="filePath">맵 파일 경로</param>
|
||||
/// <returns>로딩 결과</returns>
|
||||
public static MapLoadResult LoadMapFromFile(string filePath)
|
||||
{
|
||||
var result = new MapLoadResult();
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
result.ErrorMessage = $"파일을 찾을 수 없습니다: {filePath}";
|
||||
return result;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(filePath);
|
||||
var mapData = JsonConvert.DeserializeObject<MapFileData>(json);
|
||||
|
||||
if (mapData != null)
|
||||
{
|
||||
result.Nodes = mapData.Nodes ?? new List<MapNode>();
|
||||
result.Version = mapData.Version ?? "1.0";
|
||||
result.CreatedDate = mapData.CreatedDate;
|
||||
|
||||
// 이미지 노드들의 이미지 로드
|
||||
LoadImageNodes(result.Nodes);
|
||||
|
||||
result.Success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ErrorMessage = "맵 데이터 파싱에 실패했습니다.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.ErrorMessage = $"맵 파일 로딩 중 오류 발생: {ex.Message}";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 데이터를 파일로 저장
|
||||
/// </summary>
|
||||
/// <param name="filePath">저장할 파일 경로</param>
|
||||
/// <param name="nodes">맵 노드 목록</param>
|
||||
/// <returns>저장 성공 여부</returns>
|
||||
public static bool SaveMapToFile(string filePath, List<MapNode> nodes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var mapData = new MapFileData
|
||||
{
|
||||
Nodes = nodes,
|
||||
CreatedDate = DateTime.Now,
|
||||
Version = "1.0"
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(mapData, Formatting.Indented);
|
||||
File.WriteAllText(filePath, json);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 노드들의 이미지 로드
|
||||
/// </summary>
|
||||
/// <param name="nodes">노드 목록</param>
|
||||
private static void LoadImageNodes(List<MapNode> nodes)
|
||||
{
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node.Type == NodeType.Image)
|
||||
{
|
||||
node.LoadImage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MapNode 목록에서 RFID가 없는 노드들에 자동으로 RFID ID를 할당합니다.
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
public static void AssignAutoRfidIds(List<MapNode> mapNodes)
|
||||
{
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
// 네비게이션 가능한 노드이면서 RFID가 없는 경우에만 자동 할당
|
||||
if (node.IsNavigationNode() && !node.HasRfid())
|
||||
{
|
||||
// 기본 RFID ID 생성 (N001 -> 001)
|
||||
var rfidId = node.NodeId.Replace("N", "").PadLeft(3, '0');
|
||||
|
||||
node.SetRfidInfo(rfidId, "", "정상");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
486
Cs_HMI/AGVNavigationCore/Models/MapNode.cs
Normal file
486
Cs_HMI/AGVNavigationCore/Models/MapNode.cs
Normal file
@@ -0,0 +1,486 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 노드 정보를 관리하는 클래스
|
||||
/// 논리적 노드로서 실제 맵의 위치와 속성을 정의
|
||||
/// </summary>
|
||||
public class MapNode
|
||||
{
|
||||
/// <summary>
|
||||
/// 논리적 노드 ID (맵 에디터에서 관리하는 고유 ID)
|
||||
/// 예: "N001", "N002", "LOADER1", "CHARGER1"
|
||||
/// </summary>
|
||||
public string NodeId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 표시 이름 (사용자 친화적)
|
||||
/// 예: "로더1", "충전기1", "교차점A", "회전지점1"
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 맵 상의 위치 좌표 (픽셀 단위)
|
||||
/// </summary>
|
||||
public Point Position { get; set; } = Point.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 타입
|
||||
/// </summary>
|
||||
public NodeType Type { get; set; } = NodeType.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향 (도킹/충전 노드인 경우만 사용)
|
||||
/// </summary>
|
||||
public DockingDirection? DockDirection { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 연결된 노드 ID 목록 (경로 정보)
|
||||
/// </summary>
|
||||
public List<string> ConnectedNodes { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 회전 가능 여부 (180도 회전 가능한 지점)
|
||||
/// </summary>
|
||||
public bool CanRotate { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 장비 ID (도킹/충전 스테이션인 경우)
|
||||
/// 예: "LOADER1", "CLEANER1", "BUFFER1", "CHARGER1"
|
||||
/// </summary>
|
||||
public string StationId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 장비 타입 (도킹/충전 스테이션인 경우)
|
||||
/// </summary>
|
||||
public StationType? StationType { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 생성 일자
|
||||
/// </summary>
|
||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 수정 일자
|
||||
/// </summary>
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 설명 (추가 정보)
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 활성화 여부
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 색상 (맵 에디터 표시용)
|
||||
/// </summary>
|
||||
public Color DisplayColor { get; set; } = Color.Blue;
|
||||
|
||||
/// <summary>
|
||||
/// RFID 태그 ID (이 노드에 매핑된 RFID)
|
||||
/// </summary>
|
||||
public string RfidId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// RFID 상태 (정상, 손상, 교체예정 등)
|
||||
/// </summary>
|
||||
public string RfidStatus { get; set; } = "정상";
|
||||
|
||||
/// <summary>
|
||||
/// RFID 설치 위치 설명 (현장 작업자용)
|
||||
/// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등
|
||||
/// </summary>
|
||||
public string RfidDescription { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 텍스트 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public string LabelText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 폰트 패밀리 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public string FontFamily { get; set; } = "Arial";
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 폰트 크기 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public float FontSize { get; set; } = 12.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 폰트 스타일 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public FontStyle FontStyle { get; set; } = FontStyle.Regular;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 전경색 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public Color ForeColor { get; set; } = Color.Black;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 배경색 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public Color BackColor { get; set; } = Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 배경 표시 여부 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public bool ShowBackground { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 파일 경로 (NodeType.Image인 경우 사용)
|
||||
/// </summary>
|
||||
public string ImagePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 크기 배율 (NodeType.Image인 경우 사용)
|
||||
/// </summary>
|
||||
public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f);
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 투명도 (NodeType.Image인 경우 사용, 0.0~1.0)
|
||||
/// </summary>
|
||||
public float Opacity { get; set; } = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 회전 각도 (NodeType.Image인 경우 사용, 도 단위)
|
||||
/// </summary>
|
||||
public float Rotation { get; set; } = 0.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 로딩된 이미지 (런타임에서만 사용, JSON 직렬화 제외)
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
public Image LoadedImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public MapNode()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <param name="name">노드 이름</param>
|
||||
/// <param name="position">위치</param>
|
||||
/// <param name="type">노드 타입</param>
|
||||
public MapNode(string nodeId, string name, Point position, NodeType type)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
Name = name;
|
||||
Position = position;
|
||||
Type = type;
|
||||
CreatedDate = DateTime.Now;
|
||||
ModifiedDate = DateTime.Now;
|
||||
|
||||
// 타입별 기본 색상 설정
|
||||
SetDefaultColorByType(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 타입에 따른 기본 색상 설정
|
||||
/// </summary>
|
||||
/// <param name="type">노드 타입</param>
|
||||
public void SetDefaultColorByType(NodeType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case NodeType.Normal:
|
||||
DisplayColor = Color.Blue;
|
||||
break;
|
||||
case NodeType.Rotation:
|
||||
DisplayColor = Color.Orange;
|
||||
break;
|
||||
case NodeType.Docking:
|
||||
DisplayColor = Color.Green;
|
||||
break;
|
||||
case NodeType.Charging:
|
||||
DisplayColor = Color.Red;
|
||||
break;
|
||||
case NodeType.Label:
|
||||
DisplayColor = Color.Purple;
|
||||
break;
|
||||
case NodeType.Image:
|
||||
DisplayColor = Color.Brown;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다른 노드와의 연결 추가
|
||||
/// </summary>
|
||||
/// <param name="nodeId">연결할 노드 ID</param>
|
||||
public void AddConnection(string nodeId)
|
||||
{
|
||||
if (!ConnectedNodes.Contains(nodeId))
|
||||
{
|
||||
ConnectedNodes.Add(nodeId);
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다른 노드와의 연결 제거
|
||||
/// </summary>
|
||||
/// <param name="nodeId">연결 해제할 노드 ID</param>
|
||||
public void RemoveConnection(string nodeId)
|
||||
{
|
||||
if (ConnectedNodes.Remove(nodeId))
|
||||
{
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 스테이션 설정
|
||||
/// </summary>
|
||||
/// <param name="stationId">장비 ID</param>
|
||||
/// <param name="stationType">장비 타입</param>
|
||||
/// <param name="dockDirection">도킹 방향</param>
|
||||
public void SetDockingStation(string stationId, StationType stationType, DockingDirection dockDirection)
|
||||
{
|
||||
Type = NodeType.Docking;
|
||||
StationId = stationId;
|
||||
StationType = stationType;
|
||||
DockDirection = dockDirection;
|
||||
SetDefaultColorByType(NodeType.Docking);
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 충전 스테이션 설정
|
||||
/// </summary>
|
||||
/// <param name="stationId">충전기 ID</param>
|
||||
public void SetChargingStation(string stationId)
|
||||
{
|
||||
Type = NodeType.Charging;
|
||||
StationId = stationId;
|
||||
StationType = Models.StationType.Charger;
|
||||
DockDirection = DockingDirection.Forward; // 충전기는 항상 전진 도킹
|
||||
SetDefaultColorByType(NodeType.Charging);
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{NodeId}: {Name} ({Type}) at ({Position.X}, {Position.Y})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리스트박스 표시용 텍스트 (노드ID - 설명 - RFID 순서)
|
||||
/// </summary>
|
||||
public string DisplayText
|
||||
{
|
||||
get
|
||||
{
|
||||
var displayText = NodeId;
|
||||
|
||||
if (!string.IsNullOrEmpty(Description))
|
||||
{
|
||||
displayText += $" - {Description}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(RfidId))
|
||||
{
|
||||
displayText += $" - [{RfidId}]";
|
||||
}
|
||||
|
||||
return displayText;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 복사
|
||||
/// </summary>
|
||||
/// <returns>복사된 노드</returns>
|
||||
public MapNode Clone()
|
||||
{
|
||||
var clone = new MapNode
|
||||
{
|
||||
NodeId = NodeId,
|
||||
Name = Name,
|
||||
Position = Position,
|
||||
Type = Type,
|
||||
DockDirection = DockDirection,
|
||||
ConnectedNodes = new List<string>(ConnectedNodes),
|
||||
CanRotate = CanRotate,
|
||||
StationId = StationId,
|
||||
StationType = StationType,
|
||||
CreatedDate = CreatedDate,
|
||||
ModifiedDate = ModifiedDate,
|
||||
Description = Description,
|
||||
IsActive = IsActive,
|
||||
DisplayColor = DisplayColor,
|
||||
RfidId = RfidId,
|
||||
RfidStatus = RfidStatus,
|
||||
RfidDescription = RfidDescription,
|
||||
LabelText = LabelText,
|
||||
FontFamily = FontFamily,
|
||||
FontSize = FontSize,
|
||||
FontStyle = FontStyle,
|
||||
ForeColor = ForeColor,
|
||||
BackColor = BackColor,
|
||||
ShowBackground = ShowBackground,
|
||||
ImagePath = ImagePath,
|
||||
Scale = Scale,
|
||||
Opacity = Opacity,
|
||||
Rotation = Rotation
|
||||
};
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 로드 (256x256 이상일 경우 자동 리사이즈)
|
||||
/// </summary>
|
||||
/// <returns>로드 성공 여부</returns>
|
||||
public bool LoadImage()
|
||||
{
|
||||
if (Type != NodeType.Image) return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ImagePath) && System.IO.File.Exists(ImagePath))
|
||||
{
|
||||
LoadedImage?.Dispose();
|
||||
var originalImage = Image.FromFile(ImagePath);
|
||||
|
||||
// 이미지 크기 체크 및 리사이즈
|
||||
if (originalImage.Width > 256 || originalImage.Height > 256)
|
||||
{
|
||||
LoadedImage = ResizeImage(originalImage, 256, 256);
|
||||
originalImage.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadedImage = originalImage;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 이미지 로드 실패
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 리사이즈 (비율 유지)
|
||||
/// </summary>
|
||||
/// <param name="image">원본 이미지</param>
|
||||
/// <param name="maxWidth">최대 너비</param>
|
||||
/// <param name="maxHeight">최대 높이</param>
|
||||
/// <returns>리사이즈된 이미지</returns>
|
||||
private Image ResizeImage(Image image, int maxWidth, int maxHeight)
|
||||
{
|
||||
// 비율 계산
|
||||
double ratioX = (double)maxWidth / image.Width;
|
||||
double ratioY = (double)maxHeight / image.Height;
|
||||
double ratio = Math.Min(ratioX, ratioY);
|
||||
|
||||
// 새로운 크기 계산
|
||||
int newWidth = (int)(image.Width * ratio);
|
||||
int newHeight = (int)(image.Height * ratio);
|
||||
|
||||
// 리사이즈된 이미지 생성
|
||||
var resizedImage = new Bitmap(newWidth, newHeight);
|
||||
using (var graphics = Graphics.FromImage(resizedImage))
|
||||
{
|
||||
graphics.CompositingQuality = CompositingQuality.HighQuality;
|
||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
graphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||
graphics.DrawImage(image, 0, 0, newWidth, newHeight);
|
||||
}
|
||||
|
||||
return resizedImage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실제 표시될 크기 계산 (이미지 노드인 경우)
|
||||
/// </summary>
|
||||
/// <returns>실제 크기</returns>
|
||||
public Size GetDisplaySize()
|
||||
{
|
||||
if (Type != NodeType.Image || LoadedImage == null) return Size.Empty;
|
||||
|
||||
return new Size(
|
||||
(int)(LoadedImage.Width * Scale.Width),
|
||||
(int)(LoadedImage.Height * Scale.Height)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리소스 정리
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
LoadedImage?.Dispose();
|
||||
LoadedImage = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 찾기에 사용 가능한 노드인지 확인
|
||||
/// (라벨, 이미지 노드는 경로 찾기에서 제외)
|
||||
/// </summary>
|
||||
public bool IsNavigationNode()
|
||||
{
|
||||
return Type != NodeType.Label && Type != NodeType.Image && IsActive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID가 할당되어 있는지 확인
|
||||
/// </summary>
|
||||
public bool HasRfid()
|
||||
{
|
||||
return !string.IsNullOrEmpty(RfidId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID 정보 설정
|
||||
/// </summary>
|
||||
/// <param name="rfidId">RFID ID</param>
|
||||
/// <param name="rfidDescription">설치 위치 설명</param>
|
||||
/// <param name="rfidStatus">RFID 상태</param>
|
||||
public void SetRfidInfo(string rfidId, string rfidDescription = "", string rfidStatus = "정상")
|
||||
{
|
||||
RfidId = rfidId;
|
||||
RfidDescription = rfidDescription;
|
||||
RfidStatus = rfidStatus;
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID 정보 삭제
|
||||
/// </summary>
|
||||
public void ClearRfidInfo()
|
||||
{
|
||||
RfidId = string.Empty;
|
||||
RfidDescription = string.Empty;
|
||||
RfidStatus = "정상";
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID 기반 표시 텍스트 (RFID ID 우선, 없으면 노드ID)
|
||||
/// </summary>
|
||||
public string GetRfidDisplayText()
|
||||
{
|
||||
return HasRfid() ? RfidId : NodeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
79
Cs_HMI/AGVNavigationCore/Models/RfidMapping.cs
Normal file
79
Cs_HMI/AGVNavigationCore/Models/RfidMapping.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// RFID와 논리적 노드 ID를 매핑하는 클래스
|
||||
/// 물리적 RFID는 의미없는 고유값, 논리적 노드는 맵 에디터에서 관리
|
||||
/// </summary>
|
||||
public class RfidMapping
|
||||
{
|
||||
/// <summary>
|
||||
/// 물리적 RFID 값 (의미 없는 고유 식별자)
|
||||
/// 예: "1234567890", "ABCDEF1234" 등
|
||||
/// </summary>
|
||||
public string RfidId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 논리적 노드 ID (맵 에디터에서 관리)
|
||||
/// 예: "N001", "N002", "LOADER1", "CHARGER1" 등
|
||||
/// </summary>
|
||||
public string LogicalNodeId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 매핑 생성 일자
|
||||
/// </summary>
|
||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 수정 일자
|
||||
/// </summary>
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 설치 위치 설명 (현장 작업자용)
|
||||
/// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// RFID 상태 (정상, 손상, 교체예정 등)
|
||||
/// </summary>
|
||||
public string Status { get; set; } = "정상";
|
||||
|
||||
/// <summary>
|
||||
/// 매핑 활성화 여부
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public RfidMapping()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </summary>
|
||||
/// <param name="rfidId">물리적 RFID ID</param>
|
||||
/// <param name="logicalNodeId">논리적 노드 ID</param>
|
||||
/// <param name="description">설치 위치 설명</param>
|
||||
public RfidMapping(string rfidId, string logicalNodeId, string description = "")
|
||||
{
|
||||
RfidId = rfidId;
|
||||
LogicalNodeId = logicalNodeId;
|
||||
Description = description;
|
||||
CreatedDate = DateTime.Now;
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{RfidId} → {LogicalNodeId} ({Description})";
|
||||
}
|
||||
}
|
||||
}
|
||||
244
Cs_HMI/AGVNavigationCore/PathFinding/AGVPathResult.cs
Normal file
244
Cs_HMI/AGVNavigationCore/PathFinding/AGVPathResult.cs
Normal file
@@ -0,0 +1,244 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 경로 계산 결과 (방향성 및 명령어 포함)
|
||||
/// </summary>
|
||||
public class AGVPathResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 찾기 성공 여부
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 경로 노드 ID 목록 (시작 → 목적지 순서)
|
||||
/// </summary>
|
||||
public List<string> Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AGV 명령어 목록 (이동 방향 시퀀스)
|
||||
/// </summary>
|
||||
public List<AgvDirection> Commands { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 총 거리
|
||||
/// </summary>
|
||||
public float TotalDistance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 계산 소요 시간 (밀리초)
|
||||
/// </summary>
|
||||
public long CalculationTimeMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 예상 소요 시간 (초)
|
||||
/// </summary>
|
||||
public float EstimatedTimeSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 회전 횟수
|
||||
/// </summary>
|
||||
public int RotationCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 오류 메시지 (실패시)
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public AGVPathResult()
|
||||
{
|
||||
Success = false;
|
||||
Path = new List<string>();
|
||||
Commands = new List<AgvDirection>();
|
||||
TotalDistance = 0;
|
||||
CalculationTimeMs = 0;
|
||||
EstimatedTimeSeconds = 0;
|
||||
RotationCount = 0;
|
||||
ErrorMessage = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 성공 결과 생성
|
||||
/// </summary>
|
||||
/// <param name="path">경로</param>
|
||||
/// <param name="commands">AGV 명령어 목록</param>
|
||||
/// <param name="totalDistance">총 거리</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <returns>성공 결과</returns>
|
||||
public static AGVPathResult CreateSuccess(List<string> path, List<AgvDirection> commands, float totalDistance, long calculationTimeMs)
|
||||
{
|
||||
var result = new AGVPathResult
|
||||
{
|
||||
Success = true,
|
||||
Path = new List<string>(path),
|
||||
Commands = new List<AgvDirection>(commands),
|
||||
TotalDistance = totalDistance,
|
||||
CalculationTimeMs = calculationTimeMs
|
||||
};
|
||||
|
||||
result.CalculateMetrics();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실패 결과 생성
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">오류 메시지</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <returns>실패 결과</returns>
|
||||
public static AGVPathResult CreateFailure(string errorMessage, long calculationTimeMs)
|
||||
{
|
||||
return new AGVPathResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = errorMessage,
|
||||
CalculationTimeMs = calculationTimeMs
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 메트릭 계산
|
||||
/// </summary>
|
||||
private void CalculateMetrics()
|
||||
{
|
||||
RotationCount = CountRotations();
|
||||
EstimatedTimeSeconds = CalculateEstimatedTime();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 회전 횟수 계산
|
||||
/// </summary>
|
||||
private int CountRotations()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var command in Commands)
|
||||
{
|
||||
if (command == AgvDirection.Left || command == AgvDirection.Right)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 예상 소요 시간 계산
|
||||
/// </summary>
|
||||
/// <param name="agvSpeed">AGV 속도 (픽셀/초, 기본값: 100)</param>
|
||||
/// <param name="rotationTime">회전 시간 (초, 기본값: 3)</param>
|
||||
/// <returns>예상 소요 시간 (초)</returns>
|
||||
private float CalculateEstimatedTime(float agvSpeed = 100.0f, float rotationTime = 3.0f)
|
||||
{
|
||||
float moveTime = TotalDistance / agvSpeed;
|
||||
float totalRotationTime = RotationCount * rotationTime;
|
||||
return moveTime + totalRotationTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 명령어 요약 생성
|
||||
/// </summary>
|
||||
/// <returns>명령어 요약 문자열</returns>
|
||||
public string GetCommandSummary()
|
||||
{
|
||||
if (!Success) return "실패";
|
||||
|
||||
var summary = new List<string>();
|
||||
var currentCommand = AgvDirection.Stop;
|
||||
var count = 0;
|
||||
|
||||
foreach (var command in Commands)
|
||||
{
|
||||
if (command == currentCommand)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (count > 0)
|
||||
{
|
||||
summary.Add($"{GetCommandText(currentCommand)}×{count}");
|
||||
}
|
||||
currentCommand = command;
|
||||
count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
summary.Add($"{GetCommandText(currentCommand)}×{count}");
|
||||
}
|
||||
|
||||
return string.Join(" → ", summary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 명령어 텍스트 반환
|
||||
/// </summary>
|
||||
private string GetCommandText(AgvDirection command)
|
||||
{
|
||||
switch (command)
|
||||
{
|
||||
case AgvDirection.Forward: return "전진";
|
||||
case AgvDirection.Backward: return "후진";
|
||||
case AgvDirection.Left: return "좌회전";
|
||||
case AgvDirection.Right: return "우회전";
|
||||
case AgvDirection.Stop: return "정지";
|
||||
default: return command.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 상세 경로 정보 반환
|
||||
/// </summary>
|
||||
/// <returns>상세 정보 문자열</returns>
|
||||
public string GetDetailedInfo()
|
||||
{
|
||||
if (!Success)
|
||||
{
|
||||
return $"경로 계산 실패: {ErrorMessage} (계산시간: {CalculationTimeMs}ms)";
|
||||
}
|
||||
|
||||
return $"경로: {Path.Count}개 노드, 거리: {TotalDistance:F1}px, " +
|
||||
$"회전: {RotationCount}회, 예상시간: {EstimatedTimeSeconds:F1}초, " +
|
||||
$"계산시간: {CalculationTimeMs}ms";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PathResult로 변환 (호환성을 위해)
|
||||
/// </summary>
|
||||
/// <returns>PathResult 객체</returns>
|
||||
public PathResult ToPathResult()
|
||||
{
|
||||
if (Success)
|
||||
{
|
||||
return PathResult.CreateSuccess(Path, TotalDistance, CalculationTimeMs, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
return PathResult.CreateFailure(ErrorMessage, CalculationTimeMs, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
if (Success)
|
||||
{
|
||||
return $"Success: {Path.Count} nodes, {TotalDistance:F1}px, {RotationCount} rotations, {EstimatedTimeSeconds:F1}s";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Failed: {ErrorMessage}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
287
Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs
Normal file
287
Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs
Normal file
@@ -0,0 +1,287 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 특화 경로 탐색기 (방향성 및 도킹 제약 고려)
|
||||
/// </summary>
|
||||
public class AGVPathfinder
|
||||
{
|
||||
private AStarPathfinder _pathfinder;
|
||||
private Dictionary<string, MapNode> _nodeMap;
|
||||
|
||||
/// <summary>
|
||||
/// AGV 현재 방향
|
||||
/// </summary>
|
||||
public AgvDirection CurrentDirection { get; set; } = AgvDirection.Forward;
|
||||
|
||||
/// <summary>
|
||||
/// 회전 비용 가중치 (회전이 비싼 동작임을 반영)
|
||||
/// </summary>
|
||||
public float RotationCostWeight { get; set; } = 50.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 접근 거리 (픽셀 단위)
|
||||
/// </summary>
|
||||
public float DockingApproachDistance { get; set; } = 100.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 생성자
|
||||
/// </summary>
|
||||
public AGVPathfinder()
|
||||
{
|
||||
_pathfinder = new AStarPathfinder();
|
||||
_nodeMap = new Dictionary<string, MapNode>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 노드 설정
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
public void SetMapNodes(List<MapNode> mapNodes)
|
||||
{
|
||||
_pathfinder.SetMapNodes(mapNodes);
|
||||
_nodeMap.Clear();
|
||||
|
||||
foreach (var node in mapNodes ?? new List<MapNode>())
|
||||
{
|
||||
_nodeMap[node.NodeId] = node;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 경로 계산 (방향성 및 도킹 제약 고려)
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <param name="endNodeId">목적지 노드 ID</param>
|
||||
/// <param name="targetDirection">목적지 도착 방향 (null이면 자동 결정)</param>
|
||||
/// <returns>AGV 경로 계산 결과</returns>
|
||||
public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection = null)
|
||||
{
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
if (!_nodeMap.ContainsKey(startNodeId))
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"시작 노드를 찾을 수 없습니다: {startNodeId}", stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
if (!_nodeMap.ContainsKey(endNodeId))
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"목적지 노드를 찾을 수 없습니다: {endNodeId}", stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
var endNode = _nodeMap[endNodeId];
|
||||
|
||||
if (IsSpecialNode(endNode))
|
||||
{
|
||||
return FindPathToSpecialNode(startNodeId, endNode, targetDirection, stopwatch);
|
||||
}
|
||||
else
|
||||
{
|
||||
return FindNormalPath(startNodeId, endNodeId, targetDirection, stopwatch);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"AGV 경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 충전 스테이션으로의 경로 찾기
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <returns>AGV 경로 계산 결과</returns>
|
||||
public AGVPathResult FindPathToChargingStation(string startNodeId)
|
||||
{
|
||||
var chargingStations = _nodeMap.Values
|
||||
.Where(n => n.Type == NodeType.Charging && n.IsActive)
|
||||
.Select(n => n.NodeId)
|
||||
.ToList();
|
||||
|
||||
if (chargingStations.Count == 0)
|
||||
{
|
||||
return AGVPathResult.CreateFailure("사용 가능한 충전 스테이션이 없습니다", 0);
|
||||
}
|
||||
|
||||
var nearestResult = _pathfinder.FindNearestPath(startNodeId, chargingStations);
|
||||
if (!nearestResult.Success)
|
||||
{
|
||||
return AGVPathResult.CreateFailure("충전 스테이션으로의 경로를 찾을 수 없습니다", nearestResult.CalculationTimeMs);
|
||||
}
|
||||
|
||||
var targetNodeId = nearestResult.Path.Last();
|
||||
return FindAGVPath(startNodeId, targetNodeId, AgvDirection.Forward);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 타입의 도킹 스테이션으로의 경로 찾기
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <param name="stationType">장비 타입</param>
|
||||
/// <returns>AGV 경로 계산 결과</returns>
|
||||
public AGVPathResult FindPathToDockingStation(string startNodeId, StationType stationType)
|
||||
{
|
||||
var dockingStations = _nodeMap.Values
|
||||
.Where(n => n.Type == NodeType.Docking && n.StationType == stationType && n.IsActive)
|
||||
.Select(n => n.NodeId)
|
||||
.ToList();
|
||||
|
||||
if (dockingStations.Count == 0)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"{stationType} 타입의 사용 가능한 도킹 스테이션이 없습니다", 0);
|
||||
}
|
||||
|
||||
var nearestResult = _pathfinder.FindNearestPath(startNodeId, dockingStations);
|
||||
if (!nearestResult.Success)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"{stationType} 도킹 스테이션으로의 경로를 찾을 수 없습니다", nearestResult.CalculationTimeMs);
|
||||
}
|
||||
|
||||
var targetNodeId = nearestResult.Path.Last();
|
||||
return FindAGVPath(startNodeId, targetNodeId, AgvDirection.Backward);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 일반 노드로의 경로 계산
|
||||
/// </summary>
|
||||
private AGVPathResult FindNormalPath(string startNodeId, string endNodeId, AgvDirection? targetDirection, System.Diagnostics.Stopwatch stopwatch)
|
||||
{
|
||||
var result = _pathfinder.FindPath(startNodeId, endNodeId);
|
||||
if (!result.Success)
|
||||
{
|
||||
return AGVPathResult.CreateFailure(result.ErrorMessage, stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
var agvCommands = GenerateAGVCommands(result.Path, targetDirection ?? AgvDirection.Forward);
|
||||
return AGVPathResult.CreateSuccess(result.Path, agvCommands, result.TotalDistance, stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특수 노드(도킹/충전)로의 경로 계산
|
||||
/// </summary>
|
||||
private AGVPathResult FindPathToSpecialNode(string startNodeId, MapNode endNode, AgvDirection? targetDirection, System.Diagnostics.Stopwatch stopwatch)
|
||||
{
|
||||
var requiredDirection = GetRequiredDirectionForNode(endNode);
|
||||
var actualTargetDirection = targetDirection ?? requiredDirection;
|
||||
|
||||
var result = _pathfinder.FindPath(startNodeId, endNode.NodeId);
|
||||
if (!result.Success)
|
||||
{
|
||||
return AGVPathResult.CreateFailure(result.ErrorMessage, stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
if (actualTargetDirection != requiredDirection)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"{endNode.NodeId}는 {requiredDirection} 방향으로만 접근 가능합니다", stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
var agvCommands = GenerateAGVCommands(result.Path, actualTargetDirection);
|
||||
return AGVPathResult.CreateSuccess(result.Path, agvCommands, result.TotalDistance, stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드가 특수 노드(도킹/충전)인지 확인
|
||||
/// </summary>
|
||||
private bool IsSpecialNode(MapNode node)
|
||||
{
|
||||
return node.Type == NodeType.Docking || node.Type == NodeType.Charging;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드에 필요한 접근 방향 반환
|
||||
/// </summary>
|
||||
private AgvDirection GetRequiredDirectionForNode(MapNode node)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case NodeType.Charging:
|
||||
return AgvDirection.Forward;
|
||||
case NodeType.Docking:
|
||||
return node.DockDirection == DockingDirection.Forward ? AgvDirection.Forward : AgvDirection.Backward;
|
||||
default:
|
||||
return AgvDirection.Forward;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로에서 AGV 명령어 생성
|
||||
/// </summary>
|
||||
private List<AgvDirection> GenerateAGVCommands(List<string> path, AgvDirection targetDirection)
|
||||
{
|
||||
var commands = new List<AgvDirection>();
|
||||
if (path.Count < 2) return commands;
|
||||
|
||||
var currentDir = CurrentDirection;
|
||||
|
||||
for (int i = 0; i < path.Count - 1; i++)
|
||||
{
|
||||
var currentNodeId = path[i];
|
||||
var nextNodeId = path[i + 1];
|
||||
|
||||
if (_nodeMap.ContainsKey(currentNodeId) && _nodeMap.ContainsKey(nextNodeId))
|
||||
{
|
||||
var currentNode = _nodeMap[currentNodeId];
|
||||
var nextNode = _nodeMap[nextNodeId];
|
||||
|
||||
if (currentNode.CanRotate && ShouldRotate(currentDir, targetDirection))
|
||||
{
|
||||
commands.Add(GetRotationCommand(currentDir, targetDirection));
|
||||
currentDir = targetDirection;
|
||||
}
|
||||
|
||||
commands.Add(currentDir);
|
||||
}
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 회전이 필요한지 판단
|
||||
/// </summary>
|
||||
private bool ShouldRotate(AgvDirection current, AgvDirection target)
|
||||
{
|
||||
return current != target && (current == AgvDirection.Forward && target == AgvDirection.Backward ||
|
||||
current == AgvDirection.Backward && target == AgvDirection.Forward);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 회전 명령어 반환
|
||||
/// </summary>
|
||||
private AgvDirection GetRotationCommand(AgvDirection from, AgvDirection to)
|
||||
{
|
||||
if (from == AgvDirection.Forward && to == AgvDirection.Backward)
|
||||
return AgvDirection.Right;
|
||||
if (from == AgvDirection.Backward && to == AgvDirection.Forward)
|
||||
return AgvDirection.Right;
|
||||
|
||||
return AgvDirection.Right;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 유효성 검증
|
||||
/// </summary>
|
||||
/// <param name="path">검증할 경로</param>
|
||||
/// <returns>유효성 검증 결과</returns>
|
||||
public bool ValidatePath(List<string> path)
|
||||
{
|
||||
if (path == null || path.Count < 2) return true;
|
||||
|
||||
for (int i = 0; i < path.Count - 1; i++)
|
||||
{
|
||||
if (!_pathfinder.AreNodesConnected(path[i], path[i + 1]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
291
Cs_HMI/AGVNavigationCore/PathFinding/AStarPathfinder.cs
Normal file
291
Cs_HMI/AGVNavigationCore/PathFinding/AStarPathfinder.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding
|
||||
{
|
||||
/// <summary>
|
||||
/// A* 알고리즘 기반 경로 탐색기
|
||||
/// </summary>
|
||||
public class AStarPathfinder
|
||||
{
|
||||
private Dictionary<string, PathNode> _nodeMap;
|
||||
private List<MapNode> _mapNodes;
|
||||
|
||||
/// <summary>
|
||||
/// 휴리스틱 가중치 (기본값: 1.0)
|
||||
/// 값이 클수록 목적지 방향을 우선시하나 최적 경로를 놓칠 수 있음
|
||||
/// </summary>
|
||||
public float HeuristicWeight { get; set; } = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 최대 탐색 노드 수 (무한 루프 방지)
|
||||
/// </summary>
|
||||
public int MaxSearchNodes { get; set; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// 생성자
|
||||
/// </summary>
|
||||
public AStarPathfinder()
|
||||
{
|
||||
_nodeMap = new Dictionary<string, PathNode>();
|
||||
_mapNodes = new List<MapNode>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 노드 설정
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
public void SetMapNodes(List<MapNode> mapNodes)
|
||||
{
|
||||
_mapNodes = mapNodes ?? new List<MapNode>();
|
||||
_nodeMap.Clear();
|
||||
|
||||
// 1단계: 모든 네비게이션 노드를 PathNode로 변환
|
||||
foreach (var mapNode in _mapNodes)
|
||||
{
|
||||
if (mapNode.IsNavigationNode())
|
||||
{
|
||||
var pathNode = new PathNode(mapNode.NodeId, mapNode.Position);
|
||||
pathNode.ConnectedNodes = new List<string>(mapNode.ConnectedNodes);
|
||||
_nodeMap[mapNode.NodeId] = pathNode;
|
||||
}
|
||||
}
|
||||
|
||||
// 2단계: 양방향 연결 자동 생성 (A→B 연결이 있으면 B→A도 추가)
|
||||
EnsureBidirectionalConnections();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 단방향 연결을 양방향으로 자동 변환
|
||||
/// A→B 연결이 있으면 B→A 연결도 자동 생성
|
||||
/// </summary>
|
||||
private void EnsureBidirectionalConnections()
|
||||
{
|
||||
foreach (var nodeId in _nodeMap.Keys.ToList())
|
||||
{
|
||||
var node = _nodeMap[nodeId];
|
||||
foreach (var connectedNodeId in node.ConnectedNodes.ToList())
|
||||
{
|
||||
// 연결된 노드가 존재하고 네비게이션 가능한 노드인지 확인
|
||||
if (_nodeMap.ContainsKey(connectedNodeId))
|
||||
{
|
||||
var connectedNode = _nodeMap[connectedNodeId];
|
||||
// 역방향 연결이 없으면 추가
|
||||
if (!connectedNode.ConnectedNodes.Contains(nodeId))
|
||||
{
|
||||
connectedNode.ConnectedNodes.Add(nodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 찾기 (A* 알고리즘)
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <param name="endNodeId">목적지 노드 ID</param>
|
||||
/// <returns>경로 계산 결과</returns>
|
||||
public PathResult FindPath(string startNodeId, string endNodeId)
|
||||
{
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
if (!_nodeMap.ContainsKey(startNodeId))
|
||||
{
|
||||
return PathResult.CreateFailure($"시작 노드를 찾을 수 없습니다: {startNodeId}", stopwatch.ElapsedMilliseconds, 0);
|
||||
}
|
||||
|
||||
if (!_nodeMap.ContainsKey(endNodeId))
|
||||
{
|
||||
return PathResult.CreateFailure($"목적지 노드를 찾을 수 없습니다: {endNodeId}", stopwatch.ElapsedMilliseconds, 0);
|
||||
}
|
||||
|
||||
if (startNodeId == endNodeId)
|
||||
{
|
||||
return PathResult.CreateSuccess(new List<string> { startNodeId }, 0, stopwatch.ElapsedMilliseconds, 1);
|
||||
}
|
||||
|
||||
var startNode = _nodeMap[startNodeId];
|
||||
var endNode = _nodeMap[endNodeId];
|
||||
|
||||
var openSet = new List<PathNode>();
|
||||
var closedSet = new HashSet<string>();
|
||||
var exploredCount = 0;
|
||||
|
||||
startNode.GCost = 0;
|
||||
startNode.HCost = CalculateHeuristic(startNode, endNode);
|
||||
startNode.Parent = null;
|
||||
openSet.Add(startNode);
|
||||
|
||||
while (openSet.Count > 0 && exploredCount < MaxSearchNodes)
|
||||
{
|
||||
var currentNode = GetLowestFCostNode(openSet);
|
||||
openSet.Remove(currentNode);
|
||||
closedSet.Add(currentNode.NodeId);
|
||||
exploredCount++;
|
||||
|
||||
if (currentNode.NodeId == endNodeId)
|
||||
{
|
||||
var path = ReconstructPath(currentNode);
|
||||
var totalDistance = CalculatePathDistance(path);
|
||||
return PathResult.CreateSuccess(path, totalDistance, stopwatch.ElapsedMilliseconds, exploredCount);
|
||||
}
|
||||
|
||||
foreach (var neighborId in currentNode.ConnectedNodes)
|
||||
{
|
||||
if (closedSet.Contains(neighborId) || !_nodeMap.ContainsKey(neighborId))
|
||||
continue;
|
||||
|
||||
var neighbor = _nodeMap[neighborId];
|
||||
var tentativeGCost = currentNode.GCost + currentNode.DistanceTo(neighbor);
|
||||
|
||||
if (!openSet.Contains(neighbor))
|
||||
{
|
||||
neighbor.Parent = currentNode;
|
||||
neighbor.GCost = tentativeGCost;
|
||||
neighbor.HCost = CalculateHeuristic(neighbor, endNode);
|
||||
openSet.Add(neighbor);
|
||||
}
|
||||
else if (tentativeGCost < neighbor.GCost)
|
||||
{
|
||||
neighbor.Parent = currentNode;
|
||||
neighbor.GCost = tentativeGCost;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PathResult.CreateFailure("경로를 찾을 수 없습니다", stopwatch.ElapsedMilliseconds, exploredCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return PathResult.CreateFailure($"경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 여러 목적지 중 가장 가까운 노드로의 경로 찾기
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <param name="targetNodeIds">목적지 후보 노드 ID 목록</param>
|
||||
/// <returns>경로 계산 결과</returns>
|
||||
public PathResult FindNearestPath(string startNodeId, List<string> targetNodeIds)
|
||||
{
|
||||
if (targetNodeIds == null || targetNodeIds.Count == 0)
|
||||
{
|
||||
return PathResult.CreateFailure("목적지 노드가 지정되지 않았습니다", 0, 0);
|
||||
}
|
||||
|
||||
PathResult bestResult = null;
|
||||
foreach (var targetId in targetNodeIds)
|
||||
{
|
||||
var result = FindPath(startNodeId, targetId);
|
||||
if (result.Success && (bestResult == null || result.TotalDistance < bestResult.TotalDistance))
|
||||
{
|
||||
bestResult = result;
|
||||
}
|
||||
}
|
||||
|
||||
return bestResult ?? PathResult.CreateFailure("모든 목적지로의 경로를 찾을 수 없습니다", 0, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 휴리스틱 거리 계산 (유클리드 거리)
|
||||
/// </summary>
|
||||
private float CalculateHeuristic(PathNode from, PathNode to)
|
||||
{
|
||||
return from.DistanceTo(to) * HeuristicWeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F cost가 가장 낮은 노드 선택
|
||||
/// </summary>
|
||||
private PathNode GetLowestFCostNode(List<PathNode> nodes)
|
||||
{
|
||||
PathNode lowest = nodes[0];
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node.FCost < lowest.FCost ||
|
||||
(Math.Abs(node.FCost - lowest.FCost) < 0.001f && node.HCost < lowest.HCost))
|
||||
{
|
||||
lowest = node;
|
||||
}
|
||||
}
|
||||
return lowest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 재구성 (부모 노드를 따라 역추적)
|
||||
/// </summary>
|
||||
private List<string> ReconstructPath(PathNode endNode)
|
||||
{
|
||||
var path = new List<string>();
|
||||
var current = endNode;
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
path.Add(current.NodeId);
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
path.Reverse();
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로의 총 거리 계산
|
||||
/// </summary>
|
||||
private float CalculatePathDistance(List<string> path)
|
||||
{
|
||||
if (path.Count < 2) return 0;
|
||||
|
||||
float totalDistance = 0;
|
||||
for (int i = 0; i < path.Count - 1; i++)
|
||||
{
|
||||
if (_nodeMap.ContainsKey(path[i]) && _nodeMap.ContainsKey(path[i + 1]))
|
||||
{
|
||||
totalDistance += _nodeMap[path[i]].DistanceTo(_nodeMap[path[i + 1]]);
|
||||
}
|
||||
}
|
||||
|
||||
return totalDistance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 두 노드가 연결되어 있는지 확인
|
||||
/// </summary>
|
||||
/// <param name="nodeId1">노드 1 ID</param>
|
||||
/// <param name="nodeId2">노드 2 ID</param>
|
||||
/// <returns>연결 여부</returns>
|
||||
public bool AreNodesConnected(string nodeId1, string nodeId2)
|
||||
{
|
||||
if (!_nodeMap.ContainsKey(nodeId1) || !_nodeMap.ContainsKey(nodeId2))
|
||||
return false;
|
||||
|
||||
return _nodeMap[nodeId1].ConnectedNodes.Contains(nodeId2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 네비게이션 가능한 노드 목록 반환
|
||||
/// </summary>
|
||||
/// <returns>노드 ID 목록</returns>
|
||||
public List<string> GetNavigationNodes()
|
||||
{
|
||||
return _nodeMap.Keys.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 정보 반환
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <returns>노드 정보 또는 null</returns>
|
||||
public PathNode GetNode(string nodeId)
|
||||
{
|
||||
return _nodeMap.ContainsKey(nodeId) ? _nodeMap[nodeId] : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
101
Cs_HMI/AGVNavigationCore/PathFinding/PathNode.cs
Normal file
101
Cs_HMI/AGVNavigationCore/PathFinding/PathNode.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding
|
||||
{
|
||||
/// <summary>
|
||||
/// A* 알고리즘에서 사용하는 경로 노드
|
||||
/// </summary>
|
||||
public class PathNode
|
||||
{
|
||||
/// <summary>
|
||||
/// 노드 ID
|
||||
/// </summary>
|
||||
public string NodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 노드 위치
|
||||
/// </summary>
|
||||
public Point Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 시작점으로부터의 실제 거리 (G cost)
|
||||
/// </summary>
|
||||
public float GCost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 목적지까지의 추정 거리 (H cost - 휴리스틱)
|
||||
/// </summary>
|
||||
public float HCost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 총 비용 (F cost = G cost + H cost)
|
||||
/// </summary>
|
||||
public float FCost => GCost + HCost;
|
||||
|
||||
/// <summary>
|
||||
/// 부모 노드 (경로 추적용)
|
||||
/// </summary>
|
||||
public PathNode Parent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 연결된 노드 ID 목록
|
||||
/// </summary>
|
||||
public System.Collections.Generic.List<string> ConnectedNodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 생성자
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <param name="position">위치</param>
|
||||
public PathNode(string nodeId, Point position)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
Position = position;
|
||||
GCost = 0;
|
||||
HCost = 0;
|
||||
Parent = null;
|
||||
ConnectedNodes = new System.Collections.Generic.List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다른 노드까지의 유클리드 거리 계산
|
||||
/// </summary>
|
||||
/// <param name="other">대상 노드</param>
|
||||
/// <returns>거리</returns>
|
||||
public float DistanceTo(PathNode other)
|
||||
{
|
||||
float dx = Position.X - other.Position.X;
|
||||
float dy = Position.Y - other.Position.Y;
|
||||
return (float)Math.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{NodeId} - F:{FCost:F1} G:{GCost:F1} H:{HCost:F1}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 같음 비교 (NodeId 기준)
|
||||
/// </summary>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is PathNode other)
|
||||
{
|
||||
return NodeId == other.NodeId;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 해시코드 (NodeId 기준)
|
||||
/// </summary>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return NodeId?.GetHashCode() ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
107
Cs_HMI/AGVNavigationCore/PathFinding/PathResult.cs
Normal file
107
Cs_HMI/AGVNavigationCore/PathFinding/PathResult.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 계산 결과
|
||||
/// </summary>
|
||||
public class PathResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 찾기 성공 여부
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 경로 노드 ID 목록 (시작 → 목적지 순서)
|
||||
/// </summary>
|
||||
public List<string> Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 총 거리
|
||||
/// </summary>
|
||||
public float TotalDistance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 계산 소요 시간 (밀리초)
|
||||
/// </summary>
|
||||
public long CalculationTimeMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 탐색한 노드 수
|
||||
/// </summary>
|
||||
public int ExploredNodeCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 오류 메시지 (실패시)
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public PathResult()
|
||||
{
|
||||
Success = false;
|
||||
Path = new List<string>();
|
||||
TotalDistance = 0;
|
||||
CalculationTimeMs = 0;
|
||||
ExploredNodeCount = 0;
|
||||
ErrorMessage = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 성공 결과 생성
|
||||
/// </summary>
|
||||
/// <param name="path">경로</param>
|
||||
/// <param name="totalDistance">총 거리</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <param name="exploredNodeCount">탐색 노드 수</param>
|
||||
/// <returns>성공 결과</returns>
|
||||
public static PathResult CreateSuccess(List<string> path, float totalDistance, long calculationTimeMs, int exploredNodeCount)
|
||||
{
|
||||
return new PathResult
|
||||
{
|
||||
Success = true,
|
||||
Path = new List<string>(path),
|
||||
TotalDistance = totalDistance,
|
||||
CalculationTimeMs = calculationTimeMs,
|
||||
ExploredNodeCount = exploredNodeCount
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실패 결과 생성
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">오류 메시지</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <param name="exploredNodeCount">탐색 노드 수</param>
|
||||
/// <returns>실패 결과</returns>
|
||||
public static PathResult CreateFailure(string errorMessage, long calculationTimeMs, int exploredNodeCount)
|
||||
{
|
||||
return new PathResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = errorMessage,
|
||||
CalculationTimeMs = calculationTimeMs,
|
||||
ExploredNodeCount = exploredNodeCount
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
if (Success)
|
||||
{
|
||||
return $"Success: {Path.Count} nodes, {TotalDistance:F1}px, {CalculationTimeMs}ms";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Failed: {ErrorMessage}, {CalculationTimeMs}ms";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
275
Cs_HMI/AGVNavigationCore/PathFinding/RfidBasedPathfinder.cs
Normal file
275
Cs_HMI/AGVNavigationCore/PathFinding/RfidBasedPathfinder.cs
Normal file
@@ -0,0 +1,275 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding
|
||||
{
|
||||
/// <summary>
|
||||
/// RFID 기반 AGV 경로 탐색기
|
||||
/// 실제 현장에서 AGV가 RFID를 읽어서 위치를 파악하는 방식에 맞춤
|
||||
/// </summary>
|
||||
public class RfidBasedPathfinder
|
||||
{
|
||||
private AGVPathfinder _agvPathfinder;
|
||||
private AStarPathfinder _astarPathfinder;
|
||||
private Dictionary<string, string> _rfidToNodeMap; // RFID -> NodeId
|
||||
private Dictionary<string, string> _nodeToRfidMap; // NodeId -> RFID
|
||||
private List<MapNode> _mapNodes;
|
||||
|
||||
/// <summary>
|
||||
/// AGV 현재 방향
|
||||
/// </summary>
|
||||
public AgvDirection CurrentDirection
|
||||
{
|
||||
get => _agvPathfinder.CurrentDirection;
|
||||
set => _agvPathfinder.CurrentDirection = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 회전 비용 가중치
|
||||
/// </summary>
|
||||
public float RotationCostWeight
|
||||
{
|
||||
get => _agvPathfinder.RotationCostWeight;
|
||||
set => _agvPathfinder.RotationCostWeight = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 생성자
|
||||
/// </summary>
|
||||
public RfidBasedPathfinder()
|
||||
{
|
||||
_agvPathfinder = new AGVPathfinder();
|
||||
_astarPathfinder = new AStarPathfinder();
|
||||
_rfidToNodeMap = new Dictionary<string, string>();
|
||||
_nodeToRfidMap = new Dictionary<string, string>();
|
||||
_mapNodes = new List<MapNode>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 노드 설정 (MapNode의 RFID 정보 직접 사용)
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
public void SetMapNodes(List<MapNode> mapNodes)
|
||||
{
|
||||
// 기존 pathfinder에 맵 노드 설정
|
||||
_agvPathfinder.SetMapNodes(mapNodes);
|
||||
_astarPathfinder.SetMapNodes(mapNodes);
|
||||
|
||||
// MapNode의 RFID 정보로 매핑 구성
|
||||
_mapNodes = mapNodes ?? new List<MapNode>();
|
||||
_rfidToNodeMap.Clear();
|
||||
_nodeToRfidMap.Clear();
|
||||
|
||||
foreach (var node in _mapNodes.Where(n => n.IsActive && n.HasRfid()))
|
||||
{
|
||||
_rfidToNodeMap[node.RfidId] = node.NodeId;
|
||||
_nodeToRfidMap[node.NodeId] = node.RfidId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID 기반 AGV 경로 계산
|
||||
/// </summary>
|
||||
/// <param name="startRfidId">시작 RFID</param>
|
||||
/// <param name="endRfidId">목적지 RFID</param>
|
||||
/// <param name="targetDirection">목적지 도착 방향</param>
|
||||
/// <returns>RFID 기반 AGV 경로 계산 결과</returns>
|
||||
public RfidPathResult FindAGVPath(string startRfidId, string endRfidId, AgvDirection? targetDirection = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// RFID를 NodeId로 변환
|
||||
if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
|
||||
{
|
||||
return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
|
||||
}
|
||||
|
||||
if (!_rfidToNodeMap.TryGetValue(endRfidId, out string endNodeId))
|
||||
{
|
||||
return RfidPathResult.CreateFailure($"목적지 RFID를 찾을 수 없습니다: {endRfidId}", 0);
|
||||
}
|
||||
|
||||
// NodeId 기반으로 경로 계산
|
||||
var nodeResult = _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection);
|
||||
|
||||
// 결과를 RFID 기반으로 변환
|
||||
return ConvertToRfidResult(nodeResult, startRfidId, endRfidId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return RfidPathResult.CreateFailure($"RFID 기반 경로 계산 중 오류: {ex.Message}", 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 가장 가까운 충전소로의 RFID 기반 경로 찾기
|
||||
/// </summary>
|
||||
/// <param name="startRfidId">시작 RFID</param>
|
||||
/// <returns>RFID 기반 경로 계산 결과</returns>
|
||||
public RfidPathResult FindPathToChargingStation(string startRfidId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
|
||||
{
|
||||
return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
|
||||
}
|
||||
|
||||
var nodeResult = _agvPathfinder.FindPathToChargingStation(startNodeId);
|
||||
return ConvertToRfidResult(nodeResult, startRfidId, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return RfidPathResult.CreateFailure($"충전소 경로 계산 중 오류: {ex.Message}", 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 장비 타입의 도킹 스테이션으로의 RFID 기반 경로 찾기
|
||||
/// </summary>
|
||||
/// <param name="startRfidId">시작 RFID</param>
|
||||
/// <param name="stationType">장비 타입</param>
|
||||
/// <returns>RFID 기반 경로 계산 결과</returns>
|
||||
public RfidPathResult FindPathToDockingStation(string startRfidId, StationType stationType)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
|
||||
{
|
||||
return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
|
||||
}
|
||||
|
||||
var nodeResult = _agvPathfinder.FindPathToDockingStation(startNodeId, stationType);
|
||||
return ConvertToRfidResult(nodeResult, startRfidId, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return RfidPathResult.CreateFailure($"도킹 스테이션 경로 계산 중 오류: {ex.Message}", 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 여러 RFID 목적지 중 가장 가까운 곳으로의 경로 찾기
|
||||
/// </summary>
|
||||
/// <param name="startRfidId">시작 RFID</param>
|
||||
/// <param name="targetRfidIds">목적지 후보 RFID 목록</param>
|
||||
/// <returns>RFID 기반 경로 계산 결과</returns>
|
||||
public RfidPathResult FindNearestPath(string startRfidId, List<string> targetRfidIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
|
||||
{
|
||||
return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
|
||||
}
|
||||
|
||||
// RFID 목록을 NodeId 목록으로 변환
|
||||
var targetNodeIds = new List<string>();
|
||||
foreach (var rfidId in targetRfidIds)
|
||||
{
|
||||
if (_rfidToNodeMap.TryGetValue(rfidId, out string nodeId))
|
||||
{
|
||||
targetNodeIds.Add(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetNodeIds.Count == 0)
|
||||
{
|
||||
return RfidPathResult.CreateFailure("유효한 목적지 RFID가 없습니다", 0);
|
||||
}
|
||||
|
||||
var pathResult = _astarPathfinder.FindNearestPath(startNodeId, targetNodeIds);
|
||||
if (!pathResult.Success)
|
||||
{
|
||||
return RfidPathResult.CreateFailure(pathResult.ErrorMessage, pathResult.CalculationTimeMs);
|
||||
}
|
||||
|
||||
// AGV 명령어 생성을 위해 AGV pathfinder 사용
|
||||
var endNodeId = pathResult.Path.Last();
|
||||
var agvResult = _agvPathfinder.FindAGVPath(startNodeId, endNodeId);
|
||||
|
||||
return ConvertToRfidResult(agvResult, startRfidId, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return RfidPathResult.CreateFailure($"최근접 경로 계산 중 오류: {ex.Message}", 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID 매핑 상태 확인 (MapNode 기반)
|
||||
/// </summary>
|
||||
/// <param name="rfidId">확인할 RFID</param>
|
||||
/// <returns>MapNode 또는 null</returns>
|
||||
public MapNode GetRfidMapping(string rfidId)
|
||||
{
|
||||
return _mapNodes.FirstOrDefault(n => n.RfidId == rfidId && n.IsActive && n.HasRfid());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID로 NodeId 조회
|
||||
/// </summary>
|
||||
/// <param name="rfidId">RFID</param>
|
||||
/// <returns>NodeId 또는 null</returns>
|
||||
public string GetNodeIdByRfid(string rfidId)
|
||||
{
|
||||
return _rfidToNodeMap.TryGetValue(rfidId, out string nodeId) ? nodeId : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NodeId로 RFID 조회
|
||||
/// </summary>
|
||||
/// <param name="nodeId">NodeId</param>
|
||||
/// <returns>RFID 또는 null</returns>
|
||||
public string GetRfidByNodeId(string nodeId)
|
||||
{
|
||||
return _nodeToRfidMap.TryGetValue(nodeId, out string rfidId) ? rfidId : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 활성화된 RFID 목록 반환
|
||||
/// </summary>
|
||||
/// <returns>활성화된 RFID 목록</returns>
|
||||
public List<string> GetActiveRfidList()
|
||||
{
|
||||
return _mapNodes.Where(n => n.IsActive && n.HasRfid()).Select(n => n.RfidId).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NodeId 기반 결과를 RFID 기반 결과로 변환
|
||||
/// </summary>
|
||||
private RfidPathResult ConvertToRfidResult(AGVPathResult nodeResult, string startRfidId, string endRfidId)
|
||||
{
|
||||
if (!nodeResult.Success)
|
||||
{
|
||||
return RfidPathResult.CreateFailure(nodeResult.ErrorMessage, nodeResult.CalculationTimeMs);
|
||||
}
|
||||
|
||||
// NodeId 경로를 RFID 경로로 변환
|
||||
var rfidPath = new List<string>();
|
||||
foreach (var nodeId in nodeResult.Path)
|
||||
{
|
||||
if (_nodeToRfidMap.TryGetValue(nodeId, out string rfidId))
|
||||
{
|
||||
rfidPath.Add(rfidId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 매핑이 없는 경우 NodeId를 그대로 사용 (경고 로그 필요)
|
||||
rfidPath.Add($"[{nodeId}]");
|
||||
}
|
||||
}
|
||||
|
||||
return RfidPathResult.CreateSuccess(
|
||||
rfidPath,
|
||||
nodeResult.Commands,
|
||||
nodeResult.TotalDistance,
|
||||
nodeResult.CalculationTimeMs,
|
||||
nodeResult.EstimatedTimeSeconds,
|
||||
nodeResult.RotationCount
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
229
Cs_HMI/AGVNavigationCore/PathFinding/RfidPathResult.cs
Normal file
229
Cs_HMI/AGVNavigationCore/PathFinding/RfidPathResult.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding
|
||||
{
|
||||
/// <summary>
|
||||
/// RFID 기반 AGV 경로 계산 결과
|
||||
/// 실제 현장에서 AGV가 RFID를 기준으로 이동하는 방식에 맞춤
|
||||
/// </summary>
|
||||
public class RfidPathResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 찾기 성공 여부
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// RFID 경로 목록 (시작 → 목적지 순서)
|
||||
/// </summary>
|
||||
public List<string> RfidPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AGV 명령어 목록 (이동 방향 시퀀스)
|
||||
/// </summary>
|
||||
public List<AgvDirection> Commands { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 총 거리
|
||||
/// </summary>
|
||||
public float TotalDistance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 계산 소요 시간 (밀리초)
|
||||
/// </summary>
|
||||
public long CalculationTimeMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 예상 소요 시간 (초)
|
||||
/// </summary>
|
||||
public float EstimatedTimeSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 회전 횟수
|
||||
/// </summary>
|
||||
public int RotationCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 오류 메시지 (실패시)
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public RfidPathResult()
|
||||
{
|
||||
Success = false;
|
||||
RfidPath = new List<string>();
|
||||
Commands = new List<AgvDirection>();
|
||||
TotalDistance = 0;
|
||||
CalculationTimeMs = 0;
|
||||
EstimatedTimeSeconds = 0;
|
||||
RotationCount = 0;
|
||||
ErrorMessage = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 성공 결과 생성
|
||||
/// </summary>
|
||||
/// <param name="rfidPath">RFID 경로</param>
|
||||
/// <param name="commands">AGV 명령어 목록</param>
|
||||
/// <param name="totalDistance">총 거리</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <param name="estimatedTimeSeconds">예상 소요 시간</param>
|
||||
/// <param name="rotationCount">회전 횟수</param>
|
||||
/// <returns>성공 결과</returns>
|
||||
public static RfidPathResult CreateSuccess(
|
||||
List<string> rfidPath,
|
||||
List<AgvDirection> commands,
|
||||
float totalDistance,
|
||||
long calculationTimeMs,
|
||||
float estimatedTimeSeconds,
|
||||
int rotationCount)
|
||||
{
|
||||
return new RfidPathResult
|
||||
{
|
||||
Success = true,
|
||||
RfidPath = new List<string>(rfidPath),
|
||||
Commands = new List<AgvDirection>(commands),
|
||||
TotalDistance = totalDistance,
|
||||
CalculationTimeMs = calculationTimeMs,
|
||||
EstimatedTimeSeconds = estimatedTimeSeconds,
|
||||
RotationCount = rotationCount
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실패 결과 생성
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">오류 메시지</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <returns>실패 결과</returns>
|
||||
public static RfidPathResult CreateFailure(string errorMessage, long calculationTimeMs)
|
||||
{
|
||||
return new RfidPathResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = errorMessage,
|
||||
CalculationTimeMs = calculationTimeMs
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 명령어 요약 생성
|
||||
/// </summary>
|
||||
/// <returns>명령어 요약 문자열</returns>
|
||||
public string GetCommandSummary()
|
||||
{
|
||||
if (!Success) return "실패";
|
||||
|
||||
var summary = new List<string>();
|
||||
var currentCommand = AgvDirection.Stop;
|
||||
var count = 0;
|
||||
|
||||
foreach (var command in Commands)
|
||||
{
|
||||
if (command == currentCommand)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (count > 0)
|
||||
{
|
||||
summary.Add($"{GetCommandText(currentCommand)}×{count}");
|
||||
}
|
||||
currentCommand = command;
|
||||
count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
summary.Add($"{GetCommandText(currentCommand)}×{count}");
|
||||
}
|
||||
|
||||
return string.Join(" → ", summary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 명령어 텍스트 반환
|
||||
/// </summary>
|
||||
private string GetCommandText(AgvDirection command)
|
||||
{
|
||||
switch (command)
|
||||
{
|
||||
case AgvDirection.Forward: return "전진";
|
||||
case AgvDirection.Backward: return "후진";
|
||||
case AgvDirection.Left: return "좌회전";
|
||||
case AgvDirection.Right: return "우회전";
|
||||
case AgvDirection.Stop: return "정지";
|
||||
default: return command.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID 경로 요약 생성
|
||||
/// </summary>
|
||||
/// <returns>RFID 경로 요약 문자열</returns>
|
||||
public string GetRfidPathSummary()
|
||||
{
|
||||
if (!Success || RfidPath.Count == 0) return "경로 없음";
|
||||
|
||||
if (RfidPath.Count <= 3)
|
||||
{
|
||||
return string.Join(" → ", RfidPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{RfidPath[0]} → ... ({RfidPath.Count - 2}개 경유) → {RfidPath[RfidPath.Count - 1]}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 상세 경로 정보 반환
|
||||
/// </summary>
|
||||
/// <returns>상세 정보 문자열</returns>
|
||||
public string GetDetailedInfo()
|
||||
{
|
||||
if (!Success)
|
||||
{
|
||||
return $"RFID 경로 계산 실패: {ErrorMessage} (계산시간: {CalculationTimeMs}ms)";
|
||||
}
|
||||
|
||||
return $"RFID 경로: {RfidPath.Count}개 지점, 거리: {TotalDistance:F1}px, " +
|
||||
$"회전: {RotationCount}회, 예상시간: {EstimatedTimeSeconds:F1}초, " +
|
||||
$"계산시간: {CalculationTimeMs}ms";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 운영자용 실행 정보 반환
|
||||
/// </summary>
|
||||
/// <returns>실행 정보 문자열</returns>
|
||||
public string GetExecutionInfo()
|
||||
{
|
||||
if (!Success) return $"실행 불가: {ErrorMessage}";
|
||||
|
||||
return $"[실행준비] {GetRfidPathSummary()}\n" +
|
||||
$"[명령어] {GetCommandSummary()}\n" +
|
||||
$"[예상시간] {EstimatedTimeSeconds:F1}초";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
if (Success)
|
||||
{
|
||||
return $"Success: {RfidPath.Count} RFIDs, {TotalDistance:F1}px, {RotationCount} rotations, {EstimatedTimeSeconds:F1}s";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Failed: {ErrorMessage}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Cs_HMI/AGVNavigationCore/Properties/AssemblyInfo.cs
Normal file
36
Cs_HMI/AGVNavigationCore/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("AGVNavigationCore")]
|
||||
[assembly: AssemblyDescription("AGV Navigation and Pathfinding Core Library")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("ENIG")]
|
||||
[assembly: AssemblyProduct("AGV Navigation System")]
|
||||
[assembly: AssemblyCopyright("Copyright © ENIG 2024")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("c5f7a8b2-8d3e-4a1b-9c6e-7f4d5e2a9b1c")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
225
Cs_HMI/AGVNavigationCore/README.md
Normal file
225
Cs_HMI/AGVNavigationCore/README.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# AGVNavigationCore 프로젝트 기능 설명
|
||||
|
||||
## 📋 개요
|
||||
AGVNavigationCore는 AGV(Automated Guided Vehicle) 시스템을 위한 전문적인 경로 계산 및 네비게이션 라이브러리입니다. 실제 AGV의 물리적 제약사항과 운영 요구사항을 고려한 지능형 경로 탐색 기능을 제공합니다.
|
||||
|
||||
## 🏗️ 핵심 구조
|
||||
|
||||
### **Models 패키지**
|
||||
- **MapNode**: 맵의 논리적 노드 정보 (위치, 타입, 연결 정보, RFID 매핑)
|
||||
- **RfidMapping**: RFID와 논리적 노드 ID 간의 매핑 정보
|
||||
- **Enums**: 노드 타입, AGV 방향, 도킹 방향, 장비 타입 등 열거형 정의
|
||||
|
||||
### **PathFinding 패키지**
|
||||
경로 계산의 핵심 엔진들이 포함된 패키지
|
||||
|
||||
## 🎯 주요 기능
|
||||
|
||||
### 1. **기본 경로 탐색 (A* 알고리즘)**
|
||||
|
||||
**AStarPathfinder 클래스**:
|
||||
- 표준 A* 알고리즘 구현으로 최적 경로 탐색
|
||||
- 유클리드 거리 기반 휴리스틱 사용
|
||||
- 설정 가능한 파라미터:
|
||||
- `HeuristicWeight`: 휴리스틱 가중치 (기본 1.0)
|
||||
- `MaxSearchNodes`: 최대 탐색 노드 수 (기본 1000개)
|
||||
|
||||
**제공 기능**:
|
||||
```csharp
|
||||
// 단일 경로 탐색
|
||||
PathResult FindPath(string startNodeId, string endNodeId)
|
||||
|
||||
// 다중 목표 중 최단 경로 탐색
|
||||
PathResult FindNearestPath(string startNodeId, List<string> targetNodeIds)
|
||||
|
||||
// 노드 연결 상태 확인
|
||||
bool AreNodesConnected(string nodeId1, string nodeId2)
|
||||
```
|
||||
|
||||
### 2. **AGV 전용 지능형 경로 계산**
|
||||
|
||||
**AGVPathfinder 클래스**:
|
||||
AGV의 실제 움직임 제약사항을 고려한 전문 경로 계산기
|
||||
|
||||
**AGV 제약사항 고려**:
|
||||
- **방향성 제약**: 전진/후진만 가능, 좌우 이동 불가
|
||||
- **회전 제약**: 특정 노드(회전 가능 지점)에서만 180도 회전 가능
|
||||
- **도킹 방향**:
|
||||
- 충전기: 전진 도킹만 가능
|
||||
- 장비 (로더, 클리너 등): 후진 도킹만 가능
|
||||
|
||||
**전용 기능**:
|
||||
```csharp
|
||||
// AGV 경로 계산 (방향성 고려)
|
||||
AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection)
|
||||
|
||||
// 가장 가까운 충전소 경로
|
||||
AGVPathResult FindPathToChargingStation(string startNodeId)
|
||||
|
||||
// 특정 장비 타입 도킹 스테이션 경로
|
||||
AGVPathResult FindPathToDockingStation(string startNodeId, StationType stationType)
|
||||
```
|
||||
|
||||
### 3. **RFID 기반 경로 계산 (실제 운영용)**
|
||||
|
||||
**RfidBasedPathfinder 클래스**:
|
||||
실제 AGV가 RFID를 읽어서 위치를 파악하는 현장 운영 방식에 최적화
|
||||
|
||||
**RFID 기반 제약사항**:
|
||||
- **물리적 RFID**: 의미 없는 고유 식별자 (현장 유지보수 편의성)
|
||||
- **논리적 매핑**: RFID ↔ NodeId 분리로 맵 변경 시 유연성 확보
|
||||
- **실시간 변환**: RFID 입력을 내부적으로 NodeId로 변환하여 처리
|
||||
|
||||
**RFID 전용 기능**:
|
||||
```csharp
|
||||
// RFID 기반 AGV 경로 계산
|
||||
RfidPathResult FindAGVPath(string startRfidId, string endRfidId, AgvDirection? targetDirection)
|
||||
|
||||
// RFID 기반 충전소 경로
|
||||
RfidPathResult FindPathToChargingStation(string startRfidId)
|
||||
|
||||
// RFID 기반 도킹 스테이션 경로
|
||||
RfidPathResult FindPathToDockingStation(string startRfidId, StationType stationType)
|
||||
|
||||
// RFID 매핑 관리
|
||||
RfidMapping GetRfidMapping(string rfidId)
|
||||
string GetNodeIdByRfid(string rfidId)
|
||||
string GetRfidByNodeId(string nodeId)
|
||||
```
|
||||
|
||||
### 4. **상세한 결과 분석**
|
||||
|
||||
**PathResult (기본 결과)**:
|
||||
- 성공/실패 여부
|
||||
- 노드 ID 시퀀스
|
||||
- 총 거리 및 계산 시간
|
||||
- 탐색한 노드 수
|
||||
|
||||
**AGVPathResult (AGV 전용 결과)**:
|
||||
- **실행 가능한 명령어 시퀀스**: `[전진, 전진, 우회전, 후진, 정지]`
|
||||
- **상세 메트릭**:
|
||||
- 회전 횟수 계산
|
||||
- 예상 소요 시간 (이동 + 회전 시간)
|
||||
- 명령어 요약 (`전진×3 → 우회전×1 → 후진×2`)
|
||||
|
||||
**RfidPathResult (RFID 기반 결과)**:
|
||||
- **RFID 경로 시퀀스**: `[RFID001, RFID045, RFID067, RFID123]`
|
||||
- **AGV 명령어**: NodeId 기반 결과와 동일한 명령어 시퀀스
|
||||
- **현장 친화적 정보**:
|
||||
- RFID 경로 요약 (`RFID001 → ... (2개 경유) → RFID123`)
|
||||
- 실행 정보 (`[실행준비] → [명령어] → [예상시간]`)
|
||||
- 운영자용 상세 정보
|
||||
|
||||
### 5. **실시간 검증 및 최적화**
|
||||
|
||||
**경로 검증**:
|
||||
```csharp
|
||||
// 경로 유효성 실시간 검증
|
||||
bool ValidatePath(List<string> path)
|
||||
|
||||
// 네비게이션 가능 노드 필터링 (라벨/이미지 노드 제외)
|
||||
List<string> GetNavigationNodes()
|
||||
```
|
||||
|
||||
**성능 최적화**:
|
||||
- 메모리 효율적인 노드 관리
|
||||
- 조기 종료 조건으로 불필요한 탐색 방지
|
||||
- 캐시된 거리 계산
|
||||
|
||||
## 🔧 설정 가능한 파라미터
|
||||
|
||||
**AGV 동작 파라미터**:
|
||||
- `CurrentDirection`: AGV 현재 방향
|
||||
- `RotationCostWeight`: 회전 비용 가중치 (기본 50.0)
|
||||
- `DockingApproachDistance`: 도킹 접근 거리 (기본 100픽셀)
|
||||
|
||||
**알고리즘 파라미터**:
|
||||
- `HeuristicWeight`: A* 휴리스틱 강도
|
||||
- `MaxSearchNodes`: 탐색 제한으로 무한루프 방지
|
||||
|
||||
**RFID 매핑 파라미터**:
|
||||
- `RfidMappings`: RFID ↔ NodeId 매핑 테이블
|
||||
- `IsActive`: 매핑 활성화 상태
|
||||
- `Status`: RFID 상태 (정상, 손상, 교체예정)
|
||||
|
||||
## 🎯 실제 활용 시나리오
|
||||
|
||||
### **시나리오 1: 일반 이동 (NodeId 기반)**
|
||||
```
|
||||
현재위치(N001) → 목적지(N010)
|
||||
결과: [N001, N003, N007, N010] + [전진, 전진, 전진]
|
||||
```
|
||||
|
||||
### **시나리오 2: 일반 이동 (RFID 기반)**
|
||||
```
|
||||
현재위치(RFID123) → 목적지(RFID789)
|
||||
결과: [RFID123, RFID456, RFID789] + [전진, 우회전, 전진]
|
||||
AGV는 RFID를 읽으면서 실제 위치 확인 후 이동
|
||||
```
|
||||
|
||||
### **시나리오 3: 충전 필요**
|
||||
```
|
||||
배터리 부족 → 가장 가까운 충전소 자동 탐색
|
||||
결과: 충전소까지 최단경로 + 전진 도킹 명령어
|
||||
```
|
||||
|
||||
### **시나리오 4: 화물 적재**
|
||||
```
|
||||
로더 스테이션 접근 → 후진 도킹 필수
|
||||
결과: 로더까지 경로 + 후진 도킹 시퀀스
|
||||
```
|
||||
|
||||
## 🌟 차별화 포인트
|
||||
|
||||
1. **실제 AGV 제약사항 반영**: 이론적 경로가 아닌 실행 가능한 경로 제공
|
||||
2. **명령어 레벨 출력**: 경로뿐만 아니라 실제 AGV 제어 명령어 생성
|
||||
3. **RFID 기반 현장 운영**: 물리적 RFID와 논리적 노드 분리로 현장 유지보수성 향상
|
||||
4. **다양한 장비 지원**: 충전기, 로더, 클리너, 버퍼 등 각각의 도킹 요구사항 처리
|
||||
5. **이중 API 제공**: NodeId 기반(개발용) + RFID 기반(운영용) 동시 지원
|
||||
6. **확장성**: 새로운 AGV 타입이나 제약사항 쉽게 추가 가능
|
||||
7. **성능 최적화**: 실시간 운영에 적합한 빠른 응답속도
|
||||
|
||||
## 🚀 사용 방법
|
||||
|
||||
### 기본 사용법
|
||||
```csharp
|
||||
// 1. 경로 탐색기 초기화
|
||||
var pathfinder = new AGVPathfinder();
|
||||
pathfinder.SetMapNodes(mapNodes);
|
||||
|
||||
// 2. AGV 경로 계산
|
||||
var result = pathfinder.FindAGVPath("N001", "N010");
|
||||
|
||||
// 3. 결과 확인
|
||||
if (result.Success)
|
||||
{
|
||||
Console.WriteLine($"경로: {string.Join(" → ", result.Path)}");
|
||||
Console.WriteLine($"명령어: {result.GetCommandSummary()}");
|
||||
Console.WriteLine($"예상시간: {result.EstimatedTimeSeconds}초");
|
||||
}
|
||||
```
|
||||
|
||||
### 충전소 경로 탐색
|
||||
```csharp
|
||||
var chargingResult = pathfinder.FindPathToChargingStation("N001");
|
||||
if (chargingResult.Success)
|
||||
{
|
||||
// 충전소까지 자동 이동
|
||||
ExecuteAGVCommands(chargingResult.Commands);
|
||||
}
|
||||
```
|
||||
|
||||
## 📦 의존성
|
||||
- .NET Framework 4.8
|
||||
- Newtonsoft.Json 13.0.3
|
||||
- System.Drawing
|
||||
|
||||
## 🔗 통합 프로젝트
|
||||
이 라이브러리는 다음 프로젝트에서 사용됩니다:
|
||||
- **AGVMapEditor**: 맵 편집 및 경로 시뮬레이션
|
||||
- **AGV4**: 메인 AGV 제어 시스템
|
||||
- **AGVSimulator**: AGV 동작 시뮬레이터
|
||||
|
||||
---
|
||||
|
||||
*AGVNavigationCore는 ENIG AGV 시스템의 핵심 네비게이션 엔진입니다.*
|
||||
29
Cs_HMI/AGVNavigationCore/build.bat
Normal file
29
Cs_HMI/AGVNavigationCore/build.bat
Normal file
@@ -0,0 +1,29 @@
|
||||
@echo off
|
||||
echo Building V2GDecoder VC++ Project...
|
||||
|
||||
REM Check if Visual Studio 2022 is installed (Professional or Community)
|
||||
set MSBUILD_PRO="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_COM="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_BT="F:\(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\MSBuild.exe"
|
||||
|
||||
if exist %MSBUILD_PRO% (
|
||||
echo "Found Visual Studio 2022 Professional"
|
||||
set MSBUILD=%MSBUILD_PRO%
|
||||
) else if exist %MSBUILD_COM% (
|
||||
echo "Found Visual Studio 2022 Community"
|
||||
set MSBUILD=%MSBUILD_COM%
|
||||
) else if exist %MSBUILD_BT% (
|
||||
echo "Found Visual Studio 2022 BuildTools"
|
||||
set MSBUILD=%MSBUILD_BT%
|
||||
) else (
|
||||
echo "Visual Studio 2022 (Professional or Community) not found!"
|
||||
echo "Please install Visual Studio 2022 or update the MSBuild path."
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build Debug x64 configuration
|
||||
echo Building Debug x64 configuration...
|
||||
%MSBUILD% AGVNavigationCore.csproj
|
||||
|
||||
pause
|
||||
4
Cs_HMI/AGVNavigationCore/packages.config
Normal file
4
Cs_HMI/AGVNavigationCore/packages.config
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
||||
</packages>
|
||||
@@ -4,7 +4,7 @@
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{B2C3D4E5-F6G7-8901-BCDE-F23456789012}</ProjectGuid>
|
||||
<ProjectGuid>{B2C3D4E5-0000-0000-0000-000000000000}</ProjectGuid>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>AGVSimulator</RootNamespace>
|
||||
<AssemblyName>AGVSimulator</AssemblyName>
|
||||
@@ -42,14 +42,9 @@
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Models\SimulatorConfig.cs" />
|
||||
<Compile Include="Models\VirtualAGV.cs" />
|
||||
<Compile Include="Models\SimulationState.cs" />
|
||||
<Compile Include="Controls\SimulatorCanvas.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Controls\SimulatorCanvas.Designer.cs">
|
||||
<DependentUpon>SimulatorCanvas.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Forms\SimulatorForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
@@ -60,17 +55,19 @@
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Controls\SimulatorCanvas.resx">
|
||||
<DependentUpon>SimulatorCanvas.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Forms\SimulatorForm.resx">
|
||||
<DependentUpon>SimulatorForm.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="build.bat" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AGVNavigationCore\AGVNavigationCore.csproj">
|
||||
<Project>{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}</Project>
|
||||
<Name>AGVNavigationCore</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\AGVMapEditor\AGVMapEditor.csproj">
|
||||
<Project>{a1b2c3d4-e5f6-7890-abcd-ef1234567890}</Project>
|
||||
<Name>AGVMapEditor</Name>
|
||||
|
||||
@@ -4,6 +4,8 @@ using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AGVMapEditor.Models;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding;
|
||||
using AGVSimulator.Models;
|
||||
|
||||
namespace AGVSimulator.Controls
|
||||
@@ -388,13 +390,13 @@ namespace AGVSimulator.Controls
|
||||
|
||||
private void DrawPath(Graphics g)
|
||||
{
|
||||
if (_currentPath?.NodeSequence == null || _currentPath.NodeSequence.Count < 2)
|
||||
if (_currentPath?.Path == null || _currentPath.Path.Count < 2)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < _currentPath.NodeSequence.Count - 1; i++)
|
||||
for (int i = 0; i < _currentPath.Path.Count - 1; i++)
|
||||
{
|
||||
var currentNodeId = _currentPath.NodeSequence[i];
|
||||
var nextNodeId = _currentPath.NodeSequence[i + 1];
|
||||
var currentNodeId = _currentPath.Path[i];
|
||||
var nextNodeId = _currentPath.Path[i + 1];
|
||||
|
||||
var currentNode = _mapNodes?.FirstOrDefault(n => n.NodeId == currentNodeId);
|
||||
var nextNode = _mapNodes?.FirstOrDefault(n => n.NodeId == nextNodeId);
|
||||
@@ -583,11 +585,11 @@ namespace AGVSimulator.Controls
|
||||
if (_currentPath != null && _currentPath.Success)
|
||||
{
|
||||
y += 10;
|
||||
g.DrawString($"경로: {_currentPath.NodeSequence.Count}개 노드", font, brush, new PointF(10, y));
|
||||
g.DrawString($"경로: {_currentPath.Path.Count}개 노드", font, brush, new PointF(10, y));
|
||||
y += 15;
|
||||
g.DrawString($"거리: {_currentPath.TotalDistance:F1}", font, brush, new PointF(10, y));
|
||||
y += 15;
|
||||
g.DrawString($"계산시간: {_currentPath.CalculationTime}ms", font, brush, new PointF(10, y));
|
||||
g.DrawString($"계산시간: {_currentPath.CalculationTimeMs}ms", font, brush, new PointF(10, y));
|
||||
}
|
||||
|
||||
font.Dispose();
|
||||
|
||||
646
Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs
generated
646
Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs
generated
@@ -45,20 +45,666 @@ namespace AGVSimulator.Forms
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this._menuStrip = new System.Windows.Forms.MenuStrip();
|
||||
this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.openMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.reloadMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.launchMapEditorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.simulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.startSimulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.stopSimulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.resetToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.viewToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.fitToMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.resetZoomToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.helpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this._toolStrip = new System.Windows.Forms.ToolStrip();
|
||||
this.openMapToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.reloadMapToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.launchMapEditorToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.startSimulationToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.stopSimulationToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.resetToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.fitToMapToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.resetZoomToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this._statusStrip = new System.Windows.Forms.StatusStrip();
|
||||
this._statusLabel = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this._coordLabel = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this._controlPanel = new System.Windows.Forms.Panel();
|
||||
this._statusGroup = new System.Windows.Forms.GroupBox();
|
||||
this._pathLengthLabel = new System.Windows.Forms.Label();
|
||||
this._agvCountLabel = new System.Windows.Forms.Label();
|
||||
this._simulationStatusLabel = new System.Windows.Forms.Label();
|
||||
this._pathGroup = new System.Windows.Forms.GroupBox();
|
||||
this._clearPathButton = new System.Windows.Forms.Button();
|
||||
this._startPathButton = new System.Windows.Forms.Button();
|
||||
this._calculatePathButton = new System.Windows.Forms.Button();
|
||||
this._targetNodeCombo = new System.Windows.Forms.ComboBox();
|
||||
this.targetNodeLabel = new System.Windows.Forms.Label();
|
||||
this._startNodeCombo = new System.Windows.Forms.ComboBox();
|
||||
this.startNodeLabel = new System.Windows.Forms.Label();
|
||||
this._agvControlGroup = new System.Windows.Forms.GroupBox();
|
||||
this._setPositionButton = new System.Windows.Forms.Button();
|
||||
this._rfidTextBox = new System.Windows.Forms.TextBox();
|
||||
this._rfidLabel = new System.Windows.Forms.Label();
|
||||
this._stopSimulationButton = new System.Windows.Forms.Button();
|
||||
this._startSimulationButton = new System.Windows.Forms.Button();
|
||||
this._removeAgvButton = new System.Windows.Forms.Button();
|
||||
this._addAgvButton = new System.Windows.Forms.Button();
|
||||
this._agvListCombo = new System.Windows.Forms.ComboBox();
|
||||
this._canvasPanel = new System.Windows.Forms.Panel();
|
||||
this.btAllReset = new System.Windows.Forms.ToolStripButton();
|
||||
this._menuStrip.SuspendLayout();
|
||||
this._toolStrip.SuspendLayout();
|
||||
this._statusStrip.SuspendLayout();
|
||||
this._controlPanel.SuspendLayout();
|
||||
this._statusGroup.SuspendLayout();
|
||||
this._pathGroup.SuspendLayout();
|
||||
this._agvControlGroup.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// _menuStrip
|
||||
//
|
||||
this._menuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.fileToolStripMenuItem,
|
||||
this.simulationToolStripMenuItem,
|
||||
this.viewToolStripMenuItem,
|
||||
this.helpToolStripMenuItem});
|
||||
this._menuStrip.Location = new System.Drawing.Point(0, 0);
|
||||
this._menuStrip.Name = "_menuStrip";
|
||||
this._menuStrip.Size = new System.Drawing.Size(1200, 24);
|
||||
this._menuStrip.TabIndex = 0;
|
||||
this._menuStrip.Text = "menuStrip";
|
||||
//
|
||||
// fileToolStripMenuItem
|
||||
//
|
||||
this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.openMapToolStripMenuItem,
|
||||
this.reloadMapToolStripMenuItem,
|
||||
this.toolStripSeparator1,
|
||||
this.launchMapEditorToolStripMenuItem,
|
||||
this.toolStripSeparator4,
|
||||
this.exitToolStripMenuItem});
|
||||
this.fileToolStripMenuItem.Name = "fileToolStripMenuItem";
|
||||
this.fileToolStripMenuItem.Size = new System.Drawing.Size(57, 20);
|
||||
this.fileToolStripMenuItem.Text = "파일(&F)";
|
||||
//
|
||||
// openMapToolStripMenuItem
|
||||
//
|
||||
this.openMapToolStripMenuItem.Name = "openMapToolStripMenuItem";
|
||||
this.openMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O)));
|
||||
this.openMapToolStripMenuItem.Size = new System.Drawing.Size(183, 22);
|
||||
this.openMapToolStripMenuItem.Text = "맵 열기(&O)...";
|
||||
this.openMapToolStripMenuItem.Click += new System.EventHandler(this.OnOpenMap_Click);
|
||||
//
|
||||
// reloadMapToolStripMenuItem
|
||||
//
|
||||
this.reloadMapToolStripMenuItem.Name = "reloadMapToolStripMenuItem";
|
||||
this.reloadMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.R)));
|
||||
this.reloadMapToolStripMenuItem.Size = new System.Drawing.Size(183, 22);
|
||||
this.reloadMapToolStripMenuItem.Text = "맵 다시열기(&R)";
|
||||
this.reloadMapToolStripMenuItem.Click += new System.EventHandler(this.OnReloadMap_Click);
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
this.toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
this.toolStripSeparator1.Size = new System.Drawing.Size(180, 6);
|
||||
//
|
||||
// launchMapEditorToolStripMenuItem
|
||||
//
|
||||
this.launchMapEditorToolStripMenuItem.Name = "launchMapEditorToolStripMenuItem";
|
||||
this.launchMapEditorToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.M)));
|
||||
this.launchMapEditorToolStripMenuItem.Size = new System.Drawing.Size(183, 22);
|
||||
this.launchMapEditorToolStripMenuItem.Text = "MapEditor 실행(&M)";
|
||||
this.launchMapEditorToolStripMenuItem.Click += new System.EventHandler(this.OnLaunchMapEditor_Click);
|
||||
//
|
||||
// toolStripSeparator4
|
||||
//
|
||||
this.toolStripSeparator4.Name = "toolStripSeparator4";
|
||||
this.toolStripSeparator4.Size = new System.Drawing.Size(180, 6);
|
||||
//
|
||||
// exitToolStripMenuItem
|
||||
//
|
||||
this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
|
||||
this.exitToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Alt | System.Windows.Forms.Keys.F4)));
|
||||
this.exitToolStripMenuItem.Size = new System.Drawing.Size(183, 22);
|
||||
this.exitToolStripMenuItem.Text = "종료(&X)";
|
||||
this.exitToolStripMenuItem.Click += new System.EventHandler(this.OnExit_Click);
|
||||
//
|
||||
// simulationToolStripMenuItem
|
||||
//
|
||||
this.simulationToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.startSimulationToolStripMenuItem,
|
||||
this.stopSimulationToolStripMenuItem,
|
||||
this.resetToolStripMenuItem});
|
||||
this.simulationToolStripMenuItem.Name = "simulationToolStripMenuItem";
|
||||
this.simulationToolStripMenuItem.Size = new System.Drawing.Size(94, 20);
|
||||
this.simulationToolStripMenuItem.Text = "시뮬레이션(&S)";
|
||||
//
|
||||
// startSimulationToolStripMenuItem
|
||||
//
|
||||
this.startSimulationToolStripMenuItem.Name = "startSimulationToolStripMenuItem";
|
||||
this.startSimulationToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F5;
|
||||
this.startSimulationToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
|
||||
this.startSimulationToolStripMenuItem.Text = "시작(&S)";
|
||||
this.startSimulationToolStripMenuItem.Click += new System.EventHandler(this.OnStartSimulation_Click);
|
||||
//
|
||||
// stopSimulationToolStripMenuItem
|
||||
//
|
||||
this.stopSimulationToolStripMenuItem.Name = "stopSimulationToolStripMenuItem";
|
||||
this.stopSimulationToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F6;
|
||||
this.stopSimulationToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
|
||||
this.stopSimulationToolStripMenuItem.Text = "정지(&T)";
|
||||
this.stopSimulationToolStripMenuItem.Click += new System.EventHandler(this.OnStopSimulation_Click);
|
||||
//
|
||||
// resetToolStripMenuItem
|
||||
//
|
||||
this.resetToolStripMenuItem.Name = "resetToolStripMenuItem";
|
||||
this.resetToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F7;
|
||||
this.resetToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
|
||||
this.resetToolStripMenuItem.Text = "초기화(&R)";
|
||||
this.resetToolStripMenuItem.Click += new System.EventHandler(this.OnReset_Click);
|
||||
//
|
||||
// viewToolStripMenuItem
|
||||
//
|
||||
this.viewToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.fitToMapToolStripMenuItem,
|
||||
this.resetZoomToolStripMenuItem});
|
||||
this.viewToolStripMenuItem.Name = "viewToolStripMenuItem";
|
||||
this.viewToolStripMenuItem.Size = new System.Drawing.Size(59, 20);
|
||||
this.viewToolStripMenuItem.Text = "보기(&V)";
|
||||
//
|
||||
// fitToMapToolStripMenuItem
|
||||
//
|
||||
this.fitToMapToolStripMenuItem.Name = "fitToMapToolStripMenuItem";
|
||||
this.fitToMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.F)));
|
||||
this.fitToMapToolStripMenuItem.Size = new System.Drawing.Size(182, 22);
|
||||
this.fitToMapToolStripMenuItem.Text = "맵 맞춤(&F)";
|
||||
this.fitToMapToolStripMenuItem.Click += new System.EventHandler(this.OnFitToMap_Click);
|
||||
//
|
||||
// resetZoomToolStripMenuItem
|
||||
//
|
||||
this.resetZoomToolStripMenuItem.Name = "resetZoomToolStripMenuItem";
|
||||
this.resetZoomToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.D0)));
|
||||
this.resetZoomToolStripMenuItem.Size = new System.Drawing.Size(182, 22);
|
||||
this.resetZoomToolStripMenuItem.Text = "줌 초기화(&Z)";
|
||||
this.resetZoomToolStripMenuItem.Click += new System.EventHandler(this.OnResetZoom_Click);
|
||||
//
|
||||
// helpToolStripMenuItem
|
||||
//
|
||||
this.helpToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.aboutToolStripMenuItem});
|
||||
this.helpToolStripMenuItem.Name = "helpToolStripMenuItem";
|
||||
this.helpToolStripMenuItem.Size = new System.Drawing.Size(72, 20);
|
||||
this.helpToolStripMenuItem.Text = "도움말(&H)";
|
||||
//
|
||||
// aboutToolStripMenuItem
|
||||
//
|
||||
this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem";
|
||||
this.aboutToolStripMenuItem.Size = new System.Drawing.Size(123, 22);
|
||||
this.aboutToolStripMenuItem.Text = "정보(&A)...";
|
||||
this.aboutToolStripMenuItem.Click += new System.EventHandler(this.OnAbout_Click);
|
||||
//
|
||||
// _toolStrip
|
||||
//
|
||||
this._toolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.openMapToolStripButton,
|
||||
this.reloadMapToolStripButton,
|
||||
this.launchMapEditorToolStripButton,
|
||||
this.toolStripSeparator2,
|
||||
this.startSimulationToolStripButton,
|
||||
this.stopSimulationToolStripButton,
|
||||
this.resetToolStripButton,
|
||||
this.btAllReset,
|
||||
this.toolStripSeparator3,
|
||||
this.fitToMapToolStripButton,
|
||||
this.resetZoomToolStripButton});
|
||||
this._toolStrip.Location = new System.Drawing.Point(0, 24);
|
||||
this._toolStrip.Name = "_toolStrip";
|
||||
this._toolStrip.Size = new System.Drawing.Size(1200, 25);
|
||||
this._toolStrip.TabIndex = 1;
|
||||
this._toolStrip.Text = "toolStrip";
|
||||
//
|
||||
// openMapToolStripButton
|
||||
//
|
||||
this.openMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.openMapToolStripButton.Name = "openMapToolStripButton";
|
||||
this.openMapToolStripButton.Size = new System.Drawing.Size(51, 22);
|
||||
this.openMapToolStripButton.Text = "맵 열기";
|
||||
this.openMapToolStripButton.ToolTipText = "맵 파일을 엽니다";
|
||||
this.openMapToolStripButton.Click += new System.EventHandler(this.OnOpenMap_Click);
|
||||
//
|
||||
// reloadMapToolStripButton
|
||||
//
|
||||
this.reloadMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.reloadMapToolStripButton.Name = "reloadMapToolStripButton";
|
||||
this.reloadMapToolStripButton.Size = new System.Drawing.Size(63, 22);
|
||||
this.reloadMapToolStripButton.Text = "다시열기";
|
||||
this.reloadMapToolStripButton.ToolTipText = "현재 맵을 다시 로드합니다";
|
||||
this.reloadMapToolStripButton.Click += new System.EventHandler(this.OnReloadMap_Click);
|
||||
//
|
||||
// launchMapEditorToolStripButton
|
||||
//
|
||||
this.launchMapEditorToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.launchMapEditorToolStripButton.Name = "launchMapEditorToolStripButton";
|
||||
this.launchMapEditorToolStripButton.Size = new System.Drawing.Size(71, 22);
|
||||
this.launchMapEditorToolStripButton.Text = "MapEditor";
|
||||
this.launchMapEditorToolStripButton.ToolTipText = "MapEditor를 실행합니다";
|
||||
this.launchMapEditorToolStripButton.Click += new System.EventHandler(this.OnLaunchMapEditor_Click);
|
||||
//
|
||||
// toolStripSeparator2
|
||||
//
|
||||
this.toolStripSeparator2.Name = "toolStripSeparator2";
|
||||
this.toolStripSeparator2.Size = new System.Drawing.Size(6, 25);
|
||||
//
|
||||
// startSimulationToolStripButton
|
||||
//
|
||||
this.startSimulationToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.startSimulationToolStripButton.Name = "startSimulationToolStripButton";
|
||||
this.startSimulationToolStripButton.Size = new System.Drawing.Size(99, 22);
|
||||
this.startSimulationToolStripButton.Text = "시뮬레이션 시작";
|
||||
this.startSimulationToolStripButton.ToolTipText = "시뮬레이션을 시작합니다";
|
||||
this.startSimulationToolStripButton.Click += new System.EventHandler(this.OnStartSimulation_Click);
|
||||
//
|
||||
// stopSimulationToolStripButton
|
||||
//
|
||||
this.stopSimulationToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.stopSimulationToolStripButton.Name = "stopSimulationToolStripButton";
|
||||
this.stopSimulationToolStripButton.Size = new System.Drawing.Size(99, 22);
|
||||
this.stopSimulationToolStripButton.Text = "시뮬레이션 정지";
|
||||
this.stopSimulationToolStripButton.ToolTipText = "시뮬레이션을 정지합니다";
|
||||
this.stopSimulationToolStripButton.Click += new System.EventHandler(this.OnStopSimulation_Click);
|
||||
//
|
||||
// resetToolStripButton
|
||||
//
|
||||
this.resetToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.resetToolStripButton.Name = "resetToolStripButton";
|
||||
this.resetToolStripButton.Size = new System.Drawing.Size(47, 22);
|
||||
this.resetToolStripButton.Text = "초기화";
|
||||
this.resetToolStripButton.ToolTipText = "시뮬레이션을 초기화합니다";
|
||||
this.resetToolStripButton.Click += new System.EventHandler(this.OnReset_Click);
|
||||
//
|
||||
// toolStripSeparator3
|
||||
//
|
||||
this.toolStripSeparator3.Name = "toolStripSeparator3";
|
||||
this.toolStripSeparator3.Size = new System.Drawing.Size(6, 25);
|
||||
//
|
||||
// fitToMapToolStripButton
|
||||
//
|
||||
this.fitToMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.fitToMapToolStripButton.Name = "fitToMapToolStripButton";
|
||||
this.fitToMapToolStripButton.Size = new System.Drawing.Size(51, 22);
|
||||
this.fitToMapToolStripButton.Text = "맵 맞춤";
|
||||
this.fitToMapToolStripButton.ToolTipText = "맵 전체를 화면에 맞춥니다";
|
||||
this.fitToMapToolStripButton.Click += new System.EventHandler(this.OnFitToMap_Click);
|
||||
//
|
||||
// resetZoomToolStripButton
|
||||
//
|
||||
this.resetZoomToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.resetZoomToolStripButton.Name = "resetZoomToolStripButton";
|
||||
this.resetZoomToolStripButton.Size = new System.Drawing.Size(63, 22);
|
||||
this.resetZoomToolStripButton.Text = "줌 초기화";
|
||||
this.resetZoomToolStripButton.ToolTipText = "줌을 초기화합니다";
|
||||
this.resetZoomToolStripButton.Click += new System.EventHandler(this.OnResetZoom_Click);
|
||||
//
|
||||
// _statusStrip
|
||||
//
|
||||
this._statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this._statusLabel,
|
||||
this._coordLabel});
|
||||
this._statusStrip.Location = new System.Drawing.Point(0, 778);
|
||||
this._statusStrip.Name = "_statusStrip";
|
||||
this._statusStrip.Size = new System.Drawing.Size(1200, 22);
|
||||
this._statusStrip.TabIndex = 2;
|
||||
this._statusStrip.Text = "statusStrip";
|
||||
//
|
||||
// _statusLabel
|
||||
//
|
||||
this._statusLabel.Name = "_statusLabel";
|
||||
this._statusLabel.Size = new System.Drawing.Size(31, 17);
|
||||
this._statusLabel.Text = "준비";
|
||||
//
|
||||
// _coordLabel
|
||||
//
|
||||
this._coordLabel.Name = "_coordLabel";
|
||||
this._coordLabel.Size = new System.Drawing.Size(0, 17);
|
||||
//
|
||||
// _controlPanel
|
||||
//
|
||||
this._controlPanel.BackColor = System.Drawing.SystemColors.Control;
|
||||
this._controlPanel.Controls.Add(this._statusGroup);
|
||||
this._controlPanel.Controls.Add(this._pathGroup);
|
||||
this._controlPanel.Controls.Add(this._agvControlGroup);
|
||||
this._controlPanel.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
this._controlPanel.Location = new System.Drawing.Point(950, 49);
|
||||
this._controlPanel.Name = "_controlPanel";
|
||||
this._controlPanel.Size = new System.Drawing.Size(250, 729);
|
||||
this._controlPanel.TabIndex = 3;
|
||||
//
|
||||
// _statusGroup
|
||||
//
|
||||
this._statusGroup.Controls.Add(this._pathLengthLabel);
|
||||
this._statusGroup.Controls.Add(this._agvCountLabel);
|
||||
this._statusGroup.Controls.Add(this._simulationStatusLabel);
|
||||
this._statusGroup.Location = new System.Drawing.Point(10, 356);
|
||||
this._statusGroup.Name = "_statusGroup";
|
||||
this._statusGroup.Size = new System.Drawing.Size(230, 100);
|
||||
this._statusGroup.TabIndex = 3;
|
||||
this._statusGroup.TabStop = false;
|
||||
this._statusGroup.Text = "상태 정보";
|
||||
//
|
||||
// _pathLengthLabel
|
||||
//
|
||||
this._pathLengthLabel.AutoSize = true;
|
||||
this._pathLengthLabel.Location = new System.Drawing.Point(10, 65);
|
||||
this._pathLengthLabel.Name = "_pathLengthLabel";
|
||||
this._pathLengthLabel.Size = new System.Drawing.Size(71, 12);
|
||||
this._pathLengthLabel.TabIndex = 2;
|
||||
this._pathLengthLabel.Text = "경로 길이: -";
|
||||
//
|
||||
// _agvCountLabel
|
||||
//
|
||||
this._agvCountLabel.AutoSize = true;
|
||||
this._agvCountLabel.Location = new System.Drawing.Point(10, 45);
|
||||
this._agvCountLabel.Name = "_agvCountLabel";
|
||||
this._agvCountLabel.Size = new System.Drawing.Size(60, 12);
|
||||
this._agvCountLabel.TabIndex = 1;
|
||||
this._agvCountLabel.Text = "AGV 수: 0";
|
||||
//
|
||||
// _simulationStatusLabel
|
||||
//
|
||||
this._simulationStatusLabel.AutoSize = true;
|
||||
this._simulationStatusLabel.Location = new System.Drawing.Point(10, 25);
|
||||
this._simulationStatusLabel.Name = "_simulationStatusLabel";
|
||||
this._simulationStatusLabel.Size = new System.Drawing.Size(97, 12);
|
||||
this._simulationStatusLabel.TabIndex = 0;
|
||||
this._simulationStatusLabel.Text = "시뮬레이션: 정지";
|
||||
//
|
||||
// _pathGroup
|
||||
//
|
||||
this._pathGroup.Controls.Add(this._clearPathButton);
|
||||
this._pathGroup.Controls.Add(this._startPathButton);
|
||||
this._pathGroup.Controls.Add(this._calculatePathButton);
|
||||
this._pathGroup.Controls.Add(this._targetNodeCombo);
|
||||
this._pathGroup.Controls.Add(this.targetNodeLabel);
|
||||
this._pathGroup.Controls.Add(this._startNodeCombo);
|
||||
this._pathGroup.Controls.Add(this.startNodeLabel);
|
||||
this._pathGroup.Location = new System.Drawing.Point(10, 200);
|
||||
this._pathGroup.Name = "_pathGroup";
|
||||
this._pathGroup.Size = new System.Drawing.Size(230, 150);
|
||||
this._pathGroup.TabIndex = 1;
|
||||
this._pathGroup.TabStop = false;
|
||||
this._pathGroup.Text = "경로 제어";
|
||||
//
|
||||
// _clearPathButton
|
||||
//
|
||||
this._clearPathButton.Location = new System.Drawing.Point(150, 120);
|
||||
this._clearPathButton.Name = "_clearPathButton";
|
||||
this._clearPathButton.Size = new System.Drawing.Size(70, 25);
|
||||
this._clearPathButton.TabIndex = 6;
|
||||
this._clearPathButton.Text = "경로 지우기";
|
||||
this._clearPathButton.UseVisualStyleBackColor = true;
|
||||
this._clearPathButton.Click += new System.EventHandler(this.OnClearPath_Click);
|
||||
//
|
||||
// _startPathButton
|
||||
//
|
||||
this._startPathButton.Location = new System.Drawing.Point(80, 120);
|
||||
this._startPathButton.Name = "_startPathButton";
|
||||
this._startPathButton.Size = new System.Drawing.Size(65, 25);
|
||||
this._startPathButton.TabIndex = 5;
|
||||
this._startPathButton.Text = "경로 시작";
|
||||
this._startPathButton.UseVisualStyleBackColor = true;
|
||||
this._startPathButton.Click += new System.EventHandler(this.OnStartPath_Click);
|
||||
//
|
||||
// _calculatePathButton
|
||||
//
|
||||
this._calculatePathButton.Location = new System.Drawing.Point(10, 120);
|
||||
this._calculatePathButton.Name = "_calculatePathButton";
|
||||
this._calculatePathButton.Size = new System.Drawing.Size(65, 25);
|
||||
this._calculatePathButton.TabIndex = 4;
|
||||
this._calculatePathButton.Text = "경로 계산";
|
||||
this._calculatePathButton.UseVisualStyleBackColor = true;
|
||||
this._calculatePathButton.Click += new System.EventHandler(this.OnCalculatePath_Click);
|
||||
//
|
||||
// _targetNodeCombo
|
||||
//
|
||||
this._targetNodeCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this._targetNodeCombo.Location = new System.Drawing.Point(10, 95);
|
||||
this._targetNodeCombo.Name = "_targetNodeCombo";
|
||||
this._targetNodeCombo.Size = new System.Drawing.Size(210, 20);
|
||||
this._targetNodeCombo.TabIndex = 3;
|
||||
//
|
||||
// targetNodeLabel
|
||||
//
|
||||
this.targetNodeLabel.AutoSize = true;
|
||||
this.targetNodeLabel.Location = new System.Drawing.Point(10, 75);
|
||||
this.targetNodeLabel.Name = "targetNodeLabel";
|
||||
this.targetNodeLabel.Size = new System.Drawing.Size(63, 12);
|
||||
this.targetNodeLabel.TabIndex = 2;
|
||||
this.targetNodeLabel.Text = "목표 RFID:";
|
||||
//
|
||||
// _startNodeCombo
|
||||
//
|
||||
this._startNodeCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this._startNodeCombo.Location = new System.Drawing.Point(10, 45);
|
||||
this._startNodeCombo.Name = "_startNodeCombo";
|
||||
this._startNodeCombo.Size = new System.Drawing.Size(210, 20);
|
||||
this._startNodeCombo.TabIndex = 1;
|
||||
//
|
||||
// startNodeLabel
|
||||
//
|
||||
this.startNodeLabel.AutoSize = true;
|
||||
this.startNodeLabel.Location = new System.Drawing.Point(10, 25);
|
||||
this.startNodeLabel.Name = "startNodeLabel";
|
||||
this.startNodeLabel.Size = new System.Drawing.Size(63, 12);
|
||||
this.startNodeLabel.TabIndex = 0;
|
||||
this.startNodeLabel.Text = "시작 RFID:";
|
||||
//
|
||||
// _agvControlGroup
|
||||
//
|
||||
this._agvControlGroup.Controls.Add(this._setPositionButton);
|
||||
this._agvControlGroup.Controls.Add(this._rfidTextBox);
|
||||
this._agvControlGroup.Controls.Add(this._rfidLabel);
|
||||
this._agvControlGroup.Controls.Add(this._stopSimulationButton);
|
||||
this._agvControlGroup.Controls.Add(this._startSimulationButton);
|
||||
this._agvControlGroup.Controls.Add(this._removeAgvButton);
|
||||
this._agvControlGroup.Controls.Add(this._addAgvButton);
|
||||
this._agvControlGroup.Controls.Add(this._agvListCombo);
|
||||
this._agvControlGroup.Location = new System.Drawing.Point(10, 10);
|
||||
this._agvControlGroup.Name = "_agvControlGroup";
|
||||
this._agvControlGroup.Size = new System.Drawing.Size(230, 180);
|
||||
this._agvControlGroup.TabIndex = 0;
|
||||
this._agvControlGroup.TabStop = false;
|
||||
this._agvControlGroup.Text = "AGV 제어";
|
||||
//
|
||||
// _setPositionButton
|
||||
//
|
||||
this._setPositionButton.Location = new System.Drawing.Point(160, 138);
|
||||
this._setPositionButton.Name = "_setPositionButton";
|
||||
this._setPositionButton.Size = new System.Drawing.Size(60, 25);
|
||||
this._setPositionButton.TabIndex = 7;
|
||||
this._setPositionButton.Text = "위치설정";
|
||||
this._setPositionButton.UseVisualStyleBackColor = true;
|
||||
this._setPositionButton.Click += new System.EventHandler(this.OnSetPosition_Click);
|
||||
//
|
||||
// _rfidTextBox
|
||||
//
|
||||
this._rfidTextBox.Location = new System.Drawing.Point(10, 140);
|
||||
this._rfidTextBox.Name = "_rfidTextBox";
|
||||
this._rfidTextBox.Size = new System.Drawing.Size(140, 21);
|
||||
this._rfidTextBox.TabIndex = 6;
|
||||
this._rfidTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.OnRfidTextBox_KeyPress);
|
||||
//
|
||||
// _rfidLabel
|
||||
//
|
||||
this._rfidLabel.AutoSize = true;
|
||||
this._rfidLabel.Location = new System.Drawing.Point(10, 120);
|
||||
this._rfidLabel.Name = "_rfidLabel";
|
||||
this._rfidLabel.Size = new System.Drawing.Size(87, 12);
|
||||
this._rfidLabel.TabIndex = 5;
|
||||
this._rfidLabel.Text = "RFID 현재위치:";
|
||||
//
|
||||
// _stopSimulationButton
|
||||
//
|
||||
this._stopSimulationButton.Location = new System.Drawing.Point(120, 85);
|
||||
this._stopSimulationButton.Name = "_stopSimulationButton";
|
||||
this._stopSimulationButton.Size = new System.Drawing.Size(100, 25);
|
||||
this._stopSimulationButton.TabIndex = 4;
|
||||
this._stopSimulationButton.Text = "시뮬레이션 정지";
|
||||
this._stopSimulationButton.UseVisualStyleBackColor = true;
|
||||
this._stopSimulationButton.Click += new System.EventHandler(this.OnStopSimulation_Click);
|
||||
//
|
||||
// _startSimulationButton
|
||||
//
|
||||
this._startSimulationButton.Location = new System.Drawing.Point(10, 85);
|
||||
this._startSimulationButton.Name = "_startSimulationButton";
|
||||
this._startSimulationButton.Size = new System.Drawing.Size(100, 25);
|
||||
this._startSimulationButton.TabIndex = 3;
|
||||
this._startSimulationButton.Text = "시뮬레이션 시작";
|
||||
this._startSimulationButton.UseVisualStyleBackColor = true;
|
||||
this._startSimulationButton.Click += new System.EventHandler(this.OnStartSimulation_Click);
|
||||
//
|
||||
// _removeAgvButton
|
||||
//
|
||||
this._removeAgvButton.Location = new System.Drawing.Point(120, 55);
|
||||
this._removeAgvButton.Name = "_removeAgvButton";
|
||||
this._removeAgvButton.Size = new System.Drawing.Size(100, 25);
|
||||
this._removeAgvButton.TabIndex = 2;
|
||||
this._removeAgvButton.Text = "AGV 제거";
|
||||
this._removeAgvButton.UseVisualStyleBackColor = true;
|
||||
this._removeAgvButton.Click += new System.EventHandler(this.OnRemoveAGV_Click);
|
||||
//
|
||||
// _addAgvButton
|
||||
//
|
||||
this._addAgvButton.Location = new System.Drawing.Point(10, 55);
|
||||
this._addAgvButton.Name = "_addAgvButton";
|
||||
this._addAgvButton.Size = new System.Drawing.Size(100, 25);
|
||||
this._addAgvButton.TabIndex = 1;
|
||||
this._addAgvButton.Text = "AGV 추가";
|
||||
this._addAgvButton.UseVisualStyleBackColor = true;
|
||||
this._addAgvButton.Click += new System.EventHandler(this.OnAddAGV_Click);
|
||||
//
|
||||
// _agvListCombo
|
||||
//
|
||||
this._agvListCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this._agvListCombo.Location = new System.Drawing.Point(10, 25);
|
||||
this._agvListCombo.Name = "_agvListCombo";
|
||||
this._agvListCombo.Size = new System.Drawing.Size(210, 20);
|
||||
this._agvListCombo.TabIndex = 0;
|
||||
this._agvListCombo.SelectedIndexChanged += new System.EventHandler(this.OnAGVList_SelectedIndexChanged);
|
||||
//
|
||||
// _canvasPanel
|
||||
//
|
||||
this._canvasPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this._canvasPanel.Location = new System.Drawing.Point(0, 49);
|
||||
this._canvasPanel.Name = "_canvasPanel";
|
||||
this._canvasPanel.Size = new System.Drawing.Size(950, 729);
|
||||
this._canvasPanel.TabIndex = 4;
|
||||
//
|
||||
// btAllReset
|
||||
//
|
||||
this.btAllReset.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.btAllReset.Name = "btAllReset";
|
||||
this.btAllReset.Size = new System.Drawing.Size(71, 22);
|
||||
this.btAllReset.Text = "전체초기화";
|
||||
this.btAllReset.ToolTipText = "시뮬레이션을 초기화합니다";
|
||||
this.btAllReset.Click += new System.EventHandler(this.btAllReset_Click);
|
||||
//
|
||||
// SimulatorForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(1200, 800);
|
||||
this.Controls.Add(this._canvasPanel);
|
||||
this.Controls.Add(this._controlPanel);
|
||||
this.Controls.Add(this._statusStrip);
|
||||
this.Controls.Add(this._toolStrip);
|
||||
this.Controls.Add(this._menuStrip);
|
||||
this.MainMenuStrip = this._menuStrip;
|
||||
this.Name = "SimulatorForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "AGV 시뮬레이터";
|
||||
this.WindowState = System.Windows.Forms.FormWindowState.Maximized;
|
||||
this._menuStrip.ResumeLayout(false);
|
||||
this._menuStrip.PerformLayout();
|
||||
this._toolStrip.ResumeLayout(false);
|
||||
this._toolStrip.PerformLayout();
|
||||
this._statusStrip.ResumeLayout(false);
|
||||
this._statusStrip.PerformLayout();
|
||||
this._controlPanel.ResumeLayout(false);
|
||||
this._statusGroup.ResumeLayout(false);
|
||||
this._statusGroup.PerformLayout();
|
||||
this._pathGroup.ResumeLayout(false);
|
||||
this._pathGroup.PerformLayout();
|
||||
this._agvControlGroup.ResumeLayout(false);
|
||||
this._agvControlGroup.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.MenuStrip _menuStrip;
|
||||
private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem openMapToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
|
||||
private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem simulationToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem startSimulationToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem stopSimulationToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem resetToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem viewToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem fitToMapToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem resetZoomToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem helpToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem aboutToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStrip _toolStrip;
|
||||
private System.Windows.Forms.ToolStripButton openMapToolStripButton;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
|
||||
private System.Windows.Forms.ToolStripButton startSimulationToolStripButton;
|
||||
private System.Windows.Forms.ToolStripButton stopSimulationToolStripButton;
|
||||
private System.Windows.Forms.ToolStripButton resetToolStripButton;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
|
||||
private System.Windows.Forms.ToolStripButton fitToMapToolStripButton;
|
||||
private System.Windows.Forms.ToolStripButton resetZoomToolStripButton;
|
||||
private System.Windows.Forms.StatusStrip _statusStrip;
|
||||
private System.Windows.Forms.ToolStripStatusLabel _statusLabel;
|
||||
private System.Windows.Forms.ToolStripStatusLabel _coordLabel;
|
||||
private System.Windows.Forms.Panel _controlPanel;
|
||||
private System.Windows.Forms.GroupBox _agvControlGroup;
|
||||
private System.Windows.Forms.ComboBox _agvListCombo;
|
||||
private System.Windows.Forms.Button _addAgvButton;
|
||||
private System.Windows.Forms.Button _removeAgvButton;
|
||||
private System.Windows.Forms.Button _startSimulationButton;
|
||||
private System.Windows.Forms.Button _stopSimulationButton;
|
||||
private System.Windows.Forms.GroupBox _pathGroup;
|
||||
private System.Windows.Forms.Label startNodeLabel;
|
||||
private System.Windows.Forms.ComboBox _startNodeCombo;
|
||||
private System.Windows.Forms.Label targetNodeLabel;
|
||||
private System.Windows.Forms.ComboBox _targetNodeCombo;
|
||||
private System.Windows.Forms.Button _calculatePathButton;
|
||||
private System.Windows.Forms.Button _startPathButton;
|
||||
private System.Windows.Forms.Button _clearPathButton;
|
||||
private System.Windows.Forms.GroupBox _statusGroup;
|
||||
private System.Windows.Forms.Label _simulationStatusLabel;
|
||||
private System.Windows.Forms.Label _agvCountLabel;
|
||||
private System.Windows.Forms.Label _pathLengthLabel;
|
||||
private System.Windows.Forms.Panel _canvasPanel;
|
||||
private System.Windows.Forms.Label _rfidLabel;
|
||||
private System.Windows.Forms.TextBox _rfidTextBox;
|
||||
private System.Windows.Forms.Button _setPositionButton;
|
||||
private System.Windows.Forms.ToolStripButton btAllReset;
|
||||
private System.Windows.Forms.ToolStripMenuItem reloadMapToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem launchMapEditorToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
|
||||
private System.Windows.Forms.ToolStripButton reloadMapToolStripButton;
|
||||
private System.Windows.Forms.ToolStripButton launchMapEditorToolStripButton;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AGVMapEditor.Models;
|
||||
using AGVSimulator.Controls;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.Controls;
|
||||
using AGVSimulator.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@@ -18,50 +20,17 @@ namespace AGVSimulator.Forms
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private SimulatorCanvas _simulatorCanvas;
|
||||
private UnifiedAGVCanvas _simulatorCanvas;
|
||||
private List<MapNode> _mapNodes;
|
||||
private List<RfidMapping> _rfidMappings;
|
||||
private NodeResolver _nodeResolver;
|
||||
private PathCalculator _pathCalculator;
|
||||
private List<VirtualAGV> _agvList;
|
||||
private SimulationState _simulationState;
|
||||
private Timer _simulationTimer;
|
||||
private SimulatorConfig _config;
|
||||
private string _currentMapFilePath;
|
||||
|
||||
// UI Controls
|
||||
private MenuStrip _menuStrip;
|
||||
private ToolStrip _toolStrip;
|
||||
private StatusStrip _statusStrip;
|
||||
private Panel _controlPanel;
|
||||
private Panel _canvasPanel;
|
||||
|
||||
// Control Panel Controls
|
||||
private GroupBox _agvControlGroup;
|
||||
private ComboBox _agvListCombo;
|
||||
private Button _addAgvButton;
|
||||
private Button _removeAgvButton;
|
||||
private Button _startSimulationButton;
|
||||
private Button _stopSimulationButton;
|
||||
private Button _resetButton;
|
||||
|
||||
private GroupBox _pathGroup;
|
||||
private ComboBox _startNodeCombo;
|
||||
private ComboBox _targetNodeCombo;
|
||||
private Button _calculatePathButton;
|
||||
private Button _startPathButton;
|
||||
private Button _clearPathButton;
|
||||
|
||||
private GroupBox _viewGroup;
|
||||
private Button _fitToMapButton;
|
||||
private Button _resetZoomButton;
|
||||
|
||||
private GroupBox _statusGroup;
|
||||
private Label _simulationStatusLabel;
|
||||
private Label _agvCountLabel;
|
||||
private Label _pathLengthLabel;
|
||||
|
||||
// Status Labels
|
||||
private ToolStripStatusLabel _statusLabel;
|
||||
private ToolStripStatusLabel _coordLabel;
|
||||
// UI Controls - Designer에서 생성됨
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -88,27 +57,18 @@ namespace AGVSimulator.Forms
|
||||
|
||||
private void InitializeForm()
|
||||
{
|
||||
// 폼 설정
|
||||
Text = "AGV 시뮬레이터";
|
||||
Size = new Size(1200, 800);
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
// 설정 로드
|
||||
_config = SimulatorConfig.Load();
|
||||
|
||||
// 데이터 초기화
|
||||
_mapNodes = new List<MapNode>();
|
||||
_rfidMappings = new List<RfidMapping>();
|
||||
_agvList = new List<VirtualAGV>();
|
||||
_simulationState = new SimulationState();
|
||||
_currentMapFilePath = string.Empty;
|
||||
|
||||
// UI 컨트롤 생성
|
||||
CreateMenuStrip();
|
||||
CreateToolStrip();
|
||||
CreateStatusStrip();
|
||||
CreateControlPanel();
|
||||
// 시뮬레이터 캔버스 생성 (중앙 패널에만)
|
||||
CreateSimulatorCanvas();
|
||||
|
||||
// 레이아웃 설정
|
||||
SetupLayout();
|
||||
|
||||
// 타이머 초기화
|
||||
_simulationTimer = new Timer();
|
||||
_simulationTimer.Interval = 100; // 100ms 간격
|
||||
@@ -118,260 +78,21 @@ namespace AGVSimulator.Forms
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void CreateMenuStrip()
|
||||
{
|
||||
_menuStrip = new MenuStrip();
|
||||
|
||||
// 파일 메뉴
|
||||
var fileMenu = new ToolStripMenuItem("파일(&F)");
|
||||
fileMenu.DropDownItems.Add(new ToolStripMenuItem("맵 열기(&O)...", null, OnOpenMap_Click) { ShortcutKeys = Keys.Control | Keys.O });
|
||||
fileMenu.DropDownItems.Add(new ToolStripSeparator());
|
||||
fileMenu.DropDownItems.Add(new ToolStripMenuItem("종료(&X)", null, OnExit_Click) { ShortcutKeys = Keys.Alt | Keys.F4 });
|
||||
|
||||
// 시뮬레이션 메뉴
|
||||
var simMenu = new ToolStripMenuItem("시뮬레이션(&S)");
|
||||
simMenu.DropDownItems.Add(new ToolStripMenuItem("시작(&S)", null, OnStartSimulation_Click) { ShortcutKeys = Keys.F5 });
|
||||
simMenu.DropDownItems.Add(new ToolStripMenuItem("정지(&T)", null, OnStopSimulation_Click) { ShortcutKeys = Keys.F6 });
|
||||
simMenu.DropDownItems.Add(new ToolStripMenuItem("초기화(&R)", null, OnReset_Click) { ShortcutKeys = Keys.F7 });
|
||||
|
||||
// 보기 메뉴
|
||||
var viewMenu = new ToolStripMenuItem("보기(&V)");
|
||||
viewMenu.DropDownItems.Add(new ToolStripMenuItem("맵 맞춤(&F)", null, OnFitToMap_Click) { ShortcutKeys = Keys.Control | Keys.F });
|
||||
viewMenu.DropDownItems.Add(new ToolStripMenuItem("줌 초기화(&Z)", null, OnResetZoom_Click) { ShortcutKeys = Keys.Control | Keys.D0 });
|
||||
|
||||
// 도움말 메뉴
|
||||
var helpMenu = new ToolStripMenuItem("도움말(&H)");
|
||||
helpMenu.DropDownItems.Add(new ToolStripMenuItem("정보(&A)...", null, OnAbout_Click));
|
||||
|
||||
_menuStrip.Items.AddRange(new ToolStripItem[] { fileMenu, simMenu, viewMenu, helpMenu });
|
||||
Controls.Add(_menuStrip);
|
||||
MainMenuStrip = _menuStrip;
|
||||
}
|
||||
|
||||
private void CreateToolStrip()
|
||||
{
|
||||
_toolStrip = new ToolStrip();
|
||||
|
||||
_toolStrip.Items.Add(new ToolStripButton("맵 열기", null, OnOpenMap_Click) { ToolTipText = "맵 파일을 엽니다" });
|
||||
_toolStrip.Items.Add(new ToolStripSeparator());
|
||||
_toolStrip.Items.Add(new ToolStripButton("시뮬레이션 시작", null, OnStartSimulation_Click) { ToolTipText = "시뮬레이션을 시작합니다" });
|
||||
_toolStrip.Items.Add(new ToolStripButton("시뮬레이션 정지", null, OnStopSimulation_Click) { ToolTipText = "시뮬레이션을 정지합니다" });
|
||||
_toolStrip.Items.Add(new ToolStripButton("초기화", null, OnReset_Click) { ToolTipText = "시뮬레이션을 초기화합니다" });
|
||||
_toolStrip.Items.Add(new ToolStripSeparator());
|
||||
_toolStrip.Items.Add(new ToolStripButton("맵 맞춤", null, OnFitToMap_Click) { ToolTipText = "맵 전체를 화면에 맞춥니다" });
|
||||
_toolStrip.Items.Add(new ToolStripButton("줌 초기화", null, OnResetZoom_Click) { ToolTipText = "줌을 초기화합니다" });
|
||||
|
||||
Controls.Add(_toolStrip);
|
||||
}
|
||||
|
||||
private void CreateStatusStrip()
|
||||
{
|
||||
_statusStrip = new StatusStrip();
|
||||
|
||||
_statusLabel = new ToolStripStatusLabel("준비");
|
||||
_coordLabel = new ToolStripStatusLabel();
|
||||
|
||||
_statusStrip.Items.AddRange(new ToolStripItem[] { _statusLabel, _coordLabel });
|
||||
Controls.Add(_statusStrip);
|
||||
}
|
||||
|
||||
private void CreateControlPanel()
|
||||
{
|
||||
_controlPanel = new Panel();
|
||||
_controlPanel.Width = 250;
|
||||
_controlPanel.Dock = DockStyle.Right;
|
||||
_controlPanel.BackColor = SystemColors.Control;
|
||||
|
||||
// AGV 제어 그룹
|
||||
CreateAGVControlGroup();
|
||||
|
||||
// 경로 제어 그룹
|
||||
CreatePathControlGroup();
|
||||
|
||||
// 뷰 제어 그룹
|
||||
CreateViewControlGroup();
|
||||
|
||||
// 상태 그룹
|
||||
CreateStatusGroup();
|
||||
|
||||
Controls.Add(_controlPanel);
|
||||
}
|
||||
|
||||
private void CreateAGVControlGroup()
|
||||
{
|
||||
_agvControlGroup = new GroupBox();
|
||||
_agvControlGroup.Text = "AGV 제어";
|
||||
_agvControlGroup.Location = new Point(10, 10);
|
||||
_agvControlGroup.Size = new Size(230, 120);
|
||||
|
||||
_agvListCombo = new ComboBox();
|
||||
_agvListCombo.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
_agvListCombo.Location = new Point(10, 25);
|
||||
_agvListCombo.Size = new Size(210, 21);
|
||||
_agvListCombo.SelectedIndexChanged += OnAGVList_SelectedIndexChanged;
|
||||
|
||||
_addAgvButton = new Button();
|
||||
_addAgvButton.Text = "AGV 추가";
|
||||
_addAgvButton.Location = new Point(10, 55);
|
||||
_addAgvButton.Size = new Size(100, 25);
|
||||
_addAgvButton.Click += OnAddAGV_Click;
|
||||
|
||||
_removeAgvButton = new Button();
|
||||
_removeAgvButton.Text = "AGV 제거";
|
||||
_removeAgvButton.Location = new Point(120, 55);
|
||||
_removeAgvButton.Size = new Size(100, 25);
|
||||
_removeAgvButton.Click += OnRemoveAGV_Click;
|
||||
|
||||
_startSimulationButton = new Button();
|
||||
_startSimulationButton.Text = "시뮬레이션 시작";
|
||||
_startSimulationButton.Location = new Point(10, 85);
|
||||
_startSimulationButton.Size = new Size(100, 25);
|
||||
_startSimulationButton.Click += OnStartSimulation_Click;
|
||||
|
||||
_stopSimulationButton = new Button();
|
||||
_stopSimulationButton.Text = "시뮬레이션 정지";
|
||||
_stopSimulationButton.Location = new Point(120, 85);
|
||||
_stopSimulationButton.Size = new Size(100, 25);
|
||||
_stopSimulationButton.Click += OnStopSimulation_Click;
|
||||
|
||||
_agvControlGroup.Controls.AddRange(new Control[] {
|
||||
_agvListCombo, _addAgvButton, _removeAgvButton, _startSimulationButton, _stopSimulationButton
|
||||
});
|
||||
|
||||
_controlPanel.Controls.Add(_agvControlGroup);
|
||||
}
|
||||
|
||||
private void CreatePathControlGroup()
|
||||
{
|
||||
_pathGroup = new GroupBox();
|
||||
_pathGroup.Text = "경로 제어";
|
||||
_pathGroup.Location = new Point(10, 140);
|
||||
_pathGroup.Size = new Size(230, 150);
|
||||
|
||||
var startLabel = new Label();
|
||||
startLabel.Text = "시작 노드:";
|
||||
startLabel.Location = new Point(10, 25);
|
||||
startLabel.Size = new Size(70, 15);
|
||||
|
||||
_startNodeCombo = new ComboBox();
|
||||
_startNodeCombo.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
_startNodeCombo.Location = new Point(10, 45);
|
||||
_startNodeCombo.Size = new Size(210, 21);
|
||||
|
||||
var targetLabel = new Label();
|
||||
targetLabel.Text = "목표 노드:";
|
||||
targetLabel.Location = new Point(10, 75);
|
||||
targetLabel.Size = new Size(70, 15);
|
||||
|
||||
_targetNodeCombo = new ComboBox();
|
||||
_targetNodeCombo.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
_targetNodeCombo.Location = new Point(10, 95);
|
||||
_targetNodeCombo.Size = new Size(210, 21);
|
||||
|
||||
_calculatePathButton = new Button();
|
||||
_calculatePathButton.Text = "경로 계산";
|
||||
_calculatePathButton.Location = new Point(10, 120);
|
||||
_calculatePathButton.Size = new Size(65, 25);
|
||||
_calculatePathButton.Click += OnCalculatePath_Click;
|
||||
|
||||
_startPathButton = new Button();
|
||||
_startPathButton.Text = "경로 시작";
|
||||
_startPathButton.Location = new Point(80, 120);
|
||||
_startPathButton.Size = new Size(65, 25);
|
||||
_startPathButton.Click += OnStartPath_Click;
|
||||
|
||||
_clearPathButton = new Button();
|
||||
_clearPathButton.Text = "경로 지우기";
|
||||
_clearPathButton.Location = new Point(150, 120);
|
||||
_clearPathButton.Size = new Size(70, 25);
|
||||
_clearPathButton.Click += OnClearPath_Click;
|
||||
|
||||
_pathGroup.Controls.AddRange(new Control[] {
|
||||
startLabel, _startNodeCombo, targetLabel, _targetNodeCombo,
|
||||
_calculatePathButton, _startPathButton, _clearPathButton
|
||||
});
|
||||
|
||||
_controlPanel.Controls.Add(_pathGroup);
|
||||
}
|
||||
|
||||
private void CreateViewControlGroup()
|
||||
{
|
||||
_viewGroup = new GroupBox();
|
||||
_viewGroup.Text = "화면 제어";
|
||||
_viewGroup.Location = new Point(10, 300);
|
||||
_viewGroup.Size = new Size(230, 60);
|
||||
|
||||
_fitToMapButton = new Button();
|
||||
_fitToMapButton.Text = "맵 맞춤";
|
||||
_fitToMapButton.Location = new Point(10, 25);
|
||||
_fitToMapButton.Size = new Size(100, 25);
|
||||
_fitToMapButton.Click += OnFitToMap_Click;
|
||||
|
||||
_resetZoomButton = new Button();
|
||||
_resetZoomButton.Text = "줌 초기화";
|
||||
_resetZoomButton.Location = new Point(120, 25);
|
||||
_resetZoomButton.Size = new Size(100, 25);
|
||||
_resetZoomButton.Click += OnResetZoom_Click;
|
||||
|
||||
_resetButton = new Button();
|
||||
_resetButton.Text = "전체 초기화";
|
||||
_resetButton.Location = new Point(65, 55);
|
||||
_resetButton.Size = new Size(100, 25);
|
||||
_resetButton.Click += OnReset_Click;
|
||||
|
||||
_viewGroup.Controls.AddRange(new Control[] { _fitToMapButton, _resetZoomButton });
|
||||
|
||||
_controlPanel.Controls.Add(_viewGroup);
|
||||
}
|
||||
|
||||
private void CreateStatusGroup()
|
||||
{
|
||||
_statusGroup = new GroupBox();
|
||||
_statusGroup.Text = "상태 정보";
|
||||
_statusGroup.Location = new Point(10, 370);
|
||||
_statusGroup.Size = new Size(230, 100);
|
||||
|
||||
_simulationStatusLabel = new Label();
|
||||
_simulationStatusLabel.Text = "시뮬레이션: 정지";
|
||||
_simulationStatusLabel.Location = new Point(10, 25);
|
||||
_simulationStatusLabel.Size = new Size(210, 15);
|
||||
|
||||
_agvCountLabel = new Label();
|
||||
_agvCountLabel.Text = "AGV 수: 0";
|
||||
_agvCountLabel.Location = new Point(10, 45);
|
||||
_agvCountLabel.Size = new Size(210, 15);
|
||||
|
||||
_pathLengthLabel = new Label();
|
||||
_pathLengthLabel.Text = "경로 길이: -";
|
||||
_pathLengthLabel.Location = new Point(10, 65);
|
||||
_pathLengthLabel.Size = new Size(210, 15);
|
||||
|
||||
_statusGroup.Controls.AddRange(new Control[] {
|
||||
_simulationStatusLabel, _agvCountLabel, _pathLengthLabel
|
||||
});
|
||||
|
||||
_controlPanel.Controls.Add(_statusGroup);
|
||||
}
|
||||
|
||||
private void CreateSimulatorCanvas()
|
||||
{
|
||||
_canvasPanel = new Panel();
|
||||
_canvasPanel.Dock = DockStyle.Fill;
|
||||
|
||||
_simulatorCanvas = new SimulatorCanvas();
|
||||
_simulatorCanvas = new UnifiedAGVCanvas();
|
||||
_simulatorCanvas.Dock = DockStyle.Fill;
|
||||
_simulatorCanvas.Mode = UnifiedAGVCanvas.CanvasMode.ViewOnly;
|
||||
|
||||
_canvasPanel.Controls.Add(_simulatorCanvas);
|
||||
Controls.Add(_canvasPanel);
|
||||
}
|
||||
|
||||
private void SetupLayout()
|
||||
{
|
||||
// Z-Order 설정
|
||||
// Z-Order 설정 - 모든 컨트롤이 디자이너에 구현되어 자동 관리됨
|
||||
_canvasPanel.BringToFront();
|
||||
_controlPanel.BringToFront();
|
||||
_toolStrip.BringToFront();
|
||||
_menuStrip.BringToFront();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -382,7 +103,7 @@ namespace AGVSimulator.Forms
|
||||
{
|
||||
using (var openDialog = new OpenFileDialog())
|
||||
{
|
||||
openDialog.Filter = "맵 파일 (*.json)|*.json|모든 파일 (*.*)|*.*";
|
||||
openDialog.Filter = "AGV Map Files (*.agvmap)|*.agvmap|모든 파일 (*.*)|*.*";
|
||||
openDialog.Title = "맵 파일 열기";
|
||||
|
||||
if (openDialog.ShowDialog() == DialogResult.OK)
|
||||
@@ -430,30 +151,12 @@ namespace AGVSimulator.Forms
|
||||
|
||||
private void OnReset_Click(object sender, EventArgs e)
|
||||
{
|
||||
// 시뮬레이션 정지
|
||||
if (_simulationState.IsRunning)
|
||||
{
|
||||
OnStopSimulation_Click(sender, e);
|
||||
}
|
||||
|
||||
// AGV 초기화
|
||||
_simulatorCanvas.ClearAGVs();
|
||||
_agvList.Clear();
|
||||
|
||||
// 경로 초기화
|
||||
_simulatorCanvas.CurrentPath = null;
|
||||
|
||||
// UI 업데이트
|
||||
UpdateAGVComboBox();
|
||||
UpdateNodeComboBoxes();
|
||||
UpdateUI();
|
||||
|
||||
_statusLabel.Text = "초기화 완료";
|
||||
}
|
||||
|
||||
private void OnFitToMap_Click(object sender, EventArgs e)
|
||||
{
|
||||
_simulatorCanvas.FitToMap();
|
||||
_simulatorCanvas.FitToNodes();
|
||||
}
|
||||
|
||||
private void OnResetZoom_Click(object sender, EventArgs e)
|
||||
@@ -480,7 +183,7 @@ namespace AGVSimulator.Forms
|
||||
|
||||
var newAGV = new VirtualAGV(agvId, startPosition);
|
||||
_agvList.Add(newAGV);
|
||||
_simulatorCanvas.AddAGV(newAGV);
|
||||
_simulatorCanvas.AGVList = new List<IAGV>(_agvList.Cast<IAGV>());
|
||||
|
||||
UpdateAGVComboBox();
|
||||
UpdateUI();
|
||||
@@ -496,8 +199,8 @@ namespace AGVSimulator.Forms
|
||||
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
||||
if (selectedAGV != null)
|
||||
{
|
||||
_simulatorCanvas.RemoveAGV(selectedAGV.AgvId);
|
||||
_agvList.Remove(selectedAGV);
|
||||
_simulatorCanvas.AGVList = new List<IAGV>(_agvList.Cast<IAGV>());
|
||||
|
||||
UpdateAGVComboBox();
|
||||
UpdateUI();
|
||||
@@ -515,7 +218,7 @@ namespace AGVSimulator.Forms
|
||||
{
|
||||
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
|
||||
{
|
||||
MessageBox.Show("시작 노드와 목표 노드를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
MessageBox.Show("시작 RFID와 목표 RFID를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -524,20 +227,21 @@ namespace AGVSimulator.Forms
|
||||
|
||||
if (_pathCalculator == null)
|
||||
{
|
||||
_pathCalculator = new PathCalculator(_mapNodes, _nodeResolver);
|
||||
_pathCalculator = new PathCalculator();
|
||||
_pathCalculator.SetMapData(_mapNodes);
|
||||
}
|
||||
|
||||
var result = _pathCalculator.CalculatePath(startNode.NodeId, targetNode.NodeId, AgvDirection.Forward);
|
||||
var agvResult = _pathCalculator.FindAGVPath(startNode.NodeId, targetNode.NodeId);
|
||||
|
||||
if (result.Success)
|
||||
if (agvResult.Success)
|
||||
{
|
||||
_simulatorCanvas.CurrentPath = result;
|
||||
_pathLengthLabel.Text = $"경로 길이: {result.TotalDistance:F1}";
|
||||
_statusLabel.Text = $"경로 계산 완료 ({result.CalculationTime}ms)";
|
||||
_simulatorCanvas.CurrentPath = agvResult.ToPathResult();
|
||||
_pathLengthLabel.Text = $"경로 길이: {agvResult.TotalDistance:F1}";
|
||||
_statusLabel.Text = $"경로 계산 완료 ({agvResult.CalculationTimeMs}ms)";
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show($"경로를 찾을 수 없습니다:\n{result.ErrorMessage}", "경로 계산 실패",
|
||||
MessageBox.Show($"경로를 찾을 수 없습니다:\n{agvResult.ErrorMessage}", "경로 계산 실패",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
}
|
||||
@@ -568,6 +272,20 @@ namespace AGVSimulator.Forms
|
||||
_statusLabel.Text = "경로 지움";
|
||||
}
|
||||
|
||||
private void OnSetPosition_Click(object sender, EventArgs e)
|
||||
{
|
||||
SetAGVPositionByRfid();
|
||||
}
|
||||
|
||||
private void OnRfidTextBox_KeyPress(object sender, KeyPressEventArgs e)
|
||||
{
|
||||
if (e.KeyChar == (char)Keys.Enter)
|
||||
{
|
||||
SetAGVPositionByRfid();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSimulationTimer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
// 시뮬레이션 업데이트는 각 AGV의 내부 타이머에서 처리됨
|
||||
@@ -578,51 +296,103 @@ namespace AGVSimulator.Forms
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private void SetAGVPositionByRfid()
|
||||
{
|
||||
// 선택된 AGV 확인
|
||||
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
||||
if (selectedAGV == null)
|
||||
{
|
||||
MessageBox.Show("먼저 AGV를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
// RFID 값 확인
|
||||
var rfidId = _rfidTextBox.Text.Trim();
|
||||
if (string.IsNullOrEmpty(rfidId))
|
||||
{
|
||||
MessageBox.Show("RFID 값을 입력해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
// RFID에 해당하는 노드 직접 찾기
|
||||
var targetNode = _mapNodes?.FirstOrDefault(n => n.RfidId.Equals(rfidId, StringComparison.OrdinalIgnoreCase));
|
||||
if (targetNode == null)
|
||||
{
|
||||
MessageBox.Show($"RFID '{rfidId}'에 해당하는 노드를 찾을 수 없습니다.\n\n사용 가능한 RFID 목록:\n{GetAvailableRfidList()}",
|
||||
"RFID 찾기 실패", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// AGV 위치 설정
|
||||
_simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, targetNode.Position);
|
||||
|
||||
_statusLabel.Text = $"{selectedAGV.AgvId} 위치를 RFID '{rfidId}' (노드: {targetNode.NodeId})로 설정했습니다.";
|
||||
_rfidTextBox.Text = ""; // 입력 필드 초기화
|
||||
|
||||
// 시뮬레이터 캔버스의 해당 노드로 이동
|
||||
_simulatorCanvas.PanToNode(targetNode.NodeId);
|
||||
}
|
||||
|
||||
private string GetAvailableRfidList()
|
||||
{
|
||||
if (_mapNodes == null || _mapNodes.Count == 0)
|
||||
return "매핑된 RFID가 없습니다.";
|
||||
|
||||
var nodesWithRfid = _mapNodes.Where(n => n.HasRfid()).ToList();
|
||||
if (nodesWithRfid.Count == 0)
|
||||
return "RFID가 할당된 노드가 없습니다.";
|
||||
|
||||
// 처음 10개의 RFID만 표시
|
||||
var rfidList = nodesWithRfid.Take(10).Select(n => $"- {n.RfidId} → {n.NodeId}");
|
||||
var result = string.Join("\n", rfidList);
|
||||
|
||||
if (nodesWithRfid.Count > 10)
|
||||
result += $"\n... 외 {nodesWithRfid.Count - 10}개";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void LoadMapFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
var result = MapLoader.LoadMapFromFile(filePath);
|
||||
|
||||
// 구조체로 직접 역직렬화
|
||||
var mapData = JsonConvert.DeserializeObject<MapFileData>(json);
|
||||
|
||||
if (mapData != null)
|
||||
if (result.Success)
|
||||
{
|
||||
_mapNodes = mapData.MapNodes ?? new List<MapNode>();
|
||||
_rfidMappings = mapData.RfidMappings ?? new List<RfidMapping>();
|
||||
_mapNodes = result.Nodes;
|
||||
_currentMapFilePath = filePath;
|
||||
|
||||
// RFID가 없는 노드들에 자동 할당
|
||||
MapLoader.AssignAutoRfidIds(_mapNodes);
|
||||
|
||||
// 시뮬레이터 캔버스에 맵 설정
|
||||
_simulatorCanvas.Nodes = _mapNodes;
|
||||
|
||||
// 설정에 마지막 맵 파일 경로 저장
|
||||
_config.LastMapFilePath = filePath;
|
||||
if (_config.AutoSave)
|
||||
{
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
// UI 업데이트
|
||||
UpdateNodeComboBoxes();
|
||||
UpdateUI();
|
||||
|
||||
// 맵에 맞춤
|
||||
_simulatorCanvas.FitToNodes();
|
||||
}
|
||||
else
|
||||
{
|
||||
_mapNodes = new List<MapNode>();
|
||||
_rfidMappings = new List<RfidMapping>();
|
||||
throw new InvalidOperationException($"맵 파일 로드 실패: {result.ErrorMessage}");
|
||||
}
|
||||
|
||||
// NodeResolver 초기화
|
||||
_nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
|
||||
|
||||
// 시뮬레이터 캔버스에 맵 설정
|
||||
_simulatorCanvas.MapNodes = _mapNodes;
|
||||
|
||||
// UI 업데이트
|
||||
UpdateNodeComboBoxes();
|
||||
UpdateUI();
|
||||
|
||||
// 맵에 맞춤
|
||||
_simulatorCanvas.FitToMap();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"맵 파일 로드 실패: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// 맵 파일 데이터 구조체
|
||||
private class MapFileData
|
||||
{
|
||||
public List<MapNode> MapNodes { get; set; }
|
||||
public List<RfidMapping> RfidMappings { get; set; }
|
||||
}
|
||||
|
||||
private void UpdateNodeComboBoxes()
|
||||
{
|
||||
@@ -633,13 +403,16 @@ namespace AGVSimulator.Forms
|
||||
{
|
||||
foreach (var node in _mapNodes)
|
||||
{
|
||||
_startNodeCombo.Items.Add(node);
|
||||
_targetNodeCombo.Items.Add(node);
|
||||
if (node.IsActive && node.HasRfid())
|
||||
{
|
||||
_startNodeCombo.Items.Add(node);
|
||||
_targetNodeCombo.Items.Add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_startNodeCombo.DisplayMember = "NodeId";
|
||||
_targetNodeCombo.DisplayMember = "NodeId";
|
||||
_startNodeCombo.DisplayMember = "RfidId";
|
||||
_targetNodeCombo.DisplayMember = "RfidId";
|
||||
}
|
||||
|
||||
private void UpdateAGVComboBox()
|
||||
@@ -681,8 +454,128 @@ namespace AGVSimulator.Forms
|
||||
|
||||
_calculatePathButton.Enabled = _startNodeCombo.SelectedItem != null &&
|
||||
_targetNodeCombo.SelectedItem != null;
|
||||
|
||||
// RFID 위치 설정 관련
|
||||
var hasSelectedAGV = _agvListCombo.SelectedItem != null;
|
||||
var hasRfidNodes = _mapNodes != null && _mapNodes.Any(n => n.HasRfid());
|
||||
|
||||
_setPositionButton.Enabled = hasSelectedAGV && hasRfidNodes;
|
||||
_rfidTextBox.Enabled = hasSelectedAGV && hasRfidNodes;
|
||||
|
||||
// 맵 다시열기 버튼
|
||||
var hasCurrentMap = !string.IsNullOrEmpty(_currentMapFilePath);
|
||||
reloadMapToolStripMenuItem.Enabled = hasCurrentMap;
|
||||
reloadMapToolStripButton.Enabled = hasCurrentMap;
|
||||
}
|
||||
|
||||
private void OnReloadMap_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_currentMapFilePath))
|
||||
{
|
||||
MessageBox.Show("다시 로드할 맵 파일이 없습니다. 먼저 맵을 열어주세요.", "알림",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(_currentMapFilePath))
|
||||
{
|
||||
MessageBox.Show($"맵 파일을 찾을 수 없습니다:\n{_currentMapFilePath}", "오류",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LoadMapFile(_currentMapFilePath);
|
||||
_statusLabel.Text = $"맵 다시 로드 완료: {Path.GetFileName(_currentMapFilePath)}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"맵 파일을 다시 로드할 수 없습니다:\n{ex.Message}", "오류",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLaunchMapEditor_Click(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// MapEditor 실행 파일 경로 확인
|
||||
string mapEditorPath = _config.MapEditorExecutablePath;
|
||||
|
||||
// 경로가 설정되지 않았거나 파일이 없는 경우 사용자에게 선택을 요청
|
||||
if (string.IsNullOrEmpty(mapEditorPath) || !File.Exists(mapEditorPath))
|
||||
{
|
||||
using (var openDialog = new OpenFileDialog())
|
||||
{
|
||||
openDialog.Filter = "실행 파일 (*.exe)|*.exe|모든 파일 (*.*)|*.*";
|
||||
openDialog.Title = "AGV MapEditor 실행 파일 선택";
|
||||
openDialog.InitialDirectory = Application.StartupPath;
|
||||
|
||||
if (openDialog.ShowDialog() == DialogResult.OK)
|
||||
{
|
||||
mapEditorPath = openDialog.FileName;
|
||||
|
||||
// 설정에 저장
|
||||
_config.MapEditorExecutablePath = mapEditorPath;
|
||||
if (_config.AutoSave)
|
||||
{
|
||||
_config.Save();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return; // 사용자가 취소함
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MapEditor 실행
|
||||
var startInfo = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = mapEditorPath,
|
||||
UseShellExecute = true
|
||||
};
|
||||
|
||||
// 현재 로드된 맵 파일이 있으면 파라미터로 전달
|
||||
if (!string.IsNullOrEmpty(_currentMapFilePath) && File.Exists(_currentMapFilePath))
|
||||
{
|
||||
startInfo.Arguments = $"\"{_currentMapFilePath}\"";
|
||||
}
|
||||
|
||||
System.Diagnostics.Process.Start(startInfo);
|
||||
_statusLabel.Text = "MapEditor 실행됨";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"MapEditor를 실행할 수 없습니다:\n{ex.Message}", "오류",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void btAllReset_Click(object sender, EventArgs e)
|
||||
{
|
||||
// 시뮬레이션 정지
|
||||
if (_simulationState.IsRunning)
|
||||
{
|
||||
OnStopSimulation_Click(sender, e);
|
||||
}
|
||||
|
||||
// AGV 초기화
|
||||
_agvList.Clear();
|
||||
_simulatorCanvas.AGVList = new List<IAGV>();
|
||||
|
||||
// 경로 초기화
|
||||
_simulatorCanvas.CurrentPath = null;
|
||||
|
||||
// UI 업데이트
|
||||
UpdateAGVComboBox();
|
||||
UpdateNodeComboBoxes();
|
||||
UpdateUI();
|
||||
|
||||
_statusLabel.Text = "초기화 완료";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -58,4 +117,13 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="_menuStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<metadata name="_toolStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>132, 17</value>
|
||||
</metadata>
|
||||
<metadata name="_statusStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>237, 17</value>
|
||||
</metadata>
|
||||
</root>
|
||||
114
Cs_HMI/AGVSimulator/Models/SimulatorConfig.cs
Normal file
114
Cs_HMI/AGVSimulator/Models/SimulatorConfig.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVSimulator.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 시뮬레이터 환경 설정 클래스
|
||||
/// </summary>
|
||||
public class SimulatorConfig
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// MapEditor 실행 파일 경로
|
||||
/// </summary>
|
||||
public string MapEditorExecutablePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 마지막으로 로드한 맵 파일 경로
|
||||
/// </summary>
|
||||
public string LastMapFilePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 설정 파일 자동 저장 여부
|
||||
/// </summary>
|
||||
public bool AutoSave { get; set; } = true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Methods
|
||||
|
||||
/// <summary>
|
||||
/// 설정 파일 기본 경로
|
||||
/// </summary>
|
||||
private static string ConfigFilePath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AGVSimulator",
|
||||
"config.json");
|
||||
|
||||
/// <summary>
|
||||
/// 설정을 파일에서 로드
|
||||
/// </summary>
|
||||
/// <returns>로드된 설정 객체</returns>
|
||||
public static SimulatorConfig Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(ConfigFilePath))
|
||||
{
|
||||
var json = File.ReadAllText(ConfigFilePath);
|
||||
return JsonConvert.DeserializeObject<SimulatorConfig>(json) ?? new SimulatorConfig();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"설정 로드 실패: {ex.Message}");
|
||||
}
|
||||
|
||||
return new SimulatorConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 설정을 파일에 저장
|
||||
/// </summary>
|
||||
/// <param name="config">저장할 설정 객체</param>
|
||||
/// <returns>저장 성공 여부</returns>
|
||||
public static bool Save(SimulatorConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(ConfigFilePath);
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var json = JsonConvert.SerializeObject(config, Formatting.Indented);
|
||||
File.WriteAllText(ConfigFilePath, json);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"설정 저장 실패: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Instance Methods
|
||||
|
||||
/// <summary>
|
||||
/// 현재 설정을 저장
|
||||
/// </summary>
|
||||
/// <returns>저장 성공 여부</returns>
|
||||
public bool Save()
|
||||
{
|
||||
return Save(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MapEditor 실행 파일 경로 유효성 확인
|
||||
/// </summary>
|
||||
/// <returns>유효한 경로인지 여부</returns>
|
||||
public bool IsMapEditorPathValid()
|
||||
{
|
||||
return !string.IsNullOrEmpty(MapEditorExecutablePath) &&
|
||||
File.Exists(MapEditorExecutablePath);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -3,27 +3,18 @@ using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVMapEditor.Models;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding;
|
||||
using AGVNavigationCore.Controls;
|
||||
|
||||
namespace AGVSimulator.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 가상 AGV 상태
|
||||
/// </summary>
|
||||
public enum AGVState
|
||||
{
|
||||
Idle, // 대기
|
||||
Moving, // 이동 중
|
||||
Rotating, // 회전 중
|
||||
Docking, // 도킹 중
|
||||
Charging, // 충전 중
|
||||
Error // 오류
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 가상 AGV 클래스
|
||||
/// 실제 AGV의 동작을 시뮬레이션
|
||||
/// </summary>
|
||||
public class VirtualAGV
|
||||
public class VirtualAGV : IAGV
|
||||
{
|
||||
#region Events
|
||||
|
||||
@@ -181,7 +172,7 @@ namespace AGVSimulator.Models
|
||||
}
|
||||
|
||||
_currentPath = path;
|
||||
_remainingNodes = new List<string>(path.NodeSequence);
|
||||
_remainingNodes = new List<string>(path.Path);
|
||||
_currentNodeIndex = 0;
|
||||
|
||||
// 시작 노드 위치로 이동
|
||||
|
||||
29
Cs_HMI/AGVSimulator/build.bat
Normal file
29
Cs_HMI/AGVSimulator/build.bat
Normal file
@@ -0,0 +1,29 @@
|
||||
@echo off
|
||||
echo Building V2GDecoder VC++ Project...
|
||||
|
||||
REM Check if Visual Studio 2022 is installed (Professional or Community)
|
||||
set MSBUILD_PRO="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_COM="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_BT="F:\(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\MSBuild.exe"
|
||||
|
||||
if exist %MSBUILD_PRO% (
|
||||
echo "Found Visual Studio 2022 Professional"
|
||||
set MSBUILD=%MSBUILD_PRO%
|
||||
) else if exist %MSBUILD_COM% (
|
||||
echo "Found Visual Studio 2022 Community"
|
||||
set MSBUILD=%MSBUILD_COM%
|
||||
) else if exist %MSBUILD_BT% (
|
||||
echo "Found Visual Studio 2022 BuildTools"
|
||||
set MSBUILD=%MSBUILD_BT%
|
||||
) else (
|
||||
echo "Visual Studio 2022 (Professional or Community) not found!"
|
||||
echo "Please install Visual Studio 2022 or update the MSBuild path."
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build Debug x64 configuration
|
||||
echo Building Debug x64 configuration...
|
||||
%MSBUILD% AGVSimulator.csproj
|
||||
|
||||
pause
|
||||
@@ -1,6 +1,7 @@
|
||||
# CLAUDE.md
|
||||
|
||||
이 파일은 이 저장소의 코드로 작업할 때 Claude Code (claude.ai/code)를 위한 지침을 제공합니다.
|
||||
맵데이터는 C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap 파일을 기준으로 사용
|
||||
|
||||
## 빌드 및 개발 명령어
|
||||
|
||||
|
||||
648
Cs_HMI/Data/NewMap.agvmap
Normal file
648
Cs_HMI/Data/NewMap.agvmap
Normal file
@@ -0,0 +1,648 @@
|
||||
{
|
||||
"Nodes": [
|
||||
{
|
||||
"NodeId": "N001",
|
||||
"Name": "N001",
|
||||
"Position": "80, 160",
|
||||
"Type": 2,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N002"
|
||||
],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:34:44.9548285+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:18.1785633+09:00",
|
||||
"Description": "TOPS-2",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "001",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N001 - TOPS-2 - [001]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N002",
|
||||
"Name": "N002",
|
||||
"Position": "220, 220",
|
||||
"Type": 0,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N003"
|
||||
],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:34:48.2957516+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:20.6016371+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "002",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N002 - [002]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N003",
|
||||
"Name": "N003",
|
||||
"Position": "300, 280",
|
||||
"Type": 0,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N004"
|
||||
],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:34:49.2226656+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:23.2433989+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "003",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N003 - [003]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N004",
|
||||
"Name": "N004",
|
||||
"Position": "380, 340",
|
||||
"Type": 1,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N008",
|
||||
"N011",
|
||||
"N022"
|
||||
],
|
||||
"CanRotate": true,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:34:50.1681027+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:24.8122488+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "004",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N004 - [004]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N006",
|
||||
"Name": "N006",
|
||||
"Position": "520, 220",
|
||||
"Type": 0,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N007"
|
||||
],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:34:51.1111368+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:41.6764551+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "013",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N006 - [013]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N007",
|
||||
"Name": "N007",
|
||||
"Position": "600, 180",
|
||||
"Type": 0,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:34:51.9266982+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:43.5813583+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "014",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N007 - [014]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N008",
|
||||
"Name": "N008",
|
||||
"Position": "340, 420",
|
||||
"Type": 0,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N009"
|
||||
],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:34:53.9595825+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:33.4490347+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "009",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N008 - [009]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N009",
|
||||
"Name": "N009",
|
||||
"Position": "280, 480",
|
||||
"Type": 0,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N010"
|
||||
],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:34:54.5035702+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:35.2267268+09:00",
|
||||
"Description": "SSTRON",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "010",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N009 - SSTRON - [010]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N010",
|
||||
"Name": "N010",
|
||||
"Position": "180, 540",
|
||||
"Type": 2,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:34:55.0563237+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:37.2974468+09:00",
|
||||
"Description": "TOPS-1",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "011",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N010 - TOPS-1 - [011]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N011",
|
||||
"Name": "N011",
|
||||
"Position": "460, 420",
|
||||
"Type": 0,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N012"
|
||||
],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:34:55.8875335+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:26.5275006+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "005",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N011 - [005]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N012",
|
||||
"Name": "N012",
|
||||
"Position": "540, 480",
|
||||
"Type": 0,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N013"
|
||||
],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:34:56.3678144+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:27.9224943+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "006",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N012 - [006]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N013",
|
||||
"Name": "N013",
|
||||
"Position": "620, 520",
|
||||
"Type": 0,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N014"
|
||||
],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:34:56.8390845+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:29.5788308+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "007",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N013 - [007]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N014",
|
||||
"Name": "N014",
|
||||
"Position": "720, 580",
|
||||
"Type": 2,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:34:57.2549726+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:31.1919274+09:00",
|
||||
"Description": "SS-TRON",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "008",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N014 - SS-TRON - [008]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N019",
|
||||
"Name": "N019",
|
||||
"Position": "679, 199",
|
||||
"Type": 3,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N007"
|
||||
],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:35:56.5359098+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:45.6967709+09:00",
|
||||
"Description": "Charger",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Red",
|
||||
"RfidId": "015",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N019 - Charger - [015]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N022",
|
||||
"Name": "N022",
|
||||
"Position": "459, 279",
|
||||
"Type": 0,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N006"
|
||||
],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T08:36:48.0311551+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:39.7262145+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "012",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N022 - [012]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N023",
|
||||
"Name": "N023",
|
||||
"Position": "440, 220",
|
||||
"Type": 0,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N024",
|
||||
"N004"
|
||||
],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T09:41:36.8738794+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:47.8868788+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "016",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N023 - [016]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N024",
|
||||
"Name": "N024",
|
||||
"Position": "500, 160",
|
||||
"Type": 0,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N025"
|
||||
],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T09:41:37.4551853+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:51.7183934+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "017",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N024 - [017]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N025",
|
||||
"Name": "N025",
|
||||
"Position": "600, 120",
|
||||
"Type": 0,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [
|
||||
"N026"
|
||||
],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T09:41:38.0142374+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:54.3289018+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "018",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N025 - [018]"
|
||||
},
|
||||
{
|
||||
"NodeId": "N026",
|
||||
"Name": "N026",
|
||||
"Position": "660, 100",
|
||||
"Type": 3,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T09:41:38.5834487+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:46:57.0288799+09:00",
|
||||
"Description": "Charger",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Blue",
|
||||
"RfidId": "019",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "N026 - Charger - [019]"
|
||||
},
|
||||
{
|
||||
"NodeId": "LBL001",
|
||||
"Name": "Amkor Technology Korea",
|
||||
"Position": "160, 80",
|
||||
"Type": 4,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T11:08:22.4048927+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:08:22.4048927+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Purple",
|
||||
"RfidId": "",
|
||||
"LabelText": "Amkor Technology Korea",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "LBL001"
|
||||
},
|
||||
{
|
||||
"NodeId": "IMG001",
|
||||
"Name": "logo",
|
||||
"Position": "700, 320",
|
||||
"Type": 5,
|
||||
"DockDirection": null,
|
||||
"ConnectedNodes": [],
|
||||
"CanRotate": false,
|
||||
"StationId": "",
|
||||
"StationType": null,
|
||||
"CreatedDate": "2025-09-11T11:08:44.7897541+09:00",
|
||||
"ModifiedDate": "2025-09-11T11:08:44.7897541+09:00",
|
||||
"Description": "",
|
||||
"IsActive": true,
|
||||
"DisplayColor": "Brown",
|
||||
"RfidId": "",
|
||||
"LabelText": "",
|
||||
"FontFamily": "Arial",
|
||||
"FontSize": 12.0,
|
||||
"FontStyle": 0,
|
||||
"ForeColor": "Black",
|
||||
"BackColor": "Transparent",
|
||||
"ShowBackground": false,
|
||||
"ImagePath": "C:\\Data\\Users\\Pictures\\logo.png",
|
||||
"Scale": "1, 1",
|
||||
"Opacity": 1.0,
|
||||
"Rotation": 0.0,
|
||||
"DisplayText": "IMG001"
|
||||
}
|
||||
],
|
||||
"CreatedDate": "2025-09-11T11:46:57.8091998+09:00",
|
||||
"Version": "1.0"
|
||||
}
|
||||
Reference in New Issue
Block a user