Files
ENIG/Cs_HMI/SubProject/AGVControl/MapControl.cs
2025-06-23 17:38:24 +09:00

2252 lines
91 KiB
C#

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using AGVControl.Models;
using AR;
namespace AGVControl
{
public class RFIDConnection
{
public uint StartRFID { get; set; }
public uint EndRFID { get; set; }
public bool IsBidirectional { get; set; }
public float Distance { get; set; }
public List<uint> IntermediateRFIDs { get; set; } = new List<uint>();
public override bool Equals(object obj)
{
if (obj is RFIDConnection other)
{
return (StartRFID == other.StartRFID && EndRFID == other.EndRFID) ||
(IsBidirectional && other.IsBidirectional &&
StartRFID == other.EndRFID && EndRFID == other.StartRFID);
}
return false;
}
public override int GetHashCode()
{
return StartRFID.GetHashCode() ^ EndRFID.GetHashCode();
}
}
public class PathResult
{
public bool Success { get; set; } = false;
public string Message { get; set; }
public List<RFIDPoint> Path { get; set; }
}
public enum AGVMoveState
{
Stop = 0,
Run
}
public enum AGVActionReasonCode
{
Unknown = 0,
NoPosition, // 위치 미확정(처음 기동)
NoPath, // 경로 없음 또는 현재 위치 미확정
NotOnPath, // 현재 위치가 경로에 없음
Arrived, // 경로의 마지막 지점(목적지 도달)
Normal, // 정상(다음 RFID 있음)
NeedTurn,
NoTurnPoint,
}
public class AGVActionPrediction
{
public Direction Direction { get; set; }
public uint? NextRFID { get; set; }
public string Reason { get; set; }
public AGVActionReasonCode ReasonCode { get; set; }
public AGVMoveState MoveState { get; set; } // RUN 또는 STOP
}
public partial class MapControl : Control
{
#region
private const int SNAP_DISTANCE = 10; // 점 근접 거리
private const int LINE_WIDTH = 20; // 선 굵기
private const int TOOLBAR_WIDTH = 58; // 툴바 너비
private const int TOOLBAR_WIDTHR = 78; // 우측 툴바 너비
private const int TOOLBAR_BUTTON_HEIGHT = 40; // 툴바 버튼 높이
private const int TOOLBAR_MARGIN = 5; // 툴바 마진
private const int SELECTION_DISTANCE = 15; // 선택 가능 거리
#endregion
#region
// 맵 데이터
private List<RFIDPoint> RFIDPoints;
private List<MapText> mapTexts;
private List<CustomLine> customLines;
private List<RFIDLine> rfidLines;
private HashSet<RFIDConnection> rfidConnections;
public AGV agv;
// 화면 조작 관련
private float zoom = 1.0f;
private PointF offset = PointF.Empty;
private Point lastMousePosition;
private Point currentMousePosition;
private bool isDragging = false;
private Point? previewStartPoint = null;
private Point? lineStartPoint = null;
private Point? branchPoint = null;
// 모드 관련
private bool isAddingText = false;
private bool isAddingPoint = false;
private bool isDrawingCustomLine = false;
private bool isDrawingRFIDLine = false;
private bool isDeletingRFIDLine = false;
private bool isDrawingLine = false;
private bool isDraggingPoint = false;
private bool isDraggingText = false;
private Point dragOffset;
// 선택된 객체
private MapText selectedText = null;
private CustomLine selectedLine = null;
private RFIDPoint selectedRFID = null;
private RFIDLine selectedRFIDLine = null;
private Point? draggingPoint = null;
// 툴바 관련
private List<ToolBarItem> toolbarRects;
// RFID 관련
public string RFIDStartNo { get; set; } = string.Empty;
public int RFIDLastNumber = 0;
private string filename = string.Empty;
#endregion
#region
public MapText SelectedText => selectedText;
public CustomLine SelectedLine => selectedLine;
public RFIDPoint SelectedRFID => selectedRFID;
public RFIDLine SelectedRFIDLine => selectedRFIDLine;
#endregion
#region
public MapControl()
{
this.DoubleBuffered = true;
RFIDPoints = new List<RFIDPoint>();
mapTexts = new List<MapText>();
customLines = new List<CustomLine>();
rfidLines = new List<RFIDLine>();
rfidConnections = new HashSet<RFIDConnection>();
agv = new AGV();
UpdateToolbarRects();
}
#endregion
#region
public event EventHandler<MouseEventArgs> OnRightClick;
#endregion
#region OVERRIDE
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
if (e.Button == MouseButtons.Middle)
{
isDragging = false;
this.Cursor = Cursors.Default;
}
else if (e.Button == MouseButtons.Left)
{
isDragging = false;
isDraggingPoint = false;
isDraggingText = false;
}
}
protected override void OnMouseClick(MouseEventArgs e)
{
base.OnMouseClick(e);
var mapPoint = ScreenToMap(e.Location);
if (e.Button == MouseButtons.Right)
{
OnRightClick?.Invoke(this, e);
this.MouseMode = eMouseMode.Default;
}
else if (e.Button == MouseButtons.Left)
{
// 툴바 버튼 클릭 처리
var toolbar = toolbarRects.FirstOrDefault(t => t.Bounds.Contains(e.Location));
if (toolbar != null)
{
switch (toolbar.Title.ToLower())
{
case "+": ZoomIn(); break;
case "-": ZoomOut(); break;
case "1:1": ResetZoom(); break;
case "cut": MouseMode = (eMouseMode.rfidcut); break;
case "text": MouseMode = (eMouseMode.addtext); break;
case "line": MouseMode = (eMouseMode.addrfidline); break;
case "cline": MouseMode = (eMouseMode.addcustomline); break;
case "point": MouseMode = (eMouseMode.addrfidpoint); break;
case "path":
var input1 = AR.UTIL.InputBox("input start");
if (input1.Item1 == false) return;
var input2 = AR.UTIL.InputBox("input end");
if (input2.Item1 == false) return;
var startRFID = input1.Item2;
var endRFID = input2.Item2;
var valid1 = uint.TryParse(input1.Item2, out uint vstart);
var valid2 = uint.TryParse(input2.Item2, out uint vend);
if (valid1 == false || valid2 == false)
{
MessageBox.Show("RFID값은 정수로 입력하세요", "경로 계산", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var rlt = CalculatePath(vstart, vend);
if (rlt.Success == false)
{
MessageBox.Show(rlt.Message, "경로 계산", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
else
{
SetCurrentPath(rlt.Path); //현재 경로로 설정함
MessageBox.Show($"경로가 계산되었습니다.\nRFID 순서: {string.Join(" -> ", rlt.Path)}",
"경로 계산", MessageBoxButtons.OK, MessageBoxIcon.Information);
if (SetTargetPosition(vend) == false)
{
MessageBox.Show("목적지 설정 실패");
}
}
break;
case "pos":
var tag = AR.UTIL.InputBox("input rfid tag value");
if (tag.Item1 && tag.Item2 != "" && uint.TryParse(tag.Item2, out uint val) == true)
{
var targetRFID = SetCurrentPosition((ushort)val);
}
break;
case "save":
using (var od = new SaveFileDialog())
{
od.Filter = "path data|*.route";
od.FilterIndex = 0;
od.RestoreDirectory = true;
if (filename.isEmpty() == false)
{
od.FileName = System.IO.Path.GetFileName(filename);
od.InitialDirectory = System.IO.Path.GetDirectoryName(filename);
}
if (od.ShowDialog() == DialogResult.OK)
{
filename = od.FileName;
this.SaveToFile(filename);
this.Invalidate();
}
}
break;
case "load":
using (var od = new OpenFileDialog())
{
od.Filter = "path data|*.route";
od.FilterIndex = 0;
if (string.IsNullOrEmpty(this.filename) == false)
{
od.FileName = System.IO.Path.GetFileName(this.filename);
od.InitialDirectory = System.IO.Path.GetDirectoryName(this.filename);
}
else
{
od.RestoreDirectory = true;
}
if (od.ShowDialog() == DialogResult.OK)
{
this.LoadFromFile(od.FileName, out string errmsg);
if (errmsg.isEmpty() == false) UTIL.MsgE(errmsg);
this.Invalidate();
}
}
break;
}
return;
}
// RFID 포인트 선택
var clickedRFID = RFIDPoints.FirstOrDefault(r => GetDistance(mapPoint, r.Location) <= SNAP_DISTANCE);
switch (mousemode)
{
case eMouseMode.rfidcut:
DeleteNearbyRFIDLine(mapPoint);
break;
case eMouseMode.addrfidpoint:
if (string.IsNullOrEmpty(this.RFIDStartNo) == false)
{
if (uint.TryParse(this.RFIDStartNo, out uint rfidvalue))
{
AddRFIDPoint(mapPoint, rfidvalue);
// 숫자로 끝나는 RFID 값인 경우 자동 증가
if (Regex.IsMatch(RFIDStartNo, @"^[A-Za-z]+\d+$"))
{
// 마지막 숫자 부분 추출
Match match = Regex.Match(RFIDStartNo, @"\d+$");
if (match.Success)
{
int currentNumber = int.Parse(match.Value);
if (currentNumber > this.RFIDLastNumber)
{
RFIDLastNumber = currentNumber;
}
RFIDLastNumber++;
// 숫자 부분을 새로운 번호로 교체
RFIDStartNo = RFIDStartNo.Substring(0, match.Index) + RFIDLastNumber.ToString("D4");
}
}
}
}
break;
case eMouseMode.addtext:
var text = new MapText
{
Location = mapPoint,
Text = "새 텍스트",
TextColor = Color.Black,
BackgroundColor = Color.Transparent,
Font = new Font("Arial", 12)
};
mapTexts.Add(text);
selectedText = text;
this.Invalidate();
break;
case eMouseMode.addcustomline:
if (previewStartPoint == null)
{
previewStartPoint = mapPoint;
}
else
{
var line = new CustomLine
{
StartPoint = previewStartPoint.Value,
EndPoint = mapPoint,
LineColor = Color.Red,
LineWidth = 2
};
customLines.Add(line);
selectedLine = line;
previewStartPoint = null;
this.Invalidate();
}
break;
case eMouseMode.addrfidline:
if (clickedRFID != null)
{
if (previewStartPoint == null)
{
previewStartPoint = clickedRFID.Location;
}
else
{
var startRFID = RFIDPoints.FirstOrDefault(r => r.Location == previewStartPoint);
if (startRFID != null)
{
var line = new RFIDLine
{
StartPoint = previewStartPoint.Value,
EndPoint = clickedRFID.Location,
};
rfidLines.Add(line);
selectedRFIDLine = line;
// RFID 연결 정보 처리
ProcessRFIDConnections();
}
// 다음 라인을 위해 현재 클릭한 RFID를 시작점으로 설정
previewStartPoint = clickedRFID.Location;
}
this.Invalidate();
}
break;
}
}
}
protected override void OnMouseDoubleClick(MouseEventArgs e)
{
base.OnMouseDoubleClick(e);
if (e.Button == MouseButtons.Left)
{
var mapPoint = ScreenToMap(e.Location);
//RFID 포인트 찾기
var selected_rfid = RFIDPoints.Where(t => t.Bounds.Expand(SELECTION_DISTANCE, SELECTION_DISTANCE).Contains(mapPoint)).FirstOrDefault();
if (selected_rfid != null)
{
UTIL.ShowPropertyDialog(selected_rfid);
this.Invalidate();
return;
}
// 텍스트 객체 찾기
var selected_txt = mapTexts.Where(t => t.Bounds.Expand(SELECTION_DISTANCE, SELECTION_DISTANCE).Contains(mapPoint)).FirstOrDefault();
if (selected_txt != null)
{
UTIL.ShowPropertyDialog(selected_txt);
this.Invalidate();
return;
}
}
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
UpdateToolbarRects();
this.Invalidate();
}
protected override void OnMouseWheel(MouseEventArgs e)
{
base.OnMouseWheel(e);
if (e.Delta > 0)
{
zoom *= 1.1f;
}
else
{
zoom /= 1.2f;
}
zoom = Math.Max(0.1f, Math.Min(10.0f, zoom));
this.Invalidate();
}
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
lastMousePosition = e.Location;
var mapPoint = ScreenToMap(e.Location);
if (e.Button == MouseButtons.Middle)
{
isDragging = true;
this.Cursor = Cursors.SizeAll;
}
else if (e.Button == MouseButtons.Left && !isAddingText && !isDrawingCustomLine && !isDrawingRFIDLine)
{
isDragging = true;
// 텍스트 선택 및 드래그 시작
foreach (var text in mapTexts)
{
var textSize = CreateGraphics().MeasureString(text.Text, text.Font);
var rect = new RectangleF(
text.Location.X - SELECTION_DISTANCE,
text.Location.Y - SELECTION_DISTANCE,
textSize.Width + SELECTION_DISTANCE * 2,
textSize.Height + SELECTION_DISTANCE * 2
);
if (rect.Contains(mapPoint))
{
selectedText = text;
isDraggingText = true;
// 드래그 시작점과 텍스트 위치의 차이를 저장
dragOffset = new Point(
text.Location.X - mapPoint.X,
text.Location.Y - mapPoint.Y
);
return;
}
}
// 커스텀 라인 선택 및 드래그 포인트 설정
foreach (var line in customLines)
{
float startDistance = GetDistance(mapPoint, line.StartPoint);
float endDistance = GetDistance(mapPoint, line.EndPoint);
if (startDistance < SELECTION_DISTANCE)
{
selectedLine = line;
draggingPoint = line.StartPoint;
isDraggingPoint = true;
return;
}
else if (endDistance < SELECTION_DISTANCE)
{
selectedLine = line;
draggingPoint = line.EndPoint;
isDraggingPoint = true;
return;
}
}
// RFID 포인트 선택
var clickedRFID = RFIDPoints.FirstOrDefault(r =>
GetDistance(mapPoint, r.Location) <= SNAP_DISTANCE);
if (clickedRFID != null)
{
selectedRFID = clickedRFID;
draggingPoint = clickedRFID.Location;
isDraggingPoint = true;
return;
}
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
currentMousePosition = e.Location;
var mapPoint = ScreenToMap(e.Location);
if ((mousemode == eMouseMode.addcustomline || mousemode == eMouseMode.addrfidline) && branchPoint.HasValue)
{
UpdatePreviewLine(e.Location);
}
// 툴바 버튼 호버 상태 업데이트
var oldHovering = toolbarRects.OrderBy(t => t.Idx).Select(t => t.isHovering).ToArray();
//toolbar check
toolbarRects.ForEach(t => t.isHovering = t.Bounds.Contains(e.Location));
//hovering check
if (toolbarRects.Where(t => t.isHovering).Any())
{
this.Cursor = Cursors.Hand;
}
else if (isDeletingRFIDLine)
{
this.Cursor = GetScissorsCursor();
}
else
{
this.Cursor = Cursors.Default;
}
//hovering display update
if (toolbarRects.Where(t => t.Dirty).Any())
{
this.Invalidate();
}
if (isDragging)
{
if (e.Button == MouseButtons.Middle)
{
offset = new PointF(
offset.X + (e.Location.X - lastMousePosition.X),
offset.Y + (e.Location.Y - lastMousePosition.Y)
);
}
else if (isDraggingText && selectedText != null)
{
// 텍스트 이동 - 드래그 오프셋 적용
selectedText.Location = new Point(
mapPoint.X + dragOffset.X,
mapPoint.Y + dragOffset.Y
);
}
else if (isDraggingPoint && draggingPoint.HasValue)
{
// 포인트 이동
var delta = new Point(
mapPoint.X - ScreenToMap(lastMousePosition).X,
mapPoint.Y - ScreenToMap(lastMousePosition).Y
);
if (selectedLine != null)
{
// 커스텀 라인 포인트 이동
if (draggingPoint.Value == selectedLine.StartPoint)
{
selectedLine.StartPoint = new Point(
selectedLine.StartPoint.X + delta.X,
selectedLine.StartPoint.Y + delta.Y
);
}
else if (draggingPoint.Value == selectedLine.EndPoint)
{
selectedLine.EndPoint = new Point(
selectedLine.EndPoint.X + delta.X,
selectedLine.EndPoint.Y + delta.Y
);
}
}
else if (selectedRFID != null) // RFID 포인트 이동
{
// RFID 포인트 위치 업데이트
selectedRFID.Location = new Point(
selectedRFID.Location.X + delta.X,
selectedRFID.Location.Y + delta.Y
);
// 연결된 RFID 라인 업데이트
foreach (var line in rfidLines)
{
if (line.StartPoint == draggingPoint.Value)
{
line.StartPoint = selectedRFID.Location;
}
if (line.EndPoint == draggingPoint.Value)
{
line.EndPoint = selectedRFID.Location;
}
}
}
}
lastMousePosition = e.Location;
this.Invalidate();
}
// 미리보기 라인 업데이트를 위한 마우스 위치 저장
if (isDrawingRFIDLine || isDrawingCustomLine || previewStartPoint.HasValue)
{
currentMousePosition = mapPoint;
}
this.Invalidate();
}
#endregion
#region
private void UpdateToolbarRects()
{
int x, y, c, row;
var idx = 0;
if (this.toolbarRects == null) this.toolbarRects = new List<ToolBarItem>();
else this.toolbarRects.Clear();
//left toolbar
x = TOOLBAR_MARGIN;
y = 10;
toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "+", Bounds = new Rectangle(x, y, TOOLBAR_WIDTH - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) });
y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN;
toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "-", Bounds = new Rectangle(x, y, TOOLBAR_WIDTH - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) });
y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN;
toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "1:1", Bounds = new Rectangle(x, y, TOOLBAR_WIDTH - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) });
y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN;
toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Cut", Bounds = new Rectangle(x, y, TOOLBAR_WIDTH - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) });
//right toolbar
y = 10;
row = 0;
x = DisplayRectangle.Right - TOOLBAR_WIDTHR - TOOLBAR_MARGIN;
toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Text", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) });
y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN;
toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Line", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) });
y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN;
toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Point", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) });
y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN;
toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Magnet", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) });
y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN;
toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Load", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) });
y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN;
toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Save", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) });
y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN;
toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Pos", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) });
y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN;
toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Path", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) });
}
public void SetPreviewStartPoint(Point? point)
{
previewStartPoint = point;
this.Invalidate();
}
public void UpdatePreviewLine(Point currentPosition)
{
currentMousePosition = currentPosition;
this.Invalidate();
}
private Point SnapToPoint(Point point)
{
// RFID 포인트와 근접한지 확인
foreach (var rfid in RFIDPoints)
{
if (GetDistance(point, rfid.Location) <= SNAP_DISTANCE)
{
return rfid.Location;
}
}
return point;
}
public RFIDPoint FindRFIDPoint(uint rfidValue)
{
if (RFIDPoints == null || RFIDPoints.Any() == false) return null;
return RFIDPoints.FirstOrDefault(r => r.Value == rfidValue);
}
/// <summary>
/// 현재위치를 설정합니다
/// </summary>
/// <param name="rfidTagNo">RFID TagNo</param>
/// <returns></returns>
public bool SetCurrentPosition(UInt16 rfidTagNo)
{
var rfidPoint = FindRFIDPoint(rfidTagNo);
if (rfidPoint != null)
{
// 이동 경로에 추가 (위치 업데이트보다 먼저)
agv.AddToMovementHistory(rfidTagNo, rfidPoint.Location, this.agv.CurrentMOTDirection);
// AGV 위치 업데이트
agv.CurrentRFID = rfidPoint;
// 목적지가 설정되어 있고 경로가 있는 경우 검증
if (agv.TargetRFID.IsEmpty == false && agv.CurrentPath.Count > 0)
{
// 현재 위치가 경로에 있는지 확인
bool isOnPath = agv.CurrentPath.Contains(agv.CurrentRFID);
if (!isOnPath)
{
// 경로를 벗어났으므로 새로운 경로 계산
var pathResult = CalculatePath(agv.CurrentRFID, agv.TargetRFID);
if (pathResult.Success)
{
SetCurrentPath(pathResult.Path);
}
}
}
// 목적지 RFID에 도착했고, 해당 RFID에 고정방향이 있으면 TargetDirection을 강제 설정
if (agv.TargetRFID.Value == rfidTagNo)
{
var destRFID = FindRFIDPoint(rfidTagNo);
if (destRFID != null && destRFID.FixedDirection.HasValue)
{
agv.TargetDirection = destRFID.FixedDirection.Value;
}
}
this.Invalidate();
return true;
}
return false;
}
public bool SetTargetPosition(uint rfidValue)
{
var rfidPoint = FindRFIDPoint(rfidValue);
if (rfidPoint != null)
{
agv.TargetRFID = rfidPoint;
this.Invalidate();
return true;
}
return false;
}
private PathResult CalculatePath(RFIDPoint start, RFIDPoint end)
{
var openList = new List<RFIDPoint> { start };
var closedList = new List<RFIDPoint>();
var cameFrom = new Dictionary<uint, RFIDPoint>();
var gScore = new Dictionary<RFIDPoint, float> { { start, 0 } };
var fScore = new Dictionary<RFIDPoint, float> { { start, Heuristic(start.Location, end.Location) } };
while (openList.Count > 0)
{
var current = openList.OrderBy(p => fScore.ContainsKey(p) ? fScore[p] : float.MaxValue).First();
if (current.Location == end.Location)
{
return ReconstructPath(cameFrom, current);
}
openList.Remove(current);
closedList.Add(current);
foreach (var neighbor in GetNeighbors(current))
{
if (closedList.Contains(neighbor))
continue;
float tentativeGScore = gScore[current] + Distance(current.Location, neighbor.Location);
if (!openList.Contains(neighbor))
openList.Add(neighbor);
else if (tentativeGScore >= gScore[neighbor])
continue;
cameFrom[neighbor.Value] = current;
gScore[neighbor] = tentativeGScore;
fScore[neighbor] = gScore[neighbor] + Heuristic(neighbor.Location, end.Location);
}
}
return new PathResult
{
Success = true,
Path = openList,
};
}
private PathResult ReconstructPath(Dictionary<uint, RFIDPoint> cameFrom, RFIDPoint current)
{
var path = new List<RFIDPoint> { current };
while (cameFrom.ContainsKey(current.Value))
{
current = cameFrom[current.Value];
path.Insert(0, current);
}
return new PathResult
{
Success = true,
Path = path,
};
}
private float Heuristic(Point a, Point b)
{
return (float)Math.Sqrt(Math.Pow(a.X - b.X, 2) + Math.Pow(a.Y - b.Y, 2));
}
private float Distance(Point a, Point b)
{
var rfidA = RFIDPoints.FirstOrDefault(p => p.Location == a);
var rfidB = RFIDPoints.FirstOrDefault(p => p.Location == b);
if (rfidA == null || rfidB == null) return float.MaxValue;
var connection = rfidConnections.FirstOrDefault(c =>
(c.StartRFID == rfidA.Value && c.EndRFID == rfidB.Value) ||
(c.IsBidirectional && c.StartRFID == rfidB.Value && c.EndRFID == rfidA.Value));
if (connection != null)
{
return connection.Distance;
}
return float.MaxValue;
}
private List<RFIDPoint> GetNeighbors(RFIDPoint point)
{
var neighbors = new List<RFIDPoint>();
var currentRfidPoint = RFIDPoints.FirstOrDefault(p => p.Location == point.Location);
if (currentRfidPoint == null) return neighbors;
uint currentRfid = currentRfidPoint.Value;
foreach (var connection in rfidConnections)
{
uint neighborRfidVal = 0;
if (connection.StartRFID == currentRfid)
{
neighborRfidVal = connection.EndRFID;
}
else if (connection.EndRFID == currentRfid && connection.IsBidirectional)
{
neighborRfidVal = connection.StartRFID;
}
if (neighborRfidVal != 0)
{
var neighborRfidPoint = RFIDPoints.FirstOrDefault(p => p.Value == neighborRfidVal);
if (neighborRfidPoint != null)
{
neighbors.Add(neighborRfidPoint);
}
}
}
return neighbors.Distinct().ToList();
}
public PathResult CalculatePath(uint tagStrt, uint tagEnd)
{
var retval = new PathResult
{
Message = string.Empty,
Success = false,
Path = new List<RFIDPoint>(),
};
var sp = tagStrt; //만약시작위치가 없다면 항상 충전기 기준으로 한다
var ep = tagEnd;
var startPoint = FindRFIDPoint(sp);
var endPoint = FindRFIDPoint(ep);
if (startPoint == null || endPoint == null)
{
retval.Message = "유효한 RFID 값을 입력해주세요.";
return retval;
}
retval = CalculatePath(startPoint, endPoint);
if (retval.Path != null && retval.Path.Any())
{
//SetCurrentPath(retval.Path);
//// 경로 상의 모든 RFID 값을 가져옴
//var rfidPath = new List<uint>();
//foreach (var point in path)
//{
// var rfid = GetRFIDPoints()
// .FirstOrDefault(r => r.Location == point);
// if (rfid != null)
// {
// rfidPath.Add(rfid.RFIDValue);
// }
//}
retval.Success = true;
}
else
{
retval.Message = "경로를 찾을 수 없습니다";
}
return retval;
}
private void AddNearbyRFIDPoints(RFIDLine line)
{
const float NEARBY_DISTANCE = 20.0f; // 근처로 간주하는 거리를 50에서 20으로 줄임
// 선 근처의 RFID 포인트들을 찾아서 거리에 따라 정렬
var nearbyPoints = new List<(RFIDPoint Point, float Distance, float ProjectionRatio)>();
foreach (var rfid in RFIDPoints)
{
if (rfid.Location == line.StartPoint || rfid.Location == line.EndPoint)
continue;
// 선분과 RFID 포인트 사이의 최단 거리 계산
float distance = GetDistanceToLine(rfid.Location, line.StartPoint, line.EndPoint);
if (distance <= NEARBY_DISTANCE)
{
// 시작점으로부터의 투영 비율 계산 (0~1 사이 값)
float projectionRatio = GetProjectionRatio(rfid.Location, line.StartPoint, line.EndPoint);
if (projectionRatio >= 0 && projectionRatio <= 1) // 선분 위에 있는 점만 포함
{
nearbyPoints.Add((rfid, distance, projectionRatio));
}
}
}
// 시작점에서 끝점 방향으로 정렬
nearbyPoints.Sort((a, b) => a.ProjectionRatio.CompareTo(b.ProjectionRatio));
// 이전 RFID 값과 위치를 저장
//uint prevRFID = line.StartRFID;
Point prevPoint = line.StartPoint;
// 정렬된 포인트들을 순차적으로 연결
foreach (var item in nearbyPoints)
{
var rfidLine = new RFIDLine
{
StartPoint = prevPoint,
EndPoint = item.Point.Location,
};
rfidLines.Add(rfidLine);
// 현재 포인트를 다음 선분의 시작점으로 설정
prevPoint = item.Point.Location;
}
// 마지막 포인트와 원래 선의 끝점을 연결
var finalLine = new RFIDLine
{
StartPoint = prevPoint,
EndPoint = line.EndPoint,
};
rfidLines.Add(finalLine);
// 원래 선은 제거
rfidLines.Remove(line);
}
public void SetRFIDPoints(List<RFIDPoint> points)
{
RFIDPoints = points;
this.Invalidate();
}
public void SetAGV(AGV vehicle)
{
agv = vehicle;
this.Invalidate();
}
public void SetMapTexts(List<MapText> texts)
{
mapTexts = texts;
if (mapTexts == null) mapTexts = new List<MapText>();
this.Invalidate();
}
public void SetCustomLines(List<CustomLine> lines)
{
customLines = lines;
if (customLines == null) customLines = new List<CustomLine>();
this.Invalidate();
}
public void SetIsAddingText(bool value)
{
isAddingText = value;
isDrawingCustomLine = false;
this.Cursor = value ? Cursors.IBeam : Cursors.Default;
}
public void SetIsDrawingCustomLine(bool value)
{
isDrawingCustomLine = value;
isAddingText = false;
this.Cursor = value ? Cursors.Cross : Cursors.Default;
}
public void SetIsDrawingRFIDLine(bool value)
{
isDrawingRFIDLine = value;
isDrawingCustomLine = false;
isAddingText = false;
this.Cursor = value ? Cursors.Cross : Cursors.Default;
}
public void SetIsDeletingRFIDLine(bool value)
{
isDeletingRFIDLine = value;
isDrawingCustomLine = false;
isAddingText = false;
isDrawingRFIDLine = false;
this.Cursor = value ? GetScissorsCursor() : Cursors.Default;
}
public void SetIsDrawingLine(bool value)
{
isDrawingLine = value;
isDrawingCustomLine = false;
isAddingText = false;
isDrawingRFIDLine = false;
this.Cursor = value ? Cursors.Cross : Cursors.Default;
}
public enum eMouseMode : byte
{
Default = 0,
pan,
rfidcut,
addtext,
addcustomline,
addrfidpoint,
addrfidline,
}
private eMouseMode mousemode = eMouseMode.Default;
public eMouseMode MouseMode
{
get { return mousemode; }
set
{
if (this.mousemode == value) mousemode = eMouseMode.Default;
else this.mousemode = value;
switch (this.mousemode)
{
case eMouseMode.pan: this.Cursor = Cursors.Hand; break;
case eMouseMode.addrfidline: this.Cursor = Cursors.Default; break;
case eMouseMode.addrfidpoint: this.Cursor = Cursors.Default; break;
case eMouseMode.addtext: this.Cursor = Cursors.Default; break;
case eMouseMode.addcustomline: this.Cursor = Cursors.Default; break;
default: this.Cursor = Cursors.Default; break;
}
previewStartPoint = null;
Invalidate();
}
}
public void SetIsAddingMagnet(bool value)
{
}
public void SetIsAddingPoint(bool value)
{
isDrawingCustomLine = false;
isDrawingLine = false;
isDrawingRFIDLine = false;
isAddingPoint = value;
this.Cursor = value ? Cursors.Cross : Cursors.Default;
}
private Cursor GetScissorsCursor()
{
// 가위 커서 아이콘 생성
using (var bitmap = new Bitmap(32, 32))
using (var g = Graphics.FromImage(bitmap))
{
g.Clear(Color.Transparent);
// 가위 모양 그리기
using (var pen = new Pen(Color.Black, 2))
{
// 가위 손잡이
g.DrawEllipse(pen, 12, 20, 8, 8);
g.DrawLine(pen, 16, 20, 16, 16);
// 가위 날
g.DrawLine(pen, 16, 16, 8, 8);
g.DrawLine(pen, 16, 16, 24, 8);
}
return new Cursor(bitmap.GetHicon());
}
}
public void SetCurrentPath(List<RFIDPoint> path)
{
agv.CurrentPath = path;
this.Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
//base.OnPaint(e);
e.Graphics.TranslateTransform(offset.X, offset.Y);
e.Graphics.ScaleTransform(zoom, zoom);
DrawRFIDLines(e.Graphics);
DrawRFIDPoints(e.Graphics);
//DrawCustomLines(e.Graphics);
DrawMapTexts(e.Graphics);
DrawPath(e.Graphics);
DrawAGV(e.Graphics);
DrawAGVMotor(e.Graphics);
DrawTargetFlag(e.Graphics); // 목적지 깃발 그리기 추가
// 선택된 개체 강조 표시
if (selectedRFID != null)
{
using (Pen pen = new Pen(Color.Magenta, 2))
{
pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
e.Graphics.DrawEllipse(pen,
selectedRFID.Location.X - 10,
selectedRFID.Location.Y - 10,
20, 20);
}
}
if (selectedRFIDLine != null)
{
using (Pen pen = new Pen(Color.Magenta, 2))
{
pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
e.Graphics.DrawLine(pen, selectedRFIDLine.StartPoint, selectedRFIDLine.EndPoint);
}
}
// 미리보기 라인 그리기
if (previewStartPoint.HasValue)
{
using (Pen previewPen = new Pen(Color.FromArgb(180, Color.Yellow), LINE_WIDTH))
{
previewPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
e.Graphics.DrawLine(previewPen, previewStartPoint.Value, currentMousePosition);
}
}
// 그래픽스 변환 초기화
e.Graphics.ResetTransform();
// 툴바 버튼 그리기
foreach (var item in this.toolbarRects)
DrawToolbarButton(e.Graphics, item.Bounds, item.Title, item.isHovering);
//예측값디스플레잉(임시)
if (PredictResult != null)
{
var str = $"{PredictResult.ReasonCode}|{PredictResult.MoveState}|{PredictResult.Direction}|Next:{PredictResult.NextRFID}";
var strsize = e.Graphics.MeasureString(str, this.Font);
e.Graphics.DrawString(str, this.Font, Brushes.Red, this.Right - strsize.Width - 10, this.Bottom - strsize.Height - 10);
}
}
private void DrawRFIDPoints(Graphics g)
{
// RFID 포인트 그리기
foreach (var rfid in RFIDPoints)
{
var MarkerSize = 5;
var half = MarkerSize / 2f;
rfid.Bounds = new RectangleF(rfid.Location.X - half, rfid.Location.Y - half, MarkerSize, MarkerSize);
// 종단 RFID는 특별한 색상으로 표시
Color pointColor;
if (rfid.IsTerminal)
{
pointColor = Color.Orange; // 종단은 주황색
}
else if (rfid.IsRotatable)
{
pointColor = Color.Yellow; // 회전 가능은 노란색
}
else
{
pointColor = Color.Green; // 일반은 초록색
}
using (var brush = new SolidBrush(pointColor))
{
g.FillEllipse(brush, rfid.Bounds);
}
// 고정방향이 있으면 테두리 색상 표시
if (rfid.FixedDirection.HasValue)
{
Color borderColor = rfid.FixedDirection.Value == Direction.Forward ? Color.DeepSkyBlue : Color.Gold;
using (var pen = new Pen(borderColor, 2))
{
g.DrawEllipse(pen, rfid.Bounds.Expand(5, 5));
}
}
// 종단 RFID는 특별한 테두리 표시
if (rfid.IsTerminal)
{
using (var pen = new Pen(Color.Red, 3))
{
pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
g.DrawEllipse(pen, rfid.Bounds.Expand(8, 8));
}
}
var str = rfid.Value.ToString();
g.DrawString(str, this.Font, Brushes.DarkGray, rfid.Bounds.X, rfid.Bounds.Y + 5);
}
}
private void DrawAGV(Graphics g)
{
var agvsize = 30;
var halfsize = (int)(agvsize / 2);
// AGV의 현재 위치를 중심으로 하는 원
var circleRect = new Rectangle(
agv.CurrentRFID.Location.X - halfsize,
agv.CurrentRFID.Location.Y - halfsize,
agvsize, agvsize);
// --- BodyAngle이 결정되지 않은 경우: 기본 방향으로 그림 ---
Color bgcolor = agv.BatteryLevel > 80 ? Color.Lime : (agv.BatteryLevel > 60 ? Color.Gold : Color.Tomato);
using (var circleBrush = new SolidBrush(Color.FromArgb(150, bgcolor)))
g.FillEllipse(circleBrush, circleRect);
using (var circlePen = new Pen(Color.Black, 2))
g.DrawEllipse(circlePen, circleRect);
//motor direction
var str = agv.CurrentMOTDirection.ToString().Substring(0, 1);
var strsize = g.MeasureString(str, this.Font);
g.DrawString(str, this.Font, Brushes.White, circleRect, new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center
});
//body direction
str = agv.CurrentAGVDirection.ToString().Substring(0, 1).ToUpper();
strsize = g.MeasureString(str, this.Font);
g.DrawString(str, this.Font, Brushes.Gold, circleRect.X + (circleRect.Width / 2f) - (strsize.Width / 2f), circleRect.Bottom + 3);
// 과거 이동 경로 화살표 그리기
DrawMovementHistoryArrows(g);
}
private void DrawAGVMotor(Graphics g)
{
var agvsize = 30;
var halfsize = (int)(agvsize / 2);
// 삼각형 포인트 계산 (회전 중심점 0,0 기준)
Point[] trianglePoints = new Point[3];
var arrowSize = halfsize - 5;
trianglePoints[0] = new Point(0, -arrowSize); // 꼭짓점
trianglePoints[1] = new Point(-arrowSize, arrowSize); // 왼쪽 아래
trianglePoints[2] = new Point(arrowSize, arrowSize); // 오른쪽 아래
// 삼각형 그리기
using (var arrowBrush = new SolidBrush(Color.FromArgb(200, Color.White)))
g.FillPolygon(arrowBrush, trianglePoints);
using (var arrowPen = new Pen(Color.Black, 2))
g.DrawPolygon(arrowPen, trianglePoints);
}
// 과거 이동 경로를 화살표로 표시
private void DrawMovementHistoryArrows(Graphics g)
{
if (agv.MovementHistory.Count < 2)
return;
// 최근 3개의 이동 경로 표시 (가장 오래된 것부터)
int startIndex = Math.Max(0, agv.MovementHistory.Count - 3);
for (int i = startIndex; i < agv.MovementHistory.Count - 1; i++)
{
var startRFID = agv.MovementHistory[i];
var endRFID = agv.MovementHistory[i + 1];
//var startPos = agv.PositionHistory[i];
//var endPos = agv.PositionHistory[i + 1];
// 시간에 따른 투명도 계산
int age = agv.MovementHistory.Count - 1 - i;
int alpha = Math.Max(50, 255 - (age * 50));
var directConnection = rfidConnections.FirstOrDefault(c =>
(c.StartRFID == startRFID.rfid && c.EndRFID == endRFID.rfid) ||
(c.IsBidirectional && c.StartRFID == endRFID.rfid && c.EndRFID == startRFID.rfid));
if (directConnection != null)
{
// 직접 연결된 경우: 실선 화살표
Color arrowColor = (directConnection.StartRFID == startRFID.rfid) ? Color.Lime : Color.Red;
arrowColor = Color.FromArgb(alpha, arrowColor);
DrawArrow(g, startRFID.Position, endRFID.Position, arrowColor, 3);
}
else
{
// 직접 연결되지 않은 경우: 경로 탐색 후 점선 화살표 체인
var pathResult = CalculatePath(startRFID.rfid, endRFID.rfid);
if (pathResult.Success && pathResult.Path != null && pathResult.Path.Count > 1)
{
// 경로의 첫 단계 방향으로 전체 색상 결정
Color arrowColor = Color.Gray;
var firstStepEndPoint = pathResult.Path[1];
var firstStepEndRfidPoint = RFIDPoints.FirstOrDefault(p => p.Location == firstStepEndPoint.Location);
if (firstStepEndRfidPoint != null)
{
var firstStepConnection = rfidConnections.FirstOrDefault(c =>
(c.StartRFID == startRFID.rfid && c.EndRFID == firstStepEndRfidPoint.Value) ||
(c.IsBidirectional && c.StartRFID == firstStepEndRfidPoint.Value && c.EndRFID == startRFID.rfid));
if (firstStepConnection != null)
{
arrowColor = (firstStepConnection.StartRFID == startRFID.rfid) ? Color.Lime : Color.Red;
}
}
arrowColor = Color.FromArgb(alpha, arrowColor);
// 경로의 각 세그먼트를 점선 화살표로 그리기
for (int j = 0; j < pathResult.Path.Count - 1; j++)
{
Point segmentStart = pathResult.Path[j].Location;
Point segmentEnd = pathResult.Path[j + 1].Location;
DrawDashedArrow(g, segmentStart, segmentEnd, arrowColor, 3);
}
}
}
}
}
// 점선 화살표 그리기 헬퍼 메서드
private void DrawDashedArrow(Graphics g, Point start, Point end, Color color, int width)
{
using (var pen = new Pen(color, width))
{
pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
// 선 그리기
g.DrawLine(pen, start, end);
// 화살표 머리 그리기 (실선으로)
pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Solid;
var arrowSize = 8;
var angle = Math.Atan2(end.Y - start.Y, end.X - start.X);
var arrowAngle = Math.PI / 6; // 30도
// 화살표 끝점에서 약간 뒤로 이동
var arrowStart = new PointF(
end.X - (float)(arrowSize * Math.Cos(angle)),
end.Y - (float)(arrowSize * Math.Sin(angle))
);
// 화살표 날개 그리기
var arrow1 = new PointF(
arrowStart.X - (float)(arrowSize * Math.Cos(angle - arrowAngle)),
arrowStart.Y - (float)(arrowSize * Math.Sin(angle - arrowAngle))
);
var arrow2 = new PointF(
arrowStart.X - (float)(arrowSize * Math.Cos(angle + arrowAngle)),
arrowStart.Y - (float)(arrowSize * Math.Sin(angle + arrowAngle))
);
g.DrawLine(pen, arrowStart, arrow1);
g.DrawLine(pen, arrowStart, arrow2);
}
}
// 실선 화살표 그리기 헬퍼 메서드
private void DrawArrow(Graphics g, Point start, Point end, Color color, int width)
{
using (var pen = new Pen(color, width))
{
// 선 그리기
g.DrawLine(pen, start, end);
// 화살표 머리 그리기
var arrowSize = 8;
var angle = Math.Atan2(end.Y - start.Y, end.X - start.X);
var arrowAngle = Math.PI / 6; // 30도
// 화살표 끝점에서 약간 뒤로 이동
var arrowStart = new PointF(
end.X - (float)(arrowSize * Math.Cos(angle)),
end.Y - (float)(arrowSize * Math.Sin(angle))
);
// 화살표 날개 그리기
var arrow1 = new PointF(
arrowStart.X - (float)(arrowSize * Math.Cos(angle - arrowAngle)),
arrowStart.Y - (float)(arrowSize * Math.Sin(angle - arrowAngle))
);
var arrow2 = new PointF(
arrowStart.X - (float)(arrowSize * Math.Cos(angle + arrowAngle)),
arrowStart.Y - (float)(arrowSize * Math.Sin(angle + arrowAngle))
);
g.DrawLine(pen, arrowStart, arrow1);
g.DrawLine(pen, arrowStart, arrow2);
}
}
private void DrawCustomLines(Graphics g)
{
if (customLines == null) return;
foreach (var line in customLines)
{
using (Pen linePen = new Pen(line.LineColor, line.LineWidth))
{
g.DrawLine(linePen, line.StartPoint, line.EndPoint);
}
}
}
private void DrawMapTexts(Graphics g)
{
if (mapTexts == null) return;
foreach (var text in mapTexts)
{
var textSize = g.MeasureString(text.Text, text.Font);
if (text.Dirty)
{
text.Bounds = new RectangleF(
text.Location.X,
text.Location.Y,
textSize.Width,
textSize.Height
);
text.Dirty = false;
}
if (text.BackgroundColor != Color.Transparent)
{
using (var brush = new SolidBrush(text.BackgroundColor))
{
g.FillRectangle(brush, text.Bounds);
}
}
using (var brush = new SolidBrush(text.TextColor))
{
g.DrawString(text.Text, text.Font, brush, text.Location);
}
if (text == selectedText)
{
using (Pen pen = new Pen(Color.Blue, 1))
{
pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
g.DrawRectangle(pen, text.Bounds.X, text.Bounds.Y, text.Bounds.Width, text.Bounds.Height);
}
}
}
}
private void DrawRFIDLines(Graphics g)
{
var idx = 0;
using (Font f = new Font("arial", 4))
{
foreach (var item in rfidLines)
{
var sp = item.StartPoint;
var ep = item.EndPoint;
using (var p = new Pen(Color.FromArgb(50, Color.White), 10))
{
g.DrawLine(p, sp, ep);
var x = sp.X;
var y = sp.Y;
g.DrawString($"{idx}", f, Brushes.Gold, x, y);
x = ep.X;
y = ep.Y;
g.DrawString($"{idx}", f, Brushes.Pink, x, y);
idx++;
}
}
}
foreach (var connection in rfidConnections)
{
var startPoint = RFIDPoints.FirstOrDefault(p => p.Value == connection.StartRFID)?.Location ?? Point.Empty;
var endPoint = RFIDPoints.FirstOrDefault(p => p.Value == connection.EndRFID)?.Location ?? Point.Empty;
if (startPoint.IsEmpty || endPoint.IsEmpty) continue;
using (Pen linePen = new Pen(Color.FromArgb(50, Color.Wheat), 2))
{
if (connection.IsBidirectional)
{
// 단방향 화살표 그리기
var arrowSize = 10;
var angle = Math.Atan2(endPoint.Y - startPoint.Y, endPoint.X - startPoint.X);
var arrowPoint = new PointF(
endPoint.X - (float)(arrowSize * Math.Cos(angle)),
endPoint.Y - (float)(arrowSize * Math.Sin(angle))
);
g.DrawLine(linePen, startPoint, arrowPoint);
// 화살표 머리 그리기
var arrowAngle = Math.PI / 6;
var arrowLength = 15;
var arrow1 = new PointF(
arrowPoint.X - (float)(arrowLength * Math.Cos(angle - arrowAngle)),
arrowPoint.Y - (float)(arrowLength * Math.Sin(angle - arrowAngle))
);
var arrow2 = new PointF(
arrowPoint.X - (float)(arrowLength * Math.Cos(angle + arrowAngle)),
arrowPoint.Y - (float)(arrowLength * Math.Sin(angle + arrowAngle))
);
g.DrawLine(linePen, arrowPoint, arrow1);
g.DrawLine(linePen, arrowPoint, arrow2);
}
else
{
g.DrawLine(linePen, startPoint, endPoint);
}
}
}
// 미리보기 라인 그리기
if (previewStartPoint.HasValue && isDrawingRFIDLine)
{
using (Pen previewPen = new Pen(Color.FromArgb(180, Color.Green), 2))
{
previewPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
var currentMapPosition = ScreenToMap(currentMousePosition);
g.DrawLine(previewPen, previewStartPoint.Value, currentMapPosition);
}
}
}
private void DrawPath(Graphics g)
{
if (agv.CurrentPath == null || agv.CurrentPath.Count < 2)
return;
Color pathColor = Color.FromArgb(100, Color.Lime);
int pathWidth = 10;
using (Pen pathPen = new Pen(pathColor, pathWidth))
{
pathPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
for (int i = 0; i < agv.CurrentPath.Count - 1; i++)
{
g.DrawLine(pathPen, agv.CurrentPath[i].Location, agv.CurrentPath[i + 1].Location);
}
}
}
public List<RFIDPoint> GetRFIDPoints()
{
return RFIDPoints;
}
public List<RFIDLine> GetRFIDLines()
{
return rfidLines;
}
public void SetRFIDLines(List<RFIDLine> lines)
{
rfidLines = lines;
this.Invalidate();
}
public void AddRFIDLine(Point startPoint, Point endPoint)
{
// 시작점과 끝점 사이의 모든 RFID 포인트 찾기
var allPoints = new List<(RFIDPoint Point, float Distance)>();
var lineVector = new Point(endPoint.X - startPoint.X, endPoint.Y - startPoint.Y);
var lineLength = (float)Math.Sqrt(lineVector.X * lineVector.X + lineVector.Y * lineVector.Y);
foreach (var rfid in RFIDPoints)
{
if (rfid.Location == startPoint || rfid.Location == endPoint)
continue;
// RFID 포인트가 선 위에 있는지 확인
var pointVector = new Point(rfid.Location.X - startPoint.X, rfid.Location.Y - startPoint.Y);
var dotProduct = pointVector.X * lineVector.X + pointVector.Y * lineVector.Y;
var projectionLength = dotProduct / (lineLength * lineLength);
if (projectionLength >= 0 && projectionLength <= 1)
{
// 선과 RFID 포인트 사이의 거리 계산
var distance = GetDistanceToLine(rfid.Location, startPoint, endPoint);
if (distance <= SNAP_DISTANCE)
{
allPoints.Add((rfid, projectionLength));
}
}
}
// 시작점에서 끝점 방향으로 정렬
allPoints.Sort((a, b) => a.Distance.CompareTo(b.Distance));
// 모든 연결 정보를 포함하는 RFID 라인 생성
var line = new RFIDLine
{
StartPoint = startPoint,
EndPoint = endPoint,
};
rfidLines.Add(line);
this.Invalidate();
}
public void ClearMap()
{
RFIDPoints.Clear();
mapTexts.Clear();
customLines.Clear();
rfidLines.Clear();
// 선택 상태도 초기화
selectedText = null;
selectedLine = null;
selectedRFID = null;
selectedRFIDLine = null;
draggingPoint = null;
// 미리보기 상태도 초기화
previewStartPoint = null;
// 화면 갱신
this.Invalidate();
}
public void AddRFIDPoint(Point mapLocation, uint rfidValue)
{
var rfidPoint = new RFIDPoint
{
Location = mapLocation,
Value = rfidValue
};
RFIDPoints.Add(rfidPoint);
this.Invalidate();
}
public void SaveToFile(string filename)
{
var lines = new List<string>();
// RFID 포인트 저장
lines.Add("[RFID_POINTS]");
foreach (var point in RFIDPoints)
{
lines.Add($"{point.Location.X},{point.Location.Y},{point.Value},{point.IsRotatable},{point.FixedDirection},{point.IsTerminal}");
}
// RFID 라인 저장
lines.Add("[RFID_LINES]");
foreach (var connection in rfidConnections)
{
var startPoint = RFIDPoints.First(p => p.Value == connection.StartRFID).Location;
var endPoint = RFIDPoints.First(p => p.Value == connection.EndRFID).Location;
lines.Add($"{startPoint.X},{startPoint.Y},{endPoint.X},{endPoint.Y}," +
$"{connection.StartRFID},{connection.EndRFID},{connection.IsBidirectional},{connection.Distance}");
}
// 텍스트 저장
lines.Add("[MAP_TEXTS]");
foreach (var text in mapTexts)
{
lines.Add($"{text.Location.X},{text.Location.Y},{text.TextColor.ToArgb()},{text.BackgroundColor.ToArgb()},{text.Font.Name},{text.Font.Size},{text.Text}");
}
// 커스텀 라인 저장
lines.Add("[CUSTOM_LINES]");
foreach (var line in customLines)
{
lines.Add($"{line.StartPoint.X},{line.StartPoint.Y},{line.EndPoint.X},{line.EndPoint.Y},{line.LineColor.ToArgb()},{line.LineWidth}");
}
File.WriteAllLines(filename, lines);
this.filename = filename;
}
public bool LoadFromFile(string filename, out string message)
{
this.filename = filename;
message = string.Empty;
ClearMap();
var lines = File.ReadAllLines(filename);
var section = "";
var sb = new System.Text.StringBuilder();
foreach (var line in lines)
{
if (line.StartsWith("[") && line.EndsWith("]"))
{
section = line;
continue;
}
switch (section)
{
case "[RFID_POINTS]":
var rfidParts = line.Split(',');
if (rfidParts.Length >= 3)
{
var validX = int.TryParse(rfidParts[0], out int valX);
var validY = int.TryParse(rfidParts[1], out int valY);
var validN = uint.TryParse(rfidParts[2], out uint valRfid);
if (validX && validY && validN)
{
if (RFIDPoints.Where(t => t.Value == valRfid).Any())
{
//이미존재한다
var newvalue =
sb.AppendLine($"rfid중복{valRfid}");
}
var rfidPoint = new RFIDPoint
{
Location = new Point(valX, valY),
Value = valRfid
};
// 추가 속성 로드 (기본값 처리)
if (rfidParts.Length >= 4)
{
bool isRotatable;
bool.TryParse(rfidParts[3], out isRotatable);
rfidPoint.IsRotatable = isRotatable;
}
if (rfidParts.Length >= 5 && !string.IsNullOrEmpty(rfidParts[4]))
rfidPoint.FixedDirection = (Direction)Enum.Parse(typeof(Direction), rfidParts[4]);
if (rfidParts.Length >= 6)
{
bool isTerminal;
bool.TryParse(rfidParts[5], out isTerminal);
rfidPoint.IsTerminal = isTerminal;
}
RFIDPoints.Add(rfidPoint);
}
else sb.AppendLine($"[{section}] {line}");
}
break;
case "[RFID_LINES]":
var rfidLineParts = line.Split(',');
if (rfidLineParts.Length >= 8)
{
AddRFIDLine(
new Point(int.Parse(rfidLineParts[0]), int.Parse(rfidLineParts[1])),
new Point(int.Parse(rfidLineParts[2]), int.Parse(rfidLineParts[3]))
);
}
break;
case "[MAP_TEXTS]":
var textParts = line.Split(',');
if (textParts.Length >= 7)
{
var text = new MapText
{
Location = new Point(int.Parse(textParts[0]), int.Parse(textParts[1])),
TextColor = Color.FromArgb(int.Parse(textParts[2])),
BackgroundColor = Color.FromArgb(int.Parse(textParts[3])),
Font = new Font(textParts[4], float.Parse(textParts[5])),
Text = string.Join(",", textParts.Skip(6)) // 텍스트에 쉼표가 포함될 수 있으므로
};
mapTexts.Add(text);
}
break;
case "[CUSTOM_LINES]":
var customLineParts = line.Split(',');
if (customLineParts.Length >= 6)
{
var customLine = new CustomLine
{
StartPoint = new Point(int.Parse(customLineParts[0]), int.Parse(customLineParts[1])),
EndPoint = new Point(int.Parse(customLineParts[2]), int.Parse(customLineParts[3])),
LineColor = Color.FromArgb(int.Parse(customLineParts[4])),
LineWidth = int.Parse(customLineParts[5])
};
customLines.Add(customLine);
}
break;
}
}
// RFID 연결 정보 처리
ProcessRFIDConnections();
this.Invalidate();
message = sb.ToString();
return true;
}
private void ProcessRFIDConnections()
{
rfidConnections.Clear();
var connectionSet = new HashSet<string>();
foreach (var line in rfidLines)
{
var start = line.StartPoint;
var end = line.EndPoint;
// 1. 선 위의 모든 RFID 포인트(시작, 끝 포함)를 projectionRatio로 정렬
var pointsOnThisLine = RFIDPoints
.Where(p => IsPointOnLine(p.Location, start, end, 10f)) // 오차 허용치 넉넉히
.Select(p => new
{
RFID = p.Value,
Ratio = GetProjectionRatio(p.Location, start, end)
})
.ToList();
//// 2. 시작/끝 RFID가 목록에 없으면 강제로 추가
//if (!pointsOnThisLine.Any(p => p.RFID == line.StartRFID))
// pointsOnThisLine.Add(new { RFID = line.StartRFID, Ratio = 0f });
//if (!pointsOnThisLine.Any(p => p.RFID == line.EndRFID))
// pointsOnThisLine.Add(new { RFID = line.EndRFID, Ratio = 1f });
// 3. 정렬
pointsOnThisLine = pointsOnThisLine.OrderBy(p => p.Ratio).ToList();
// 4. 순서대로 1:1 연결
for (int i = 0; i < pointsOnThisLine.Count - 1; i++)
{
var from = pointsOnThisLine[i].RFID;
var to = pointsOnThisLine[i + 1].RFID;
var key = $"{Math.Min(from, to)}_{Math.Max(from, to)}";
if (connectionSet.Contains(key)) continue;
var fromPt = RFIDPoints.FirstOrDefault(p => p.Value == from)?.Location ?? line.StartPoint;
var toPt = RFIDPoints.FirstOrDefault(p => p.Value == to)?.Location ?? line.EndPoint;
rfidConnections.Add(new RFIDConnection
{
StartRFID = from,
EndRFID = to,
Distance = GetDistance(fromPt, toPt)
});
connectionSet.Add(key);
}
}
}
// tolerance 인자를 받는 IsPointOnLine
private bool IsPointOnLine(Point point, Point lineStart, Point lineEnd, float tolerance = 10.0f)
{
var distance = GetDistanceToLine(point, lineStart, lineEnd);
if (distance > tolerance) return false;
var projectionRatio = Math.Round(GetProjectionRatio(point, lineStart, lineEnd), 2);
return projectionRatio >= 0 && projectionRatio <= 1.0;
}
private void DeleteNearbyRFIDLine(Point clickPoint)
{
const float DELETE_DISTANCE = 10.0f; // 클릭 지점으로부터의 허용 거리
RFIDLine lineToDelete = null;
float minDistance = float.MaxValue;
foreach (var line in rfidLines)
{
float distance = GetDistanceToLine(clickPoint, line.StartPoint, line.EndPoint);
if (distance < DELETE_DISTANCE && distance < minDistance)
{
minDistance = distance;
lineToDelete = line;
}
}
if (lineToDelete != null)
{
rfidLines.Remove(lineToDelete);
this.Invalidate();
}
}
private void ZoomIn()
{
zoom *= 1.2f;
zoom = Math.Min(10.0f, zoom);
this.Invalidate();
}
private void ZoomOut()
{
zoom /= 1.2f;
zoom = Math.Max(0.1f, zoom);
this.Invalidate();
}
private void ResetZoom()
{
zoom = 1.0f;
offset = PointF.Empty;
this.Invalidate();
}
private void DrawToolbarButton(Graphics g, Rectangle rect, string text, bool isHovering)
{
var color1 = isHovering ? Color.LightSkyBlue : Color.White;
var color2 = isHovering ? Color.DeepSkyBlue : Color.WhiteSmoke;
using (var brush = new LinearGradientBrush(rect, color1, color2, LinearGradientMode.Vertical))
using (var pen = new Pen(Color.Gray))
using (var font = new Font("Tahoma", 9, FontStyle.Bold))
using (var format = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center })
{
g.FillRectangle(Brushes.LightGray, rect.X + 2, rect.Y + 2, rect.Width, rect.Height);
g.FillRectangle(brush, rect);
g.DrawRectangle(pen, rect);
g.DrawString(text, font, Brushes.Black, rect, format);
}
}
/// <summary>
/// 목적지에 깃발 표시
/// </summary>
/// <param name="g"></param>
private void DrawTargetFlag(Graphics g)
{
//대상이없다면 진행하지 않습니다
if (agv.TargetRFID.IsEmpty) return;
// 바닥에 흰색 원 그리기
using (var baseBrush = new SolidBrush(Color.Red))
using (var basePen = new Pen(Color.Black, 1))
{
var baseSize = 8;
g.FillEllipse(baseBrush,
agv.TargetRFID.Location.X - baseSize / 2,
agv.TargetRFID.Location.Y - baseSize / 2,
baseSize, baseSize);
g.DrawEllipse(basePen,
agv.TargetRFID.Location.X - baseSize / 2,
agv.TargetRFID.Location.Y - baseSize / 2,
baseSize, baseSize);
}
// 깃대 그리기 (길이를 2/3로 줄임)
using (var polePen = new Pen(Color.Brown, 3))
{
var poleLength = 27; // 40 * 2/3 ≈ 27
g.DrawLine(polePen,
agv.TargetRFID.Location.X,
agv.TargetRFID.Location.Y,
agv.TargetRFID.Location.X,
agv.TargetRFID.Location.Y - poleLength);
}
// 깃발 그리기
Point[] flagPoints = new Point[3];
flagPoints[0] = new Point(agv.TargetRFID.Location.X, agv.TargetRFID.Location.Y - 27); // 깃대 길이에 맞춤
flagPoints[1] = new Point(agv.TargetRFID.Location.X + 20, agv.TargetRFID.Location.Y - 22);
flagPoints[2] = new Point(agv.TargetRFID.Location.X, agv.TargetRFID.Location.Y - 17);
using (var flagBrush = new SolidBrush(Color.Red))
using (var flagPen = new Pen(Color.DarkRed, 1))
{
g.FillPolygon(flagBrush, flagPoints);
g.DrawPolygon(flagPen, flagPoints);
}
}
public AGVActionPrediction PredictResult = null;
// AGV 행동 예측 함수
public AGVActionPrediction PredictNextAction()
{
// 1. 위치를 모를 때 (CurrentRFID가 0 또는 미설정)
if (agv.CurrentRFID.Value == 0)
{
PredictResult = new AGVActionPrediction
{
Direction = Direction.Backward,
NextRFID = null,
Reason = "AGV 위치 미확정(처음 기동)",
ReasonCode = AGVActionReasonCode.NoPosition,
MoveState = AGVMoveState.Run
};
return PredictResult;
}
// 2. 경로가 없거나 현재 위치가 경로에 없음
if ((agv.CurrentPath?.Count ?? 0) < 2 )
{
PredictResult = new AGVActionPrediction
{
Direction = agv.CurrentMOTDirection,
NextRFID = null,
Reason = "경로 없음 또는 현재 위치 미확정",
ReasonCode = AGVActionReasonCode.NoPath,
MoveState = AGVMoveState.Stop
};
return PredictResult;
}
// 3. 경로상에서 다음 RFID 예측
int idx = agv.CurrentPath.FindIndex(p => p.Value == agv.CurrentRFID.Value);
if (idx < 0)
{
PredictResult = new AGVActionPrediction
{
Direction = agv.CurrentMOTDirection,
NextRFID = null,
Reason = "현재 위치가 경로에 없음",
ReasonCode = AGVActionReasonCode.NotOnPath,
MoveState = AGVMoveState.Stop
};
return PredictResult;
}
// 4. 목적지 도달 전, 방향 미리 판단 및 회전 위치 예측
// 목적지 RFID 정보
var destPoint = agv.CurrentPath.Last();
var destRFID = RFIDPoints.FirstOrDefault(r => r.Value == destPoint.Value); //
if (destRFID != null && destRFID.FixedDirection.HasValue) //대상에 진입방향이 고정되어 있는지?
{
// 목적지에 도달할 때의 방향 예측
if (agv.CurrentPath.Count >= 2)
{
// 목적지 바로 전 위치에서 목적지로 이동할 때의 방향
var beforeDest = agv.CurrentPath[agv.CurrentPath.Count - 2];
float arriveDeltaX = destPoint.Location.X - beforeDest.Location.X;
float arriveDeltaY = destPoint.Location.Y - beforeDest.Location.Y;
Direction arriveDir = (Math.Abs(arriveDeltaX) > Math.Abs(arriveDeltaY)) ?
(arriveDeltaX > 0 ? Direction.Forward : Direction.Backward) :
(arriveDeltaY > 0 ? Direction.Forward : Direction.Backward);
if (arriveDir != destRFID.FixedDirection.Value)
{
// 목적지 도달 전, 마지막 회전 가능한 RFID를 찾음
int lastRotatableIdx = -1;
for (int i = 0; i < agv.CurrentPath.Count - 1; i++)
{
var rfid = RFIDPoints.FirstOrDefault(r => r.Location == agv.CurrentPath[i].Location);
if (rfid != null && rfid.IsRotatable)
lastRotatableIdx = i;
}
if (lastRotatableIdx >= 0)
{
// 회전 가능한 위치에 도달하면 NeedTurn 반환 (STOP)
if (idx == lastRotatableIdx)
{
var rfid = RFIDPoints.FirstOrDefault(r => r.Location == agv.CurrentPath[lastRotatableIdx].Location);
PredictResult = new AGVActionPrediction
{
Direction = agv.CurrentMOTDirection,
NextRFID = rfid?.Value,
Reason = "목적지 진입방향 맞추기 위해 회전 필요",
ReasonCode = AGVActionReasonCode.NeedTurn,
MoveState = AGVMoveState.Stop
};
return PredictResult;
}
else if (idx < lastRotatableIdx)
{
// 회전 가능한 위치까지 이동 안내 (RUN)
var rfid = RFIDPoints.FirstOrDefault(r => r.Location == agv.CurrentPath[lastRotatableIdx].Location);
float moveDeltaX = agv.CurrentPath[lastRotatableIdx].Location.X - agv.CurrentRFID.Location.X;
float moveDeltaY = agv.CurrentPath[lastRotatableIdx].Location.Y - agv.CurrentRFID.Location.Y;
Direction moveDir = (Math.Abs(moveDeltaX) > Math.Abs(moveDeltaY)) ?
(moveDeltaX > 0 ? Direction.Forward : Direction.Backward) :
(moveDeltaY > 0 ? Direction.Forward : Direction.Backward);
PredictResult = new AGVActionPrediction
{
Direction = moveDir,
NextRFID = rfid?.Value,
Reason = "회전 가능한 위치로 이동 중",
ReasonCode = AGVActionReasonCode.Normal,
MoveState = AGVMoveState.Run
};
return PredictResult;
}
}
// 회전 가능한 위치가 없음 (STOP)
PredictResult = new AGVActionPrediction
{
Direction = agv.CurrentMOTDirection,
NextRFID = null,
Reason = "경로상에 회전 가능한 위치가 없음",
ReasonCode = AGVActionReasonCode.NoTurnPoint,
MoveState = AGVMoveState.Stop
};
return PredictResult;
}
}
}
// 5. 목적지 도달 시(방향이 맞는 경우) (STOP)
if (idx == agv.CurrentPath.Count - 1)
{
PredictResult = new AGVActionPrediction
{
Direction = agv.CurrentMOTDirection,
NextRFID = null,
Reason = "경로의 마지막 지점(목적지 도달)",
ReasonCode = AGVActionReasonCode.Arrived,
MoveState = AGVMoveState.Stop
};
return PredictResult;
}
// 6. 일반 경로 주행 (RUN)
var nextPoint = agv.CurrentPath[idx + 1];
var nextRFID = RFIDPoints.FirstOrDefault(r => r.Value == nextPoint.Value)?.Value;
// X, Y 좌표 모두 고려한 방향 판단
float deltaX = nextPoint.Location.X - agv.CurrentRFID.Location.X;
float deltaY = nextPoint.Location.Y - agv.CurrentRFID.Location.Y;
Direction nextDir = (Math.Abs(deltaX) > Math.Abs(deltaY)) ?
(deltaX > 0 ? Direction.Forward : Direction.Backward) :
(deltaY > 0 ? Direction.Forward : Direction.Backward);
PredictResult = new AGVActionPrediction
{
Direction = nextDir,
NextRFID = nextRFID,
Reason = null,
ReasonCode = AGVActionReasonCode.Normal,
MoveState = AGVMoveState.Run
};
return PredictResult;
}
#endregion
#region
public float GetDistance(Point p1, Point p2)
{
float dx = p1.X - p2.X;
float dy = p1.Y - p2.Y;
return (float)Math.Sqrt(dx * dx + dy * dy); // double을 float로 명시적 캐스팅
}
// 화면 좌표를 실제 맵 좌표로 변환
public Point ScreenToMap(Point screenPoint)
{
int adjustedX = screenPoint.X;
return new Point(
(int)((adjustedX - offset.X) / zoom),
(int)((screenPoint.Y - offset.Y) / zoom)
);
}
private float GetProjectionRatio(Point point, Point lineStart, Point lineEnd)
{
float lineLength = GetDistance(lineStart, lineEnd);
if (lineLength == 0) return 0;
return ((point.X - lineStart.X) * (lineEnd.X - lineStart.X) +
(point.Y - lineStart.Y) * (lineEnd.Y - lineStart.Y)) / (lineLength * lineLength);
}
private float GetDistanceToLine(Point point, Point lineStart, Point lineEnd)
{
float lineLength = GetDistance(lineStart, lineEnd);
if (lineLength == 0) return GetDistance(point, lineStart);
float t = ((point.X - lineStart.X) * (lineEnd.X - lineStart.X) +
(point.Y - lineStart.Y) * (lineEnd.Y - lineStart.Y)) / (lineLength * lineLength);
t = Math.Max(0, Math.Min(1, t));
float projectionX = lineStart.X + t * (lineEnd.X - lineStart.X);
float projectionY = lineStart.Y + t * (lineEnd.Y - lineStart.Y);
return GetDistance(point, new Point((int)projectionX, (int)projectionY));
}
#endregion
}
}