Files
ENIG/Cs_HMI/AGVMapEditor/Forms/MainForm.cs
ChiKyun Kim 7567602479 feat: Add AGV Map Editor and Simulator tools
- Add AGVMapEditor: Visual map editing with drag-and-drop node placement
  * RFID mapping separation (physical ID ↔ logical node mapping)
  * A* pathfinding algorithm with AGV directional constraints
  * JSON map data persistence with structured format
  * Interactive map canvas with zoom/pan functionality

- Add AGVSimulator: Real-time AGV movement simulation
  * Virtual AGV with state machine (Idle, Moving, Rotating, Docking, Charging, Error)
  * Path execution and visualization from calculated routes
  * Real-time position tracking and battery simulation
  * Integration with map editor data format

- Update solution structure and build configuration
- Add comprehensive documentation in CLAUDE.md
- Implement AGV-specific constraints (forward/backward docking, rotation limits)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-10 17:39:23 +09:00

586 lines
19 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 AGVMapEditor.Controls;
using Newtonsoft.Json;
namespace AGVMapEditor.Forms
{
/// <summary>
/// AGV 맵 에디터 메인 폼
/// </summary>
public partial class MainForm : Form
{
#region Fields
private NodeResolver _nodeResolver;
private List<MapNode> _mapNodes;
private List<RfidMapping> _rfidMappings;
private MapCanvas _mapCanvas;
// 현재 선택된 노드
private MapNode _selectedNode;
// 파일 경로
private string _currentMapFile = string.Empty;
private bool _hasChanges = false;
#endregion
#region Constructor
public MainForm()
{
InitializeComponent();
InitializeData();
InitializeMapCanvas();
UpdateTitle();
}
#endregion
#region Initialization
private void InitializeData()
{
_mapNodes = new List<MapNode>();
_rfidMappings = new List<RfidMapping>();
_nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
}
private void InitializeMapCanvas()
{
_mapCanvas = new MapCanvas(_mapNodes);
_mapCanvas.Dock = DockStyle.Fill;
_mapCanvas.NodeSelected += OnNodeSelected;
_mapCanvas.NodeMoved += OnNodeMoved;
_mapCanvas.BackgroundClicked += OnBackgroundClicked;
// 스플리터 패널에 맵 캔버스 추가
splitContainer1.Panel2.Controls.Add(_mapCanvas);
}
#endregion
#region Event Handlers
private void MainForm_Load(object sender, EventArgs e)
{
RefreshNodeList();
RefreshRfidMappingList();
}
private void OnNodeSelected(object sender, MapNode node)
{
_selectedNode = node;
UpdateNodeProperties();
}
private void OnNodeMoved(object sender, MapNode node)
{
_hasChanges = true;
UpdateTitle();
RefreshNodeList();
}
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 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();
}
private void btnAddRfidMapping_Click(object sender, EventArgs e)
{
AddNewRfidMapping();
}
private void btnDeleteRfidMapping_Click(object sender, EventArgs e)
{
DeleteSelectedRfidMapping();
}
#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)
{
_nodeResolver.RemoveMapNode(_selectedNode.NodeId);
_selectedNode = null;
_hasChanges = true;
RefreshNodeList();
RefreshRfidMappingList();
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 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 OpenMap()
{
var openFileDialog = new OpenFileDialog
{
Filter = "AGV Map Files (*.agvmap)|*.agvmap|JSON Files (*.json)|*.json|All Files (*.*)|*.*",
DefaultExt = "agvmap"
};
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
try
{
LoadMapFromFile(openFileDialog.FileName);
_currentMapFile = openFileDialog.FileName;
_hasChanges = false;
RefreshAll();
UpdateTitle();
MessageBox.Show("맵이 성공적으로 로드되었습니다.", "성공", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
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|JSON Files (*.json)|*.json",
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 json = File.ReadAllText(filePath);
var mapData = JsonConvert.DeserializeObject<MapData>(json);
_mapNodes = mapData.Nodes ?? new List<MapNode>();
_rfidMappings = mapData.RfidMappings ?? new List<RfidMapping>();
_nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
}
private void SaveMapToFile(string filePath)
{
var mapData = new MapData
{
Nodes = _mapNodes,
RfidMappings = _rfidMappings,
CreatedDate = DateTime.Now,
Version = "1.0"
};
var json = JsonConvert.SerializeObject(mapData, Formatting.Indented);
File.WriteAllText(filePath, json);
}
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();
RefreshRfidMappingList();
RefreshMapCanvas();
ClearNodeProperties();
}
private void RefreshNodeList()
{
listBoxNodes.DataSource = null;
listBoxNodes.DataSource = _mapNodes;
listBoxNodes.DisplayMember = "Name";
listBoxNodes.ValueMember = "NodeId";
}
private void RefreshRfidMappingList()
{
listBoxRfidMappings.DataSource = null;
listBoxRfidMappings.DataSource = _rfidMappings;
listBoxRfidMappings.DisplayMember = "ToString";
}
private void RefreshMapCanvas()
{
_mapCanvas?.Invalidate();
}
private void UpdateNodeProperties()
{
if (_selectedNode == null)
{
ClearNodeProperties();
return;
}
// 선택된 노드의 속성을 프로퍼티 패널에 표시
// (실제로는 PropertyGrid나 별도 컨트롤 사용)
labelSelectedNode.Text = $"선택된 노드: {_selectedNode.Name} ({_selectedNode.NodeId})";
}
private void ClearNodeProperties()
{
labelSelectedNode.Text = "선택된 노드: 없음";
}
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 Data Model for Serialization
private class MapData
{
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";
}
#endregion
}
}