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:
@@ -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
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user