Files
ENIG/Cs_HMI/AGVMapEditor/Forms/MainForm.cs
ChiKyun Kim de0e39e030 refactor: Consolidate RFID mapping and add bidirectional pathfinding
Major improvements to AGV navigation system:

• Consolidated RFID management into MapNode, removing duplicate RfidMapping class
• Enhanced MapNode with RFID metadata fields (RfidStatus, RfidDescription)
• Added automatic bidirectional connection generation in pathfinding algorithms
• Updated all components to use unified MapNode-based RFID system
• Added command line argument support for AGVMapEditor auto-loading files
• Fixed pathfinding failures by ensuring proper node connectivity

Technical changes:
- Removed RfidMapping class and dependencies across all projects
- Updated AStarPathfinder with EnsureBidirectionalConnections() method
- Modified MapLoader to use AssignAutoRfidIds() for RFID automation
- Enhanced UnifiedAGVCanvas, SimulatorForm, and MainForm for MapNode integration
- Improved data consistency and reduced memory footprint

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 16:41:52 +09:00

786 lines
25 KiB
C#

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using AGVMapEditor.Models;
using AGVNavigationCore.Controls;
using AGVNavigationCore.Models;
using Newtonsoft.Json;
namespace AGVMapEditor.Forms
{
/// <summary>
/// AGV 맵 에디터 메인 폼
/// </summary>
public partial class MainForm : Form
{
#region Fields
private List<MapNode> _mapNodes;
private UnifiedAGVCanvas _mapCanvas;
// 현재 선택된 노드
private MapNode _selectedNode;
// 파일 경로
private string _currentMapFile = string.Empty;
private bool _hasChanges = false;
#endregion
#region Constructor
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
#region Initialization
private void InitializeData()
{
_mapNodes = new List<MapNode>();
}
private void InitializeMapCanvas()
{
_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.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
#region Event Handlers
private void MainForm_Load(object sender, EventArgs e)
{
RefreshNodeList();
// 속성 변경 시 이벤트 연결
_propertyGrid.PropertyValueChanged += PropertyGrid_PropertyValueChanged;
}
private void OnNodeAdded(object sender, MapNode node)
{
_hasChanges = true;
UpdateTitle();
RefreshNodeList();
// RFID 자동 할당
}
private void OnNodeSelected(object sender, MapNode node)
{
_selectedNode = node;
UpdateNodeProperties();
}
private void OnNodeMoved(object sender, MapNode node)
{
_hasChanges = true;
UpdateTitle();
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;
ClearNodeProperties();
}
#endregion
#region Menu Event Handlers
private void newToolStripMenuItem_Click(object sender, EventArgs e)
{
if (CheckSaveChanges())
{
NewMap();
}
}
private void openToolStripMenuItem_Click(object sender, EventArgs e)
{
if (CheckSaveChanges())
{
OpenMap();
}
}
private void saveToolStripMenuItem_Click(object sender, EventArgs e)
{
SaveMap();
}
private void saveAsToolStripMenuItem_Click(object sender, EventArgs e)
{
SaveAsMap();
}
private void closeToolStripMenuItem_Click(object sender, EventArgs e)
{
CloseMap();
}
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{
this.Close();
}
#endregion
#region Button Event Handlers
private void btnAddNode_Click(object sender, EventArgs e)
{
AddNewNode();
}
private void btnDeleteNode_Click(object sender, EventArgs e)
{
DeleteSelectedNode();
}
private void btnAddConnection_Click(object sender, EventArgs e)
{
AddConnectionToSelectedNode();
}
private void btnRemoveConnection_Click(object sender, EventArgs e)
{
RemoveConnectionFromSelectedNode();
}
#endregion
#region Node Management
private void AddNewNode()
{
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();
}
private void DeleteSelectedNode()
{
if (_selectedNode == null)
{
MessageBox.Show("삭제할 노드를 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var result = MessageBox.Show($"노드 '{_selectedNode.Name}'를 삭제하시겠습니까?\n연결된 RFID 매핑도 함께 삭제됩니다.",
"삭제 확인", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
// 노드 제거
_mapNodes.Remove(_selectedNode);
_selectedNode = null;
_hasChanges = true;
RefreshNodeList();
RefreshMapCanvas();
ClearNodeProperties();
UpdateTitle();
}
}
private void AddConnectionToSelectedNode()
{
if (_selectedNode == null)
{
MessageBox.Show("연결을 추가할 노드를 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 다른 노드들 중에서 선택
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);
return;
}
// 간단한 선택 다이얼로그 (실제로는 별도 폼을 만들어야 함)
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)
{
_selectedNode.AddConnection(targetNode.NodeId);
_hasChanges = true;
RefreshMapCanvas();
UpdateNodeProperties();
UpdateTitle();
}
}
private void RemoveConnectionFromSelectedNode()
{
if (_selectedNode == null || _selectedNode.ConnectedNodes.Count == 0)
{
MessageBox.Show("연결을 제거할 노드를 선택하거나 연결된 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 연결된 노드들 중에서 선택
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))
{
_selectedNode.RemoveConnection(targetNodeId);
_hasChanges = true;
RefreshMapCanvas();
UpdateNodeProperties();
UpdateTitle();
}
}
private string GenerateNodeId()
{
int counter = 1;
string nodeId;
do
{
nodeId = $"N{counter:D3}";
counter++;
} while (_mapNodes.Any(n => n.NodeId == nodeId));
return nodeId;
}
#endregion
#region File Operations
private void NewMap()
{
_mapNodes.Clear();
_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|All Files (*.*)|*.*",
DefaultExt = "agvmap"
};
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
try
{
LoadMapFromFile(openFileDialog.FileName);
_currentMapFile = openFileDialog.FileName;
_hasChanges = false;
RefreshAll();
UpdateTitle();
}
catch (Exception ex)
{
MessageBox.Show($"맵 로드 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
private void SaveMap()
{
if (string.IsNullOrEmpty(_currentMapFile))
{
SaveAsMap();
}
else
{
try
{
SaveMapToFile(_currentMapFile);
_hasChanges = false;
UpdateTitle();
MessageBox.Show("맵이 성공적으로 저장되었습니다.", "성공", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"맵 저장 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
private void SaveAsMap()
{
var saveFileDialog = new SaveFileDialog
{
Filter = "AGV Map Files (*.agvmap)|*.agvmap",
DefaultExt = "agvmap",
FileName = "NewMap.agvmap"
};
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
try
{
SaveMapToFile(saveFileDialog.FileName);
_currentMapFile = saveFileDialog.FileName;
_hasChanges = false;
UpdateTitle();
MessageBox.Show("맵이 성공적으로 저장되었습니다.", "성공", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"맵 저장 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
private void LoadMapFromFile(string filePath)
{
var result = MapLoader.LoadMapFromFile(filePath);
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)
{
if (!MapLoader.SaveMapToFile(filePath, _mapNodes))
{
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("변경사항이 있습니다. 저장하시겠습니까?", "변경사항 저장",
MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
SaveMap();
return !_hasChanges; // 저장이 성공했으면 true
}
else if (result == DialogResult.Cancel)
{
return false;
}
}
return true;
}
#endregion
#region UI Updates
private void RefreshAll()
{
RefreshNodeList();
RefreshMapCanvas();
ClearNodeProperties();
}
private void RefreshNodeList()
{
listBoxNodes.DataSource = null;
listBoxNodes.DataSource = _mapNodes;
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 ListBoxNodes_SelectedIndexChanged(object sender, EventArgs e)
{
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()
{
_mapCanvas?.Invalidate();
}
private void UpdateNodeProperties()
{
if (_selectedNode == null)
{
ClearNodeProperties();
return;
}
// 노드 래퍼 객체 생성 (타입에 따라 다른 래퍼 사용)
var nodeWrapper = NodePropertyWrapperFactory.CreateWrapper(_selectedNode, _mapNodes);
_propertyGrid.SelectedObject = nodeWrapper;
_propertyGrid.Focus();
}
private void ClearNodeProperties()
{
_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;
}
#endregion
#region Form Events
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (!CheckSaveChanges())
{
e.Cancel = true;
}
}
#endregion
#region PropertyGrid
private void PropertyGrid_PropertyValueChanged(object s, PropertyValueChangedEventArgs e)
{
// 속성이 변경되었을 때 자동으로 변경사항 표시
_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
}
}