using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Windows.Forms; using AGVControl.Models; using AR; using COMM; namespace AGVControl { 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 RFIDPoints; private List mapTexts; private List customLines; private List rfidLines; private HashSet 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 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(); mapTexts = new List(); customLines = new List(); rfidLines = new List(); rfidConnections = new HashSet(); agv = new AGV(); UpdateToolbarRects(); } #endregion #region 이벤트 핸들러 public event EventHandler 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 "clear": this.agv.MainPath.Clear(); 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 PointF DrawLineWithLength(Graphics graphics, Pen pen, PointF p1, PointF p2, float length) { // 두 점 사이의 벡터 계산 float dx = p2.X - p1.X; float dy = p2.Y - p1.Y; // 두 점 사이의 거리 계산 float distance = (float)Math.Sqrt(dx * dx + dy * dy); // 거리가 0이면 선을 그릴 수 없음 if (distance == 0) return PointF.Empty; // 단위 벡터 계산 (방향) float unitX = dx / distance; float unitY = dy / distance; // 시작점에서 지정된 길이만큼 떨어진 끝점 계산 PointF endPoint = new PointF( p1.X + unitX * length, p1.Y + unitY * length ); // 선 그리기 graphics.DrawLine(pen, p1, endPoint); return endPoint; } private void UpdateToolbarRects() { int x, y, c, row; var idx = 0; if (this.toolbarRects == null) this.toolbarRects = new List(); else this.toolbarRects.Clear(); //left toolbar x = TOOLBAR_MARGIN; y = 10; var menu_left = new string[] { "+", "-", "1:1", "Cut" }; foreach (var item in menu_left) { toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = item, Bounds = new Rectangle(x, y, TOOLBAR_WIDTH - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) }); y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN; } //right toolbar y = 10; row = 0; x = DisplayRectangle.Right - TOOLBAR_WIDTHR - TOOLBAR_MARGIN; var menu_right = new string[] { "Text", "Line", "Point", "Magnet", "Load", "Save", "Pos", "Path", "Clear" }; foreach (var item in menu_right) { toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = item, 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) }); //y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN; //toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Clear", 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); } /// /// 현재위치를 설정합니다 /// /// RFID TagNo /// 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.MainPath.Count > 0) { // 현재 위치가 경로에 있는지 확인 bool isOnPath = agv.MainPath.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 { start }; var closedList = new List(); var cameFrom = new Dictionary(); var gScore = new Dictionary { { start, 0 } }; var fScore = new Dictionary { { start, Heuristic(start.Location, end.Location) } }; var maxcount = 50; var loopcount = 0; 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 { Path = openList, }; } private PathResult ReconstructPath(Dictionary cameFrom, RFIDPoint current) { var path = new List { current }; while (cameFrom.ContainsKey(current.Value)) { current = cameFrom[current.Value]; path.Insert(0, current); } return new PathResult { 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) { return (float)Math.Sqrt(Math.Pow(a.X - b.X, 2) + Math.Pow(a.Y - b.Y, 2)); //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.P1.Value == rfidA.Value && c.P2.Value == rfidB.Value) || // (c.IsBidirectional && c.P1.Value == rfidB.Value && c.P2.Value == rfidA.Value)); //if (connection != null) //{ // return connection.Distance; //} //return float.MaxValue; } /// /// 이웃포인터를 반환합니다 /// /// /// private List GetNeighbors(RFIDPoint pt) { var neighbors = new List(); //값이 없는 경우 오류 반환 if (pt == null) return neighbors; //연결정보에서 데이터를 찾은 후 반환한다 foreach (var connection in rfidConnections) { RFIDPoint nPT = null; if (connection.P1.Value == pt.Value) { nPT = connection.P2; } else if (connection.P2.Value == pt.Value) { nPT = connection.P1; } if (nPT != null) neighbors.Add(nPT); } //중복제거후 반한 return neighbors.Distinct().ToList(); } public PathResult CalculatePath(uint tagStrt, uint tagEnd) { var retval = new PathResult { Message = string.Empty, Path = new List(), }; 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.Success == false) retval.Message = "경로를 찾을 수 없습니다"; return retval; } public void SetRFIDPoints(List points) { RFIDPoints = points; this.Invalidate(); } public void SetAGV(AGV vehicle) { agv = vehicle; this.Invalidate(); } public void SetMapTexts(List texts) { mapTexts = texts; if (mapTexts == null) mapTexts = new List(); this.Invalidate(); } public void SetCustomLines(List lines) { customLines = lines; if (customLines == null) customLines = new List(); 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 path) { agv.MainPath = 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); } //경로정보표시(임시) var pathstr = ""; if (agv.MainPath.Any()) { pathstr = "● Path : " + string.Join("▶", agv.MainPath.Select(t => t.Value).ToArray()); //pathstr += "\n● Target Direction Match : " + (agv.IsTargetDirectionMatch ? "O" : "X"); } else pathstr = "● Path : no data"; using (var f = new Font("Arial", 10, FontStyle.Bold)) e.Graphics.DrawString(pathstr, f, Brushes.DeepSkyBlue, this.Left + 65, this.Top + 10); var histstr = ""; if (agv.MovementHistory.Count > 1) { histstr = "● Route : " + string.Join("▶", agv.MovementHistory.Select(t => t.Value.ToString() + $"[{t.Direction.ToString()[0]}]").ToArray()); } else histstr = "● Route : no data"; using (var f = new Font("Arial", 10, FontStyle.Bold)) e.Graphics.DrawString(histstr, f, Brushes.DeepSkyBlue, this.Left + 65, this.Top + 30); } 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.IsRotatable) { Color borderColor = Color.Yellow; using (var pen = new Pen(borderColor, 2)) { g.DrawEllipse(pen, rfid.Bounds.Expand(16, 16)); g.DrawEllipse(pen, rfid.Bounds.Expand(10, 10)); g.DrawEllipse(pen, rfid.Bounds.Expand(4, 4)); } } // 고정방향이 있으면 테두리 색상 표시 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); //이동경로정보를 따라서 리프트의 위치를 표시해준다. if (agv.MovementHistory.Any() && agv.MovementHistory.Count > 1) { var prept = agv.MovementHistory.Skip(agv.MovementHistory.Count - 2).First(); var lstpt = agv.MovementHistory.Last(); RFIDPoint TargetPT = null; //뒤로이동하는경우라면 이전위치에 리프트가 있다. if (lstpt.Direction == Direction.Backward) { TargetPT = prept; } else //앞으로이동한다면 이동방향과 동일하다 { //이전위치는 제거 하고 처음발견된 것을 대상으로 한다 TargetPT = this.GetNeighbors(lstpt).Where(t => t.Value != prept.Value).FirstOrDefault(); } if (TargetPT != null) { using (var p = new Pen(Color.Black, 3)) { var circleRadius = 6; var pt = DrawLineWithLength(g, p, lstpt.Location, TargetPT.Location, 25); var liftRect = new RectangleF(pt.X - circleRadius, pt.Y - circleRadius, circleRadius * 2, circleRadius * 2); g.FillEllipse(Brushes.Black, liftRect); var liftColor = agv.IsTargetDirectionMatch ? Color.White : Color.HotPink; using (var pBorder = new Pen(liftColor, 3)) g.DrawEllipse(pBorder, liftRect); } } } // --- 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, 4)) 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.IsTargetDirectionMatch ? "O" : "X";// .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.P1.Value == startRFID.Value && c.P2.Value == endRFID.Value) || (c.P1.Value == endRFID.Value && c.P2.Value == startRFID.Value)); if (directConnection != null) { // 직접 연결된 경우: 실선 화살표 Color arrowColor = (directConnection.P1.Value == startRFID.Value) ? Color.Lime : Color.Red; arrowColor = Color.FromArgb(alpha, arrowColor); DrawArrow(g, startRFID.Location, endRFID.Location, arrowColor, 3); } else { // 직접 연결되지 않은 경우: 경로 탐색 후 점선 화살표 체인 var pathResult = CalculatePath(startRFID.Value, endRFID.Value); 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.P1.Value == startRFID.Value && c.P2.Value == firstStepEndRfidPoint.Value) || (c.P1.Value == firstStepEndRfidPoint.Value && c.P2.Value == startRFID.Value)); if (firstStepConnection != null) { arrowColor = (firstStepConnection.P1.Value == startRFID.Value) ? 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 / 4f * Math.Cos(angle)), end.Y - (float)(arrowSize / 4f * 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++; // } //} } //연결정보에서 그림을 그린다. var didx = 0; foreach (var connection in rfidConnections) { didx += 1; var startPoint = RFIDPoints.FirstOrDefault(p => p.Value == connection.P1.Value)?.Location ?? Point.Empty; var endPoint = RFIDPoints.FirstOrDefault(p => p.Value == connection.P2.Value)?.Location ?? Point.Empty; if (startPoint.IsEmpty || endPoint.IsEmpty) continue; //var lcolor = didx % 2 == 0 ? Color.White : Color.LightSkyBlue; using (Pen linePen = new Pen(Color.FromArgb(30, Color.White), 10)) { g.DrawLine(linePen, startPoint, endPoint); g.DrawLine(Pens.SlateGray, 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.MainPath == null || agv.MainPath.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.MainPath.Count - 1; i++) { g.DrawLine(pathPen, agv.MainPath[i].Location, agv.MainPath[i + 1].Location); } } } public List GetRFIDPoints() { return RFIDPoints; } public List GetRFIDLines() { return rfidLines; } public void SetRFIDLines(List 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(); // 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.P1.Value).Location; var endPoint = RFIDPoints.First(p => p.Value == connection.P2.Value).Location; lines.Add($"{startPoint.X},{startPoint.Y},{endPoint.X},{endPoint.Y}," + $"{connection.P1},{connection.P2},{connection.DisableP1_to_P2}{connection.DisableP2_to_P1},{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(); 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 fromItem = RFIDPoints.FirstOrDefault(p => p.Value == from); var toItem = RFIDPoints.FirstOrDefault(p => p.Value == to); var fromPt = fromItem?.Location ?? line.StartPoint; var toPt = toItem?.Location ?? line.EndPoint; rfidConnections.Add(new RFIDConnection { P1 = fromItem, P2 = toItem, 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); } } /// /// 목적지에 깃발 표시 /// /// 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); } } /// /// 목표지정으로 모터방향이 이동하고 있는가? /// history 데이터가 있어야 하며 기준데이터가 없는 경우 null 반환 /// /// public bool? IsMotDirection_To_Target() { if (agv.MovementHistory.Any() == false || agv.MovementHistory.Count < 2) return null; if (agv.MainPath.Any() == false) return null; var prept = agv.MovementHistory.Skip(agv.MovementHistory.Count - 2).First(); var lstpt = agv.MovementHistory.Last(); //현재 이후의 경로를 가져온다 var curidx = agv.MainPath.FindIndex(t => t.Value == lstpt.Value); var preidx = agv.MainPath.FindIndex(t => t.Value == prept.Value); if (curidx == -1 || preidx == -1) return null; //지정된경로 반대방향으로 이동하고 있다 return preidx < curidx; } /// /// 리프트방향과 대상위치와의 방향이 일치하는가? /// 목적지경로가 셋팅된 경우 현재 이동방향이 목적지방향과 일치하는가? /// 이동경로정보가 없거나 목적지가 없으면 null 이 반환됨 /// /// public bool? IsLiftDirectionMatch() { if (agv.MovementHistory.Any() && agv.MovementHistory.Count > 1) { RFIDPoint TargetPT = null; var prept = agv.MovementHistory.Skip(agv.MovementHistory.Count - 2).First(); var lstpt = agv.MovementHistory.Last(); //뒤로이동하는경우라면 이전위치에 리프트가 있다. if (lstpt.Direction == Direction.Backward) { TargetPT = prept; } else //앞으로 이동한다면 이동방향과 동일하다 { //이전위치는 제거 하고 처음발견된 것을 대상으로 한다 TargetPT = this.GetNeighbors(lstpt).Where(t => t.Value != prept.Value).FirstOrDefault(); } //목적지가 있다면 목적지의 방향과 일치하는지 확인해야한다 //남은경로중에 방향이 고정된 핀이 있다면 그것과 일치하는지 확인해야 한다 if (agv.MainPath.Any()) { //지정된경로 반대방향으로 이동하고 있다 if ((IsMotDirection_To_Target() ?? false) == false) { return false; } else { var nextRoutes = agv.MainPath.Skip(agv.MainPath.FindIndex(t => t.Value == lstpt.Value) + 1).ToList(); var DirectionMatch = true; foreach (var item in nextRoutes) { if (item.FixedDirection != null && item.FixedDirection != lstpt.Direction) { DirectionMatch = false; break; } } return DirectionMatch; } } else { //대상포인트와의 방향만 체크한다. //고정대상이없다면 방향이 맞는것으로 한다 return (TargetPT.FixedDirection ?? lstpt.Direction) == lstpt.Direction; } } else { //이동된경로정보가 없다면 리프트 방향을 체크할 수 없으므로 대상과 위치가 맞지 않는걸로 기본값을 설정한다 //이렇게 설정하면 대상으로 이동불가하고 뒤로 가도록 유도된다 return null; } } public AGVActionPrediction PredictResult = null; //public static double Distance(this Point pt1, Point pt2) //{ // return Math.Sqrt(Math.Pow(pt1.X-pt2.X,2)+Math.Pow(pt1.Y-pt2.Y,2)); //} // AGV 다음행동 예측 함수 public AGVActionPrediction PredictNextAction() { try { // 0. 설정경로와 리프트 방향 체크 (경로설정이 없을때에는 직선이동경로내의 방향들과 체크한다) agv.IsTargetDirectionMatch = IsLiftDirectionMatch() ?? false; // 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.MovementHistory.Any() == false || agv.MovementHistory.Count < 2) { PredictResult = new AGVActionPrediction { Direction = Direction.Backward, NextRFID = null, Reason = "AGV이동방향 알수없음", ReasonCode = AGVActionReasonCode.NoDirection, MoveState = AGVMoveState.Run }; return PredictResult; } // 3. 경로가 없거나 현재 위치가 경로에 없음 if ((agv.MainPath?.Count ?? 0) < 2) { PredictResult = new AGVActionPrediction { Direction = agv.CurrentMOTDirection, NextRFID = null, Reason = "경로 없음 또는 현재 위치 미확정", ReasonCode = AGVActionReasonCode.NoPath, MoveState = AGVMoveState.Stop }; return PredictResult; } // 4. 경로상에서 다음 RFID 예측 int idx = agv.MainPath.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 destRFID = agv.MainPath.Last(); //리프트 방향이 맞는가? var IsLiftDir = IsLiftDirectionMatch() ?? false; //모션이동방향이 맞는가? var IsMotDir = IsMotDirection_To_Target() ?? false; var PrePT = agv.MovementHistory.Skip(agv.MovementHistory.Count - 1).First(); var curPT = agv.MovementHistory.Last(); //리프트방향이 맞지 않다면 회전가능한 위치로 이동을 해야한다 if (IsLiftDir == false) { //회전가능한 위치로 이동을 해야한다 //1. 가까운 회전위치를 찾는다 var nearTurnPoint = RFIDPoints.Where(t => t.IsRotatable)?.OrderBy(t => GetDistance(t.Location, agv.CurrentRFID.Location)).FirstOrDefault() ?? null; if (nearTurnPoint == null) { return new AGVActionPrediction { Direction = agv.CurrentMOTDirection, NextRFID = null, Reason = "회전 가능한 위치가 없습니다", ReasonCode = AGVActionReasonCode.NoTurnPoint, MoveState = AGVMoveState.Stop }; } //2. 이동하기위한 경로계산 및 이동을 한다 (생성조건) //2-1. 서브경로가없는경우 //2-2. 시작과 종료번호가 다른 경우(경로가 변경이 되는 조건이다) if (agv.SubPath.Any() == false || agv.SubPath.Count < 2 || agv.SubPath.First().Value != PrePT.Value || agv.SubPath.Last().Value != nearTurnPoint.Value) { var rlt = CalculatePath(PrePT, nearTurnPoint); //이전포인트도 추가를 해준다 if (rlt.Success) agv.SubPath = rlt.Path; else { agv.SubPath.Clear(); return new AGVActionPrediction { Direction = agv.CurrentMOTDirection, NextRFID = null, Reason = "회전 위치까지의 경로를 계산할 수 없습니다", ReasonCode = AGVActionReasonCode.PathCalcError, MoveState = AGVMoveState.Stop }; } } //3. 턴위치까지 이동이 완료되지 않았다면 계속 이동을 하게한다 if (agv.CurrentRFID.Value != nearTurnPoint.Value) { //현재 모터방향을 확인하여 대상까지 이동하도록 해야한다 var curidx = agv.SubPath.FindIndex(t => t.Value == curPT.Value); var preidx = agv.SubPath.FindIndex(t => t.Value == PrePT.Value); Direction newdirection = agv.CurrentMOTDirection; string message = "턴위치로 이동중"; if (preidx > curidx) { //지정경로를 거꾸로 이동하고 있다 if (agv.CurrentMOTDirection == Direction.Forward) newdirection = Direction.Backward; else newdirection = Direction.Forward; message += "(방향전환)"; } return new AGVActionPrediction { Direction = newdirection, NextRFID = null, Reason = message, ReasonCode = AGVActionReasonCode.MoveForTurn, MoveState = AGVMoveState.Run, }; } return new AGVActionPrediction { Direction = agv.CurrentMOTDirection, NextRFID = null, Reason = "턴 완료 대기", ReasonCode = AGVActionReasonCode.NeedTurn, MoveState = AGVMoveState.Stop }; } //리프트 방향이 맞다 //모션방향이 맞지 않다면 이동방향을 변경해준다 //3. 턴위치까지 이동이 완료되지 않았다면 계속 이동을 하게한다 if (agv.CurrentRFID.Value != destRFID.Value) { //현재 모터방향을 확인하여 대상까지 이동하도록 해야한다 var curidx = agv.MainPath.FindIndex(t => t.Value == curPT.Value); var preidx = agv.MainPath.FindIndex(t => t.Value == PrePT.Value); Direction newdirection = agv.CurrentMOTDirection; string message = "목적지 이동중"; if (preidx > curidx) { //지정경로를 거꾸로 이동하고 있다 if (agv.CurrentMOTDirection == Direction.Forward) newdirection = Direction.Backward; else newdirection = Direction.Forward; message += "(방향전환)"; } return new AGVActionPrediction { Direction = newdirection, NextRFID = null, Reason = message, ReasonCode = AGVActionReasonCode.Normal, MoveState = AGVMoveState.Run, }; } // 5. 목적지 도달 시 PredictResult = new AGVActionPrediction { Direction = agv.CurrentMOTDirection, NextRFID = null, Reason = "경로의 마지막 지점(목적지 도달)", ReasonCode = AGVActionReasonCode.Arrived, MoveState = AGVMoveState.Stop }; return PredictResult; } catch (Exception ex) { PredictResult = new AGVActionPrediction { Direction = agv.CurrentMOTDirection, NextRFID = null, Reason = $"ERR:{ex.Message}", ReasonCode = AGVActionReasonCode.Unknown, MoveState = AGVMoveState.Stop }; 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 } }