- 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>
586 lines
19 KiB
C#
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
|
|
}
|
|
} |