- PathFinding 폴더를 Core, Validation, Planning, Analysis로 세분화 - 네임스페이스 정리 및 using 문 업데이트 - UnifiedAGVCanvas에 SetDockingError 메서드 추가 - 도킹 검증 시스템 인프라 구축 - DockingValidator 유틸리티 클래스 추가 - 빌드 오류 수정 및 안정성 개선 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1159 lines
39 KiB
C#
1159 lines
39 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;
|
|
private bool _hasCommandLineArgs = false;
|
|
|
|
// 노드 연결 정보를 표현하는 클래스
|
|
public class NodeConnectionInfo
|
|
{
|
|
public string FromNodeId { get; set; }
|
|
public string FromNodeName { get; set; }
|
|
public string FromRfidId { get; set; }
|
|
public string ToNodeId { get; set; }
|
|
public string ToNodeName { get; set; }
|
|
public string ToRfidId { get; set; }
|
|
public string ConnectionType { get; set; }
|
|
|
|
public override string ToString()
|
|
{
|
|
// RFID가 있으면 RFID(노드이름), 없으면 NodeID(노드이름) 형태로 표시
|
|
string fromDisplay = !string.IsNullOrEmpty(FromRfidId)
|
|
? $"{FromRfidId}({FromNodeName})"
|
|
: $"---({FromNodeId})";
|
|
|
|
string toDisplay = !string.IsNullOrEmpty(ToRfidId)
|
|
? $"{ToRfidId}({ToNodeName})"
|
|
: $"---({ToNodeId})";
|
|
|
|
// 양방향 연결은 ↔ 기호 사용
|
|
string arrow = ConnectionType == "양방향" ? "↔" : "→";
|
|
return $"{fromDisplay} {arrow} {toDisplay}";
|
|
}
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public MainForm() : this(null)
|
|
{
|
|
}
|
|
|
|
public MainForm(string[] args)
|
|
{
|
|
InitializeComponent();
|
|
InitializeData();
|
|
InitializeMapCanvas();
|
|
UpdateTitle();
|
|
|
|
// 명령줄 인수로 파일이 전달되었으면 자동으로 열기
|
|
if (args != null && args.Length > 0)
|
|
{
|
|
_hasCommandLineArgs = true;
|
|
string filePath = args[0];
|
|
if (System.IO.File.Exists(filePath))
|
|
{
|
|
LoadMapFromFile(filePath);
|
|
}
|
|
else
|
|
{
|
|
MessageBox.Show($"지정된 파일을 찾을 수 없습니다: {filePath}", "파일 오류",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
}
|
|
}
|
|
// 명령줄 인수가 없는 경우는 Form_Load에서 마지막 맵 파일 자동 로드 확인
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Initialization
|
|
|
|
private void InitializeData()
|
|
{
|
|
_mapNodes = new List<MapNode>();
|
|
}
|
|
|
|
private void InitializeMapCanvas()
|
|
{
|
|
_mapCanvas = new UnifiedAGVCanvas();
|
|
_mapCanvas.Dock = DockStyle.Fill;
|
|
_mapCanvas.Nodes = _mapNodes;
|
|
// RfidMappings 제거 - MapNode에 통합됨
|
|
|
|
// 이벤트 연결
|
|
_mapCanvas.NodeAdded += OnNodeAdded;
|
|
_mapCanvas.NodeSelected += OnNodeSelected;
|
|
_mapCanvas.NodeMoved += OnNodeMoved;
|
|
_mapCanvas.NodeDeleted += OnNodeDeleted;
|
|
_mapCanvas.ConnectionDeleted += OnConnectionDeleted;
|
|
_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 btnDeleteConnection = new Button();
|
|
btnDeleteConnection.Text = "연결삭제 (X)";
|
|
btnDeleteConnection.Size = new Size(80, 28);
|
|
btnDeleteConnection.Location = new Point(570, 3);
|
|
btnDeleteConnection.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.DeleteConnection;
|
|
|
|
// 구분선
|
|
var separator1 = new Label();
|
|
separator1.Text = "|";
|
|
separator1.Size = new Size(10, 28);
|
|
separator1.Location = new Point(655, 3);
|
|
separator1.TextAlign = ContentAlignment.MiddleCenter;
|
|
|
|
// 그리드 토글 버튼
|
|
var btnToggleGrid = new Button();
|
|
btnToggleGrid.Text = "그리드";
|
|
btnToggleGrid.Size = new Size(60, 28);
|
|
btnToggleGrid.Location = new Point(670, 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(735, 3);
|
|
btnFitMap.Click += (s, e) => _mapCanvas.FitToNodes();
|
|
|
|
// 툴바에 버튼들 추가
|
|
toolbarPanel.Controls.AddRange(new Control[]
|
|
{
|
|
btnSelect, btnMove, btnAddNode, btnAddLabel, btnAddImage, btnConnect, btnDelete, btnDeleteConnection, 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;
|
|
|
|
// 명령줄 인수가 없는 경우에만 마지막 맵 파일 자동 로드 확인
|
|
if (!_hasCommandLineArgs)
|
|
{
|
|
this.Show();
|
|
Application.DoEvents();
|
|
CheckAndLoadLastMapFile();
|
|
}
|
|
}
|
|
|
|
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 OnConnectionDeleted(object sender, (MapNode From, MapNode To) connection)
|
|
{
|
|
_hasChanges = true;
|
|
UpdateTitle();
|
|
RefreshNodeConnectionList();
|
|
UpdateNodeProperties(); // 연결 정보 업데이트
|
|
}
|
|
|
|
private void OnMapChanged(object sender, EventArgs e)
|
|
{
|
|
_hasChanges = true;
|
|
UpdateTitle();
|
|
}
|
|
|
|
private void OnBackgroundClicked(object sender, Point location)
|
|
{
|
|
_selectedNode = null;
|
|
ClearNodeProperties();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ToolStrip Button Event Handlers
|
|
|
|
private void btnNew_Click(object sender, EventArgs e)
|
|
{
|
|
if (CheckSaveChanges())
|
|
{
|
|
NewMap();
|
|
}
|
|
}
|
|
|
|
private void btnOpen_Click(object sender, EventArgs e)
|
|
{
|
|
if (CheckSaveChanges())
|
|
{
|
|
OpenMap();
|
|
}
|
|
}
|
|
|
|
private void btnReopen_Click(object sender, EventArgs e)
|
|
{
|
|
if (string.IsNullOrEmpty(_currentMapFile))
|
|
{
|
|
MessageBox.Show("다시 열 파일이 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
return;
|
|
}
|
|
|
|
if (!File.Exists(_currentMapFile))
|
|
{
|
|
MessageBox.Show($"파일을 찾을 수 없습니다: {_currentMapFile}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
return;
|
|
}
|
|
|
|
if (CheckSaveChanges())
|
|
{
|
|
LoadMapFromFile(_currentMapFile);
|
|
UpdateStatusBar($"파일을 다시 열었습니다: {Path.GetFileName(_currentMapFile)}");
|
|
}
|
|
}
|
|
|
|
private void btnClose_Click(object sender, EventArgs e)
|
|
{
|
|
CloseMap();
|
|
}
|
|
|
|
private void btnSave_Click(object sender, EventArgs e)
|
|
{
|
|
SaveMap();
|
|
}
|
|
|
|
private void btnSaveAs_Click(object sender, EventArgs e)
|
|
{
|
|
SaveAsMap();
|
|
}
|
|
|
|
private void btnExit_Click(object sender, EventArgs e)
|
|
{
|
|
this.Close();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Keyboard Shortcuts
|
|
|
|
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
|
{
|
|
switch (keyData)
|
|
{
|
|
case Keys.Control | Keys.N:
|
|
btnNew_Click(null, null);
|
|
return true;
|
|
case Keys.Control | Keys.O:
|
|
btnOpen_Click(null, null);
|
|
return true;
|
|
case Keys.Control | Keys.S:
|
|
btnSave_Click(null, null);
|
|
return true;
|
|
}
|
|
return base.ProcessCmdKey(ref msg, keyData);
|
|
}
|
|
|
|
#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에 통합
|
|
|
|
// 현재 파일 경로 업데이트
|
|
_currentMapFile = filePath;
|
|
_hasChanges = false;
|
|
|
|
// 설정에 마지막 맵 파일 경로 저장
|
|
EditorSettings.Instance.UpdateLastMapFile(filePath);
|
|
|
|
UpdateTitle();
|
|
UpdateNodeList();
|
|
RefreshNodeConnectionList();
|
|
|
|
// 맵 로드 후 자동으로 맵에 맞춤
|
|
_mapCanvas.FitToNodes();
|
|
|
|
UpdateStatusBar($"맵 파일을 성공적으로 로드했습니다: {Path.GetFileName(filePath)}");
|
|
}
|
|
else
|
|
{
|
|
MessageBox.Show($"맵 파일 로딩 실패: {result.ErrorMessage}", "오류",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
|
|
private void SaveMapToFile(string filePath)
|
|
{
|
|
if (MapLoader.SaveMapToFile(filePath, _mapNodes))
|
|
{
|
|
// 현재 파일 경로 업데이트
|
|
_currentMapFile = filePath;
|
|
_hasChanges = false;
|
|
|
|
// 설정에 마지막 맵 파일 경로 저장
|
|
EditorSettings.Instance.UpdateLastMapFile(filePath);
|
|
|
|
UpdateTitle();
|
|
UpdateStatusBar($"맵 파일을 성공적으로 저장했습니다: {Path.GetFileName(filePath)}");
|
|
}
|
|
else
|
|
{
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 마지막 맵 파일이 있는지 확인하고 사용자에게 로드할지 물어봄
|
|
/// </summary>
|
|
private void CheckAndLoadLastMapFile()
|
|
{
|
|
var settings = EditorSettings.Instance;
|
|
|
|
if (settings.AutoLoadLastMapFile && settings.HasValidLastMapFile())
|
|
{
|
|
string fileName = Path.GetFileName(settings.LastMapFilePath);
|
|
var result = MessageBox.Show(
|
|
$"마지막으로 사용한 맵 파일을 찾았습니다:\n\n{fileName}\n\n이 파일을 열까요?",
|
|
"마지막 맵 파일 로드",
|
|
MessageBoxButtons.YesNo,
|
|
MessageBoxIcon.Question);
|
|
|
|
if (result == DialogResult.Yes)
|
|
{
|
|
LoadMapFromFile(settings.LastMapFilePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region UI Updates
|
|
|
|
private void RefreshAll()
|
|
{
|
|
RefreshNodeList();
|
|
RefreshNodeConnectionList();
|
|
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.Name))
|
|
{
|
|
displayText += $" - {node.Name}";
|
|
}
|
|
|
|
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 RefreshNodeConnectionList()
|
|
{
|
|
var connections = new List<NodeConnectionInfo>();
|
|
var processedPairs = new HashSet<string>();
|
|
|
|
// 모든 노드의 연결 정보를 수집 (중복 방지)
|
|
foreach (var fromNode in _mapNodes)
|
|
{
|
|
foreach (var toNodeId in fromNode.ConnectedNodes)
|
|
{
|
|
var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId);
|
|
if (toNode != null)
|
|
{
|
|
// 중복 체크 (단일 연결만 표시)
|
|
string pairKey1 = $"{fromNode.NodeId}-{toNode.NodeId}";
|
|
string pairKey2 = $"{toNode.NodeId}-{fromNode.NodeId}";
|
|
|
|
if (!processedPairs.Contains(pairKey1) && !processedPairs.Contains(pairKey2))
|
|
{
|
|
// 사전 순으로 정렬하여 일관성 있게 표시
|
|
var (firstNode, secondNode) = string.Compare(fromNode.NodeId, toNode.NodeId) < 0
|
|
? (fromNode, toNode)
|
|
: (toNode, fromNode);
|
|
|
|
connections.Add(new NodeConnectionInfo
|
|
{
|
|
FromNodeId = firstNode.NodeId,
|
|
FromNodeName = firstNode.Name,
|
|
FromRfidId = firstNode.RfidId,
|
|
ToNodeId = secondNode.NodeId,
|
|
ToNodeName = secondNode.Name,
|
|
ToRfidId = secondNode.RfidId,
|
|
ConnectionType = "양방향" // 모든 연결이 양방향
|
|
});
|
|
|
|
processedPairs.Add(pairKey1);
|
|
processedPairs.Add(pairKey2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 리스트박스에 표시
|
|
lstNodeConnection.DataSource = null;
|
|
lstNodeConnection.DataSource = connections;
|
|
lstNodeConnection.DisplayMember = "ToString";
|
|
|
|
// 리스트박스 클릭 이벤트 연결
|
|
lstNodeConnection.SelectedIndexChanged -= LstNodeConnection_SelectedIndexChanged;
|
|
lstNodeConnection.SelectedIndexChanged += LstNodeConnection_SelectedIndexChanged;
|
|
|
|
// 더블클릭 이벤트 연결 (연결 삭제)
|
|
lstNodeConnection.DoubleClick -= LstNodeConnection_DoubleClick;
|
|
lstNodeConnection.DoubleClick += LstNodeConnection_DoubleClick;
|
|
}
|
|
|
|
|
|
private void LstNodeConnection_SelectedIndexChanged(object sender, EventArgs e)
|
|
{
|
|
if (lstNodeConnection.SelectedItem is NodeConnectionInfo connectionInfo)
|
|
{
|
|
// 캔버스에서 해당 연결선 강조 표시
|
|
_mapCanvas?.HighlightConnection(connectionInfo.FromNodeId, connectionInfo.ToNodeId);
|
|
|
|
// 연결된 노드들을 맵에서 하이라이트 표시 (선택적)
|
|
var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.FromNodeId);
|
|
if (fromNode != null)
|
|
{
|
|
_selectedNode = fromNode;
|
|
UpdateNodeProperties();
|
|
_mapCanvas?.Invalidate();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 선택 해제 시 강조 표시 제거
|
|
_mapCanvas?.ClearHighlightedConnection();
|
|
}
|
|
}
|
|
|
|
private void LstNodeConnection_DoubleClick(object sender, EventArgs e)
|
|
{
|
|
// 더블클릭으로 연결 삭제
|
|
DeleteSelectedConnection();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 노드 목록을 업데이트
|
|
/// </summary>
|
|
private void UpdateNodeList()
|
|
{
|
|
if (listBoxNodes != null)
|
|
{
|
|
listBoxNodes.DataSource = null;
|
|
listBoxNodes.DataSource = _mapNodes;
|
|
listBoxNodes.DisplayMember = "DisplayText";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 상태바에 메시지 표시
|
|
/// </summary>
|
|
/// <param name="message">표시할 메시지</param>
|
|
private void UpdateStatusBar(string message)
|
|
{
|
|
if (toolStripStatusLabel1 != null)
|
|
{
|
|
toolStripStatusLabel1.Text = message;
|
|
}
|
|
}
|
|
|
|
#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)
|
|
{
|
|
// RFID 값 변경시 중복 검사
|
|
if (e.ChangedItem.PropertyDescriptor.Name == "RFID")
|
|
{
|
|
string newRfidValue = e.ChangedItem.Value?.ToString();
|
|
if (!string.IsNullOrEmpty(newRfidValue) && CheckRfidDuplicate(newRfidValue))
|
|
{
|
|
// 중복된 RFID 값 발견
|
|
MessageBox.Show($"RFID 값 '{newRfidValue}'이(가) 이미 다른 노드에서 사용 중입니다.\n입력값을 되돌립니다.",
|
|
"RFID 중복 오류", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
|
|
// 원래 값으로 되돌리기 - PropertyGrid의 SelectedObject 사용
|
|
e.ChangedItem.PropertyDescriptor.SetValue(_propertyGrid.SelectedObject, e.OldValue);
|
|
_propertyGrid.Refresh();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 속성이 변경되었을 때 자동으로 변경사항 표시
|
|
_hasChanges = true;
|
|
UpdateTitle();
|
|
|
|
// 현재 선택된 노드를 기억
|
|
var currentSelectedNode = _selectedNode;
|
|
|
|
RefreshNodeList();
|
|
RefreshMapCanvas();
|
|
|
|
// 선택된 노드를 다시 선택
|
|
if (currentSelectedNode != null)
|
|
{
|
|
var nodeIndex = _mapNodes.IndexOf(currentSelectedNode);
|
|
if (nodeIndex >= 0)
|
|
{
|
|
listBoxNodes.SelectedIndex = nodeIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// RFID 값 중복 검사
|
|
/// </summary>
|
|
/// <param name="rfidValue">검사할 RFID 값</param>
|
|
/// <returns>중복되면 true, 아니면 false</returns>
|
|
private bool CheckRfidDuplicate(string rfidValue)
|
|
{
|
|
if (string.IsNullOrEmpty(rfidValue) || _mapNodes == null)
|
|
return false;
|
|
|
|
// 현재 편집 중인 노드 제외하고 중복 검사
|
|
string currentNodeId = null;
|
|
var selectedObject = _propertyGrid.SelectedObject;
|
|
|
|
// 다양한 PropertyWrapper 타입 처리
|
|
if (selectedObject is NodePropertyWrapper nodeWrapper)
|
|
{
|
|
currentNodeId = nodeWrapper.WrappedNode?.NodeId;
|
|
}
|
|
else if (selectedObject is LabelNodePropertyWrapper labelWrapper)
|
|
{
|
|
currentNodeId = labelWrapper.WrappedNode?.NodeId;
|
|
}
|
|
else if (selectedObject is ImageNodePropertyWrapper imageWrapper)
|
|
{
|
|
currentNodeId = imageWrapper.WrappedNode?.NodeId;
|
|
}
|
|
|
|
int duplicateCount = 0;
|
|
foreach (var node in _mapNodes)
|
|
{
|
|
// 현재 편집 중인 노드는 제외
|
|
if (node.NodeId == currentNodeId)
|
|
continue;
|
|
|
|
// 같은 RFID 값을 가진 노드가 있는지 확인
|
|
if (!string.IsNullOrEmpty(node.RfidId) && node.RfidId.Equals(rfidValue, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
duplicateCount++;
|
|
break; // 하나라도 발견되면 중복
|
|
}
|
|
}
|
|
|
|
return duplicateCount > 0;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Data Model for Serialization
|
|
|
|
|
|
#endregion
|
|
|
|
private void btNodeRemove_Click(object sender, EventArgs e)
|
|
{
|
|
DeleteSelectedConnection();
|
|
}
|
|
|
|
private void DeleteSelectedConnection()
|
|
{
|
|
if (lstNodeConnection.SelectedItem is NodeConnectionInfo connectionInfo)
|
|
{
|
|
var result = MessageBox.Show(
|
|
$"다음 연결을 삭제하시겠습니까?\n{connectionInfo}",
|
|
"연결 삭제 확인",
|
|
MessageBoxButtons.YesNo,
|
|
MessageBoxIcon.Question);
|
|
|
|
if (result == DialogResult.Yes)
|
|
{
|
|
// 단일 연결 삭제
|
|
var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.FromNodeId);
|
|
var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.ToNodeId);
|
|
|
|
if (fromNode != null && toNode != null)
|
|
{
|
|
// 단일 연결 삭제 (어느 방향에 저장되어 있는지 확인 후 삭제)
|
|
if (fromNode.ConnectedNodes.Contains(toNode.NodeId))
|
|
{
|
|
fromNode.RemoveConnection(toNode.NodeId);
|
|
}
|
|
else if (toNode.ConnectedNodes.Contains(fromNode.NodeId))
|
|
{
|
|
toNode.RemoveConnection(fromNode.NodeId);
|
|
}
|
|
|
|
_hasChanges = true;
|
|
|
|
RefreshNodeConnectionList();
|
|
RefreshMapCanvas();
|
|
UpdateNodeProperties();
|
|
UpdateTitle();
|
|
|
|
toolStripStatusLabel1.Text = $"연결 삭제됨: {connectionInfo.FromNodeId} ↔ {connectionInfo.ToNodeId}";
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MessageBox.Show("삭제할 연결을 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
}
|
|
}
|
|
}
|
|
} |