1076 lines
32 KiB
C#
1076 lines
32 KiB
C#
using AGVNavigationCore.Models;
|
|
using AGVNavigationCore.PathFinding;
|
|
using AGVNavigationCore.PathFinding.Core;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Drawing;
|
|
using System.Drawing.Drawing2D;
|
|
using System.Linq;
|
|
using System.Reflection.Emit;
|
|
using System.Windows.Forms;
|
|
|
|
namespace AGVNavigationCore.Controls
|
|
{
|
|
/// <summary>
|
|
/// 통합 AGV 캔버스 컨트롤
|
|
/// 맵 편집, AGV 시뮬레이션, 실시간 모니터링을 모두 지원
|
|
/// </summary>
|
|
public partial class UnifiedAGVCanvas : UserControl
|
|
{
|
|
#region Constants
|
|
|
|
private const int NODE_SIZE = 24;
|
|
private const int NODE_RADIUS = NODE_SIZE / 2;
|
|
private const int GRID_SIZE = 20;
|
|
private const float CONNECTION_WIDTH = 1.0f;
|
|
private const int SNAP_DISTANCE = 10;
|
|
private const int AGV_SIZE = 40;
|
|
private const int CONNECTION_ARROW_SIZE = 8;
|
|
|
|
#endregion
|
|
|
|
#region Enums
|
|
|
|
/// <summary>
|
|
/// 캔버스 모드
|
|
/// </summary>
|
|
public enum CanvasMode
|
|
{
|
|
Edit, // 편집 가능 (맵 에디터)
|
|
Sync, // 동기화 모드 (장비 설정 동기화)
|
|
Emulator, // 에뮬레이터 모드
|
|
Run // 가동 모드 (User Request)
|
|
}
|
|
|
|
/// <summary>
|
|
/// 편집 모드 (CanvasMode.Edit일 때만 적용)
|
|
/// </summary>
|
|
public enum EditMode
|
|
{
|
|
Select, // 선택 모드
|
|
Move, // 이동 모드
|
|
AddNode, // 노드 추가 모드
|
|
Connect, // 연결 모드
|
|
Delete, // 삭제 모드
|
|
DeleteConnection, // 연결 삭제 모드
|
|
AddLabel, // 라벨 추가 모드
|
|
AddImage, // 이미지 추가 모드
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Fields
|
|
|
|
// 캔버스 모드
|
|
private CanvasMode _canvasMode = CanvasMode.Edit;
|
|
private EditMode _editMode = EditMode.Select;
|
|
|
|
// 맵 데이터
|
|
private List<MapNode> _nodes;
|
|
private List<MapLabel> _labels; // 추가
|
|
private List<MapImage> _images; // 추가
|
|
private List<MapMark> _marks;
|
|
private List<MapMagnet> _magnets;
|
|
|
|
// 선택된 객체들 (나중에 NodeBase로 통일 필요)
|
|
private NodeBase _selectedNode;
|
|
|
|
private List<NodeBase> _selectedNodes; // 다중 선택 (NodeBase로 변경 고려)
|
|
|
|
private NodeBase _hoveredNode;
|
|
|
|
private NodeBase _destinationNode;
|
|
|
|
// AGV 관련
|
|
private List<IAGV> _agvList;
|
|
private Dictionary<string, Point> _agvPositions;
|
|
private Dictionary<string, AgvDirection> _agvDirections;
|
|
private Dictionary<string, AGVState> _agvStates;
|
|
|
|
// 경로 관련
|
|
private AGVPathResult _currentPath;
|
|
private List<AGVPathResult> _allPaths;
|
|
|
|
// 도킹 검증 관련
|
|
private Dictionary<string, bool> _dockingErrors;
|
|
|
|
// UI 요소들
|
|
private Image _companyLogo;
|
|
private string _companyLogoPath = string.Empty;
|
|
private string _measurementInfo = string.Empty;
|
|
|
|
// 편집 관련 (EditMode에서만 사용)
|
|
private bool _isDragging;
|
|
private Point _dragOffset;
|
|
private Point _dragStartPosition; // 드래그 시작 위치 (고스트 표시용)
|
|
private Point _lastMousePosition;
|
|
private bool _isConnectionMode;
|
|
private MapNode _connectionStartNode;
|
|
private Point _connectionEndPoint;
|
|
private int _mouseMoveCounter = 0; // 디버그용: MouseMove 실행 횟수
|
|
private int _dragHandleIndex = -1; // 드래그 중인 핸들 인덱스
|
|
private const int HANDLE_SIZE = 8; // 편집 핸들 크기
|
|
|
|
// 영역 선택 관련
|
|
private bool _isAreaSelecting;
|
|
private Point _areaSelectStart;
|
|
private Point _areaSelectEnd;
|
|
|
|
// 그리드 및 줌 관련
|
|
private bool _showGrid = true;
|
|
private float _zoomFactor = 1.0f;
|
|
private PointF _panOffset = PointF.Empty; // float 정밀도로 변경 (팬 이동 정확도 개선)
|
|
private bool _isPanning;
|
|
|
|
// 자동 증가 카운터
|
|
private int _nodeCounter = 1;
|
|
|
|
// 강조 연결
|
|
private (string FromNodeId, string ToNodeId)? _highlightedConnection = null;
|
|
|
|
// RFID 중복 검사
|
|
private HashSet<string> _duplicateRfidNodes = new HashSet<string>();
|
|
|
|
// 동기화 모드 관련
|
|
private string _syncMessage = "동기화 중...";
|
|
private float _syncProgress = 0.0f;
|
|
private string _syncDetail = "";
|
|
|
|
string _alertmesage = "";
|
|
bool showalert = false;
|
|
|
|
// 깜박임 효과를 위한 타이머 및 상태
|
|
private Timer _alertBlinkTimer;
|
|
private bool _isAlertBlinkOn = true;
|
|
|
|
public void SetAlertMessage(string m)
|
|
{
|
|
_alertmesage = m;
|
|
showalert = !string.IsNullOrEmpty(m);
|
|
|
|
//if (showalert)
|
|
//{
|
|
// if (_alertBlinkTimer == null)
|
|
// {
|
|
// _alertBlinkTimer = new Timer();
|
|
// _alertBlinkTimer.Interval = 500; // 0.5초 간격
|
|
// _alertBlinkTimer.Tick += _alertBlinkTimer_Tick;
|
|
// }
|
|
// _alertBlinkTimer.Start();
|
|
// _isAlertBlinkOn = true;
|
|
//}
|
|
//else
|
|
//{
|
|
// if (_alertBlinkTimer != null)
|
|
// {
|
|
// _alertBlinkTimer.Stop();
|
|
// }
|
|
// _isAlertBlinkOn = false;
|
|
//}
|
|
//Invalidate(); // 즉시 갱신
|
|
}
|
|
|
|
private void _alertBlinkTimer_Tick(object sender, EventArgs e)
|
|
{
|
|
//_isAlertBlinkOn = !_isAlertBlinkOn;
|
|
//Invalidate();
|
|
}
|
|
|
|
|
|
// 브러쉬 및 펜
|
|
private Brush _normalNodeBrush;
|
|
private Brush _rotationNodeBrush;
|
|
private Brush _dockingNodeBrush;
|
|
private Brush _chargingNodeBrush;
|
|
private Brush _selectedNodeBrush;
|
|
private Brush _hoveredNodeBrush;
|
|
private Brush _destinationNodeBrush;
|
|
private Brush _gridBrush;
|
|
private Brush _agvBrush;
|
|
private Brush _pathBrush;
|
|
|
|
private Pen _connectionPen;
|
|
private Pen _gridPen;
|
|
private Pen _tempConnectionPen;
|
|
private Pen _selectedNodePen;
|
|
private Pen _destinationNodePen;
|
|
private Pen _pathPen;
|
|
private Pen _agvPen;
|
|
private Pen _highlightedConnectionPen;
|
|
private Pen _magnetPen;
|
|
private Pen _markPen;
|
|
private ToolTip _tooltip;
|
|
|
|
// 컨텍스트 메뉴
|
|
private ContextMenuStrip _contextMenu;
|
|
|
|
// 이벤트
|
|
public event EventHandler<NodeBase> NodeRightClicked;
|
|
|
|
#endregion
|
|
|
|
#region Events
|
|
|
|
// 맵 편집 이벤트
|
|
public delegate void NodeSelectHandler(object sender, NodeBase node, MouseEventArgs e);
|
|
public event NodeSelectHandler NodeSelect;
|
|
|
|
public event EventHandler<NodeBase> NodeAdded;
|
|
public event EventHandler<List<NodeBase>> NodesSelected; // 다중 선택 이벤트
|
|
public event EventHandler<NodeBase> NodeDeleted;
|
|
public event EventHandler<NodeBase> NodeMoved;
|
|
public event EventHandler<(MapNode From, MapNode To)> ConnectionDeleted;
|
|
public event EventHandler<MapImage> ImageDoubleClicked;
|
|
public event EventHandler<MapLabel> LabelDoubleClicked;
|
|
public event EventHandler MapChanged;
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
public string PredictMessage { get; set; } = "";
|
|
public string MapFileName { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// 캔버스 모드
|
|
/// </summary>
|
|
public CanvasMode Mode
|
|
{
|
|
get => _canvasMode;
|
|
set
|
|
{
|
|
_canvasMode = value;
|
|
UpdateModeUI();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 강조해서 표시할 특정 노드 ID (예: Gateway)
|
|
/// 이 값이 설정되면 해당 노드만 강조 표시됩니다.
|
|
/// </summary>
|
|
public string HighlightNodeId { get; set; }
|
|
|
|
public void RemoveItem(NodeBase item)
|
|
{
|
|
if (item is MapImage img) RemoveImage(img);
|
|
else if (item is MapLabel lb) RemoveLabel(lb);
|
|
else if (item is MapNode nd) RemoveNode(nd);
|
|
else if (item is MapMark mk) RemoveMark(mk);
|
|
else if (item is MapMagnet mg) RemoveMagnet(mg);
|
|
else throw new Exception("unknown type");
|
|
|
|
}
|
|
public void RemoveNode(MapNode node)
|
|
{
|
|
if (_nodes != null && _nodes.Contains(node))
|
|
{
|
|
_nodes.Remove(node);
|
|
Invalidate();
|
|
}
|
|
}
|
|
public void RemoveLabel(MapLabel label)
|
|
{
|
|
if (_labels != null && _labels.Contains(label))
|
|
{
|
|
_labels.Remove(label);
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
public void RemoveImage(MapImage image)
|
|
{
|
|
if (_images != null && _images.Contains(image))
|
|
{
|
|
_images.Remove(image);
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
public void RemoveMark(MapMark mark)
|
|
{
|
|
if (_marks != null && _marks.Contains(mark))
|
|
{
|
|
_marks.Remove(mark);
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
public void RemoveMagnet(MapMagnet magnet)
|
|
{
|
|
if (_magnets != null && _magnets.Contains(magnet))
|
|
{
|
|
_magnets.Remove(magnet);
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 편집 모드 (CanvasMode.Edit일 때만 적용)
|
|
/// </summary>
|
|
public EditMode CurrentEditMode
|
|
{
|
|
get => _editMode;
|
|
set
|
|
{
|
|
if (_canvasMode != CanvasMode.Edit) return;
|
|
|
|
_editMode = value;
|
|
if (_editMode != EditMode.Connect)
|
|
{
|
|
CancelConnection();
|
|
}
|
|
Cursor = GetCursorForMode(_editMode);
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 외부에서 Pan(X,Y) 및 Zoom 값을 설정합니다.
|
|
/// </summary>
|
|
/// <param name="panX">Pan X 좌표</param>
|
|
/// <param name="panY">Pan Y 좌표</param>
|
|
/// <param name="zoom">Zoom Level (0.1 ~ 5.0)</param>
|
|
public void SetView(float panX, float panY, float zoom)
|
|
{
|
|
// Zoom 값 범위 제한
|
|
float newZoom = Math.Max(0.1f, Math.Min(5.0f, zoom));
|
|
|
|
_panOffset = new PointF(panX, panY);
|
|
_zoomFactor = newZoom;
|
|
|
|
Invalidate();
|
|
}
|
|
|
|
[Browsable(false)]
|
|
public PointF PanOffset
|
|
{
|
|
get => _panOffset;
|
|
set
|
|
{
|
|
_panOffset = value;
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 그리드 표시 여부
|
|
/// </summary>
|
|
public bool ShowGrid
|
|
{
|
|
get => _showGrid;
|
|
set
|
|
{
|
|
_showGrid = value;
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 줌 팩터
|
|
/// </summary>
|
|
public float ZoomFactor
|
|
{
|
|
get => _zoomFactor;
|
|
set
|
|
{
|
|
_zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value));
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
[Browsable(false)]
|
|
public MapImage SelectedImage
|
|
{
|
|
get { return this._selectedNode as MapImage; }
|
|
}
|
|
|
|
[Browsable(false)]
|
|
public MapLabel SelectedLabel
|
|
{
|
|
get { return this._selectedNode as MapLabel; }
|
|
}
|
|
|
|
[Browsable(false)]
|
|
public MapMark SelectedMark
|
|
{
|
|
get { return this._selectedNode as MapMark; }
|
|
}
|
|
|
|
|
|
[Browsable(false)]
|
|
public MapMagnet SelectedMagnet
|
|
{
|
|
get { return this._selectedNode as MapMagnet; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 선택된 노드 (단일)
|
|
/// </summary>
|
|
public MapNode SelectedNode
|
|
{
|
|
get { return this._selectedNode as MapNode; }
|
|
set
|
|
{
|
|
_selectedNode = value;
|
|
if (value != null)
|
|
{
|
|
_selectedNodes.Clear();
|
|
_selectedNodes.Add(value);
|
|
}
|
|
else
|
|
{
|
|
_selectedNodes.Clear();
|
|
}
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 선택된 노드들 (다중)
|
|
/// </summary>
|
|
public List<NodeBase> SelectedNodes => _selectedNodes ?? new List<NodeBase>();
|
|
|
|
|
|
public List<NodeBase> Items
|
|
{
|
|
get
|
|
{
|
|
List<NodeBase> items = new List<NodeBase>();
|
|
if (Nodes != null && Nodes.Any()) items.AddRange(Nodes);
|
|
if (Labels != null && Labels.Any()) items.AddRange(Labels);
|
|
if (Images != null && Images.Any()) items.AddRange(Images);
|
|
if (Marks != null && Marks.Any()) items.AddRange(Marks);
|
|
if (Magnets != null && Magnets.Any()) items.AddRange(Magnets);
|
|
return items;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Map file loading 결과를 셋팅합니다
|
|
/// </summary>
|
|
/// <param name="result"></param>
|
|
public void SetMapLoadResult(MapLoader.MapLoadResult result)
|
|
{
|
|
this.Nodes = result.Nodes;
|
|
this.Labels = result.Labels; // 추가
|
|
this.Images = result.Images; // 추가
|
|
this.Marks = result.Marks;
|
|
this.Magnets = result.Magnets;
|
|
|
|
// 🔥 맵 설정 적용 (배경색, 그리드 표시)
|
|
if (result.Settings != null)
|
|
{
|
|
this.BackColor = Color.FromArgb(result.Settings.BackgroundColorArgb);
|
|
this.ShowGrid = result.Settings.ShowGrid;
|
|
}
|
|
|
|
this.FitToNodes();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 맵 데이터를 셋팅합니다
|
|
/// </summary>
|
|
public void SetMapData(List<MapNode> nodes, List<MapLabel> labels = null, List<MapImage> images = null, List<MapMark> marks = null, List<MapMagnet> magnets = null)
|
|
{
|
|
this.Nodes = nodes;
|
|
this.Labels = labels ?? new List<MapLabel>();
|
|
this.Images = images ?? new List<MapImage>();
|
|
this.Marks = marks ?? new List<MapMark>();
|
|
this.Magnets = magnets ?? new List<MapMagnet>();
|
|
this.FitToNodes();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 노드 목록
|
|
/// </summary>
|
|
public List<MapNode> Nodes
|
|
{
|
|
get => _nodes ?? new List<MapNode>();
|
|
set
|
|
{
|
|
_nodes = value ?? new List<MapNode>();
|
|
|
|
// 기존 노드들의 최대 번호를 찾아서 _nodeCounter 설정
|
|
UpdateNodeCounter();
|
|
|
|
// RFID 중복값 검사
|
|
DetectDuplicateRfidNodes();
|
|
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 라벨 목록
|
|
/// </summary>
|
|
public List<MapLabel> Labels
|
|
{
|
|
get => _labels ?? new List<MapLabel>();
|
|
set
|
|
{
|
|
_labels = value ?? new List<MapLabel>();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 이미지 목록
|
|
/// </summary>
|
|
public List<MapImage> Images
|
|
{
|
|
get => _images ?? new List<MapImage>();
|
|
set
|
|
{
|
|
_images = value ?? new List<MapImage>();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 마크 목록
|
|
/// </summary>
|
|
public List<MapMark> Marks
|
|
{
|
|
get => _marks ?? new List<MapMark>();
|
|
set
|
|
{
|
|
_marks = value ?? new List<MapMark>();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 마그넷 목록
|
|
/// </summary>
|
|
public List<MapMagnet> Magnets
|
|
{
|
|
get => _magnets ?? new List<MapMagnet>();
|
|
set
|
|
{
|
|
_magnets = value ?? new List<MapMagnet>();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 목록
|
|
/// </summary>
|
|
public List<IAGV> AGVList
|
|
{
|
|
get => _agvList ?? new List<IAGV>();
|
|
set
|
|
{
|
|
_agvList = value ?? new List<IAGV>();
|
|
UpdateAGVData();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 표시할 경로
|
|
/// </summary>
|
|
public AGVPathResult CurrentPath
|
|
{
|
|
get => _currentPath;
|
|
set
|
|
{
|
|
_currentPath = value;
|
|
UpdateDestinationNode();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 상세경로가 설정되어있는가?
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public bool HasPath()
|
|
{
|
|
if (_currentPath == null) return false;
|
|
if (_currentPath.DetailedPath == null) return false;
|
|
return _currentPath.DetailedPath.Any();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모든 경로 목록 (다중 AGV 경로 표시용)
|
|
/// </summary>
|
|
public List<AGVPathResult> AllPaths
|
|
{
|
|
get => _allPaths ?? new List<AGVPathResult>();
|
|
set
|
|
{
|
|
_allPaths = value ?? new List<AGVPathResult>();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 회사 로고 이미지
|
|
/// </summary>
|
|
public Image CompanyLogo
|
|
{
|
|
get => _companyLogo;
|
|
set
|
|
{
|
|
_companyLogo = value;
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 측정 정보 텍스트
|
|
/// </summary>
|
|
public string MeasurementInfo
|
|
{
|
|
get => _measurementInfo;
|
|
set
|
|
{
|
|
_measurementInfo = value;
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Connection Highlighting
|
|
|
|
/// <summary>
|
|
/// 특정 연결을 강조 표시
|
|
/// </summary>
|
|
/// <param name="fromNodeId">시작 노드 ID</param>
|
|
/// <param name="toNodeId">끝 노드 ID</param>
|
|
public void HighlightConnection(string fromNodeId, string toNodeId)
|
|
{
|
|
if (string.IsNullOrEmpty(fromNodeId) || string.IsNullOrEmpty(toNodeId))
|
|
{
|
|
_highlightedConnection = null;
|
|
}
|
|
else
|
|
{
|
|
// 사전순으로 정렬하여 저장 (연결이 단일 방향으로 저장되므로)
|
|
if (string.Compare(fromNodeId, toNodeId, StringComparison.Ordinal) <= 0)
|
|
{
|
|
_highlightedConnection = (fromNodeId, toNodeId);
|
|
}
|
|
else
|
|
{
|
|
_highlightedConnection = (toNodeId, fromNodeId);
|
|
}
|
|
}
|
|
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 연결 강조 표시 해제
|
|
/// </summary>
|
|
public void ClearHighlightedConnection()
|
|
{
|
|
_highlightedConnection = null;
|
|
Invalidate();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public UnifiedAGVCanvas()
|
|
{
|
|
InitializeComponent();
|
|
InitializeCanvas();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Initialization
|
|
|
|
private void InitializeCanvas()
|
|
{
|
|
SetStyle(ControlStyles.AllPaintingInWmPaint |
|
|
ControlStyles.UserPaint |
|
|
ControlStyles.DoubleBuffer |
|
|
ControlStyles.ResizeRedraw, true);
|
|
|
|
_nodes = new List<MapNode>();
|
|
_labels = new List<MapLabel>();
|
|
_images = new List<MapImage>();
|
|
_marks = new List<MapMark>();
|
|
_magnets = new List<MapMagnet>();
|
|
|
|
_selectedNodes = new List<NodeBase>(); // 다중 선택 리스트 초기화
|
|
_agvList = new List<IAGV>();
|
|
_agvPositions = new Dictionary<string, Point>();
|
|
_agvDirections = new Dictionary<string, AgvDirection>();
|
|
_agvStates = new Dictionary<string, AGVState>();
|
|
_allPaths = new List<AGVPathResult>();
|
|
_dockingErrors = new Dictionary<string, bool>();
|
|
|
|
InitializeBrushesAndPens();
|
|
CreateContextMenu();
|
|
|
|
_tooltip = new ToolTip();
|
|
_tooltip.AutoPopDelay = 5000;
|
|
_tooltip.InitialDelay = 1000;
|
|
_tooltip.ReshowDelay = 500;
|
|
_tooltip.ShowAlways = true;
|
|
}
|
|
|
|
private void InitializeBrushesAndPens()
|
|
{
|
|
// 노드 브러쉬
|
|
_normalNodeBrush = new SolidBrush(Color.LightBlue);
|
|
_rotationNodeBrush = new SolidBrush(Color.Yellow);
|
|
_dockingNodeBrush = new SolidBrush(Color.Orange);
|
|
_chargingNodeBrush = new SolidBrush(Color.Green);
|
|
_selectedNodeBrush = new SolidBrush(Color.Red);
|
|
_hoveredNodeBrush = new SolidBrush(Color.LightCyan);
|
|
_destinationNodeBrush = new SolidBrush(Color.Gold);
|
|
|
|
// AGV 및 경로 브러쉬
|
|
_agvBrush = new SolidBrush(Color.Red);
|
|
_pathBrush = new SolidBrush(Color.Purple);
|
|
|
|
// 그리드 브러쉬
|
|
_gridBrush = new SolidBrush(Color.LightGray);
|
|
|
|
// 펜
|
|
_connectionPen = new Pen(Color.White, CONNECTION_WIDTH);
|
|
_connectionPen.DashStyle = DashStyle.Dash;
|
|
_connectionPen.EndCap = LineCap.ArrowAnchor;
|
|
|
|
_gridPen = new Pen(Color.LightGray, 1);
|
|
_tempConnectionPen = new Pen(Color.Orange, 2) { DashStyle = DashStyle.Dash };
|
|
_selectedNodePen = new Pen(Color.Red, 3);
|
|
_destinationNodePen = new Pen(Color.Orange, 4);
|
|
_pathPen = new Pen(Color.Purple, 3);
|
|
_agvPen = new Pen(Color.Red, 3);
|
|
_highlightedConnectionPen = new Pen(Color.Red, 4) { DashStyle = DashStyle.Solid };
|
|
_magnetPen = new Pen(Color.FromArgb(100, Color.LightSkyBlue), 15) { DashStyle = DashStyle.Solid };
|
|
_markPen = new Pen(Color.White, 3); // 마크는 흰색 선으로 표시
|
|
}
|
|
|
|
private void CreateContextMenu()
|
|
{
|
|
_contextMenu = new ContextMenuStrip();
|
|
// 컨텍스트 메뉴는 EditMode에서만 사용
|
|
}
|
|
|
|
private void UpdateModeUI()
|
|
{
|
|
// 모드에 따른 UI 업데이트
|
|
_contextMenu.Enabled = true;
|
|
Cursor = GetCursorForMode(_editMode);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region AGV Management
|
|
|
|
/// <summary>
|
|
/// AGV 위치 업데이트
|
|
/// </summary>
|
|
public void UpdateAGVPosition(string agvId, Point position)
|
|
{
|
|
if (_agvPositions.ContainsKey(agvId))
|
|
_agvPositions[agvId] = position;
|
|
else
|
|
_agvPositions.Add(agvId, position);
|
|
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 방향 업데이트
|
|
/// </summary>
|
|
public void UpdateAGVDirection(string agvId, AgvDirection direction)
|
|
{
|
|
if (_agvDirections.ContainsKey(agvId))
|
|
_agvDirections[agvId] = direction;
|
|
else
|
|
_agvDirections.Add(agvId, direction);
|
|
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 상태 업데이트
|
|
/// </summary>
|
|
public void UpdateAGVState(string agvId, AGVState state)
|
|
{
|
|
if (_agvStates.ContainsKey(agvId))
|
|
_agvStates[agvId] = state;
|
|
else
|
|
_agvStates.Add(agvId, state);
|
|
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 위치 설정 (시뮬레이터용)
|
|
/// </summary>
|
|
/// <param name="agvId">AGV ID</param>
|
|
/// <param name="position">새로운 위치</param>
|
|
public void SetAGVPosition(string agvId, MapNode node, AgvDirection direction)
|
|
{
|
|
UpdateAGVPosition(agvId, node.Position);
|
|
UpdateAGVDirection(agvId, direction);
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 데이터 동기화
|
|
/// </summary>
|
|
private void UpdateAGVData()
|
|
{
|
|
if (_agvList == null) return;
|
|
|
|
foreach (var agv in _agvList)
|
|
{
|
|
UpdateAGVPosition(agv.AgvId, agv.CurrentPosition);
|
|
UpdateAGVDirection(agv.AgvId, agv.CurrentDirection);
|
|
UpdateAGVState(agv.AgvId, agv.CurrentState);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private Cursor GetCursorForMode(EditMode mode)
|
|
{
|
|
if (_canvasMode != CanvasMode.Edit)
|
|
return Cursors.Default;
|
|
|
|
switch (mode)
|
|
{
|
|
case EditMode.AddNode:
|
|
return Cursors.Cross;
|
|
case EditMode.Move:
|
|
return Cursors.SizeAll;
|
|
case EditMode.Connect:
|
|
return Cursors.Hand;
|
|
case EditMode.Delete:
|
|
return Cursors.No;
|
|
default:
|
|
return Cursors.Default;
|
|
}
|
|
}
|
|
|
|
private void CancelConnection()
|
|
{
|
|
_isConnectionMode = false;
|
|
_connectionStartNode = null;
|
|
_connectionEndPoint = Point.Empty;
|
|
Invalidate();
|
|
}
|
|
|
|
private void UpdateDestinationNode()
|
|
{
|
|
_destinationNode = null;
|
|
|
|
if (_currentPath != null && _currentPath.Success && _currentPath.Path != null && _currentPath.Path.Count > 0)
|
|
{
|
|
// 경로의 마지막 노드가 목적지
|
|
_destinationNode = _currentPath.Path[_currentPath.Path.Count - 1];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 동기화 상태 설정
|
|
/// </summary>
|
|
/// <param name="message">메인 메시지</param>
|
|
/// <param name="progress">진행률 (0.0 ~ 1.0)</param>
|
|
/// <param name="detail">상세 메시지</param>
|
|
public void SetSyncStatus(string message, float progress, string detail = "")
|
|
{
|
|
_syncMessage = message;
|
|
_syncProgress = Math.Max(0.0f, Math.Min(1.0f, progress));
|
|
_syncDetail = detail;
|
|
|
|
if (_canvasMode != CanvasMode.Sync)
|
|
{
|
|
_canvasMode = CanvasMode.Sync;
|
|
UpdateModeUI();
|
|
}
|
|
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 동기화 모드 종료
|
|
/// </summary>
|
|
public void ExitSyncMode(CanvasMode newmode)
|
|
{
|
|
if (_canvasMode == CanvasMode.Sync)
|
|
{
|
|
_canvasMode = newmode; // 기본 모드로 복귀 (또는 이전 모드)
|
|
UpdateModeUI();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cleanup
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
|
|
// 브러쉬 정리
|
|
_normalNodeBrush?.Dispose();
|
|
_rotationNodeBrush?.Dispose();
|
|
_dockingNodeBrush?.Dispose();
|
|
_chargingNodeBrush?.Dispose();
|
|
_selectedNodeBrush?.Dispose();
|
|
_hoveredNodeBrush?.Dispose();
|
|
_destinationNodeBrush?.Dispose();
|
|
_gridBrush?.Dispose();
|
|
_agvBrush?.Dispose();
|
|
_pathBrush?.Dispose();
|
|
|
|
// 펜 정리
|
|
_connectionPen?.Dispose();
|
|
_gridPen?.Dispose();
|
|
_tempConnectionPen?.Dispose();
|
|
_selectedNodePen?.Dispose();
|
|
_destinationNodePen?.Dispose();
|
|
_pathPen?.Dispose();
|
|
_agvPen?.Dispose();
|
|
_highlightedConnectionPen?.Dispose();
|
|
_magnetPen?.Dispose();
|
|
_markPen?.Dispose();
|
|
|
|
// 컨텍스트 메뉴 정리
|
|
_contextMenu?.Dispose();
|
|
|
|
// 이미지 정리
|
|
_companyLogo?.Dispose();
|
|
|
|
// 타이머 정리
|
|
if (_alertBlinkTimer != null)
|
|
{
|
|
_alertBlinkTimer.Stop();
|
|
_alertBlinkTimer.Dispose();
|
|
_alertBlinkTimer = null;
|
|
}
|
|
}
|
|
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// RFID 중복값을 가진 노드들을 감지하고 표시
|
|
/// 나중에 추가된 노드(인덱스가 더 큰)를 중복으로 간주
|
|
/// </summary>
|
|
private void DetectDuplicateRfidNodes()
|
|
{
|
|
_duplicateRfidNodes.Clear();
|
|
|
|
if (_nodes == null || _nodes.Count == 0)
|
|
return;
|
|
|
|
// RFID값과 해당 노드의 인덱스를 저장
|
|
var rfidToNodeIndex = new Dictionary<ushort, List<int>>();
|
|
|
|
// 모든 노드의 RFID값 수집
|
|
for (int i = 0; i < _nodes.Count; i++)
|
|
{
|
|
var node = _nodes[i];
|
|
if (node.HasRfid())
|
|
{
|
|
if (!rfidToNodeIndex.ContainsKey(node.RfidId))
|
|
{
|
|
rfidToNodeIndex[node.RfidId] = new List<int>();
|
|
}
|
|
rfidToNodeIndex[node.RfidId].Add(i);
|
|
}
|
|
}
|
|
|
|
// 중복된 RFID를 가진 노드들을 찾아서 나중에 추가된 것들을 표시
|
|
foreach (var kvp in rfidToNodeIndex)
|
|
{
|
|
if (kvp.Value.Count > 1)
|
|
{
|
|
// 첫 번째 노드는 원본으로 유지, 나머지는 중복으로 표시
|
|
for (int i = 1; i < kvp.Value.Count; i++)
|
|
{
|
|
int duplicateNodeIndex = kvp.Value[i];
|
|
_duplicateRfidNodes.Add(_nodes[duplicateNodeIndex].Id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 기존 노드들의 최대 번호를 찾아서 _nodeCounter를 업데이트
|
|
/// </summary>
|
|
private void UpdateNodeCounter()
|
|
{
|
|
if (_nodes == null || _nodes.Count == 0)
|
|
{
|
|
_nodeCounter = 1;
|
|
return;
|
|
}
|
|
|
|
int maxNumber = 0;
|
|
foreach (var node in _nodes)
|
|
{
|
|
// NodeId에서 숫자 부분 추출 (예: "N001" -> 1)
|
|
if (node.Id.StartsWith("N") && int.TryParse(node.Id.Substring(1), out int number))
|
|
{
|
|
maxNumber = Math.Max(maxNumber, number);
|
|
}
|
|
}
|
|
|
|
_nodeCounter = maxNumber + 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 특정 노드에 도킹 오류 표시를 설정/해제합니다.
|
|
/// </summary>
|
|
/// <param name="nodeId">노드 ID</param>
|
|
/// <param name="hasError">오류 여부</param>
|
|
public void SetDockingError(string nodeId, bool hasError)
|
|
{
|
|
if (string.IsNullOrEmpty(nodeId))
|
|
return;
|
|
|
|
if (hasError)
|
|
{
|
|
_dockingErrors[nodeId] = true;
|
|
}
|
|
else
|
|
{
|
|
_dockingErrors.Remove(nodeId);
|
|
}
|
|
|
|
Invalidate(); // 화면 다시 그리기
|
|
}
|
|
|
|
/// <summary>
|
|
/// 특정 노드에 도킹 오류가 있는지 확인합니다.
|
|
/// </summary>
|
|
/// <param name="nodeId">노드 ID</param>
|
|
/// <returns>도킹 오류 여부</returns>
|
|
public bool HasDockingError(string nodeId)
|
|
{
|
|
return _dockingErrors.ContainsKey(nodeId) && _dockingErrors[nodeId];
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모든 도킹 오류를 초기화합니다.
|
|
/// </summary>
|
|
public void ClearDockingErrors()
|
|
{
|
|
_dockingErrors.Clear();
|
|
Invalidate();
|
|
}
|
|
|
|
}
|
|
|
|
} |