refactor: Consolidate RFID mapping and add bidirectional pathfinding
Major improvements to AGV navigation system: • Consolidated RFID management into MapNode, removing duplicate RfidMapping class • Enhanced MapNode with RFID metadata fields (RfidStatus, RfidDescription) • Added automatic bidirectional connection generation in pathfinding algorithms • Updated all components to use unified MapNode-based RFID system • Added command line argument support for AGVMapEditor auto-loading files • Fixed pathfinding failures by ensuring proper node connectivity Technical changes: - Removed RfidMapping class and dependencies across all projects - Updated AStarPathfinder with EnsureBidirectionalConnections() method - Modified MapLoader to use AssignAutoRfidIds() for RFID automation - Enhanced UnifiedAGVCanvas, SimulatorForm, and MainForm for MapNode integration - Improved data consistency and reduced memory footprint 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,11 @@ namespace AGVMapEditor.Models
|
||||
/// <summary>도킹 스테이션</summary>
|
||||
Docking,
|
||||
/// <summary>충전 스테이션</summary>
|
||||
Charging
|
||||
Charging,
|
||||
/// <summary>라벨 (UI 요소)</summary>
|
||||
Label,
|
||||
/// <summary>이미지 (UI 요소)</summary>
|
||||
Image
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
15
Cs_HMI/AGVMapEditor/Models/MapData.cs
Normal file
15
Cs_HMI/AGVMapEditor/Models/MapData.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
|
||||
public class MapData
|
||||
{
|
||||
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public string Version { get; set; } = "1.0";
|
||||
}
|
||||
|
||||
}
|
||||
210
Cs_HMI/AGVMapEditor/Models/MapImage.cs
Normal file
210
Cs_HMI/AGVMapEditor/Models/MapImage.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 이미지 정보를 관리하는 클래스
|
||||
/// 디자인 요소용 이미지/비트맵 요소
|
||||
/// </summary>
|
||||
public class MapImage
|
||||
{
|
||||
/// <summary>
|
||||
/// 이미지 고유 ID
|
||||
/// </summary>
|
||||
public string ImageId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 파일 경로
|
||||
/// </summary>
|
||||
public string ImagePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 맵 상의 위치 좌표 (좌상단 기준)
|
||||
/// </summary>
|
||||
public Point Position { get; set; } = Point.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 크기 (원본 크기 기준 배율)
|
||||
/// </summary>
|
||||
public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f);
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 투명도 (0.0 ~ 1.0)
|
||||
/// </summary>
|
||||
public float Opacity { get; set; } = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 회전 각도 (도 단위)
|
||||
/// </summary>
|
||||
public float Rotation { get; set; } = 0.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 설명
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 생성 일자
|
||||
/// </summary>
|
||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 수정 일자
|
||||
/// </summary>
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 활성화 여부
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 로딩된 이미지 (런타임에서만 사용, JSON 직렬화 제외)
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
public Image LoadedImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public MapImage()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </summary>
|
||||
/// <param name="imageId">이미지 ID</param>
|
||||
/// <param name="imagePath">이미지 파일 경로</param>
|
||||
/// <param name="position">위치</param>
|
||||
public MapImage(string imageId, string imagePath, Point position)
|
||||
{
|
||||
ImageId = imageId;
|
||||
ImagePath = imagePath;
|
||||
Position = position;
|
||||
CreatedDate = DateTime.Now;
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 로드 (256x256 이상일 경우 자동 리사이즈)
|
||||
/// </summary>
|
||||
/// <returns>로드 성공 여부</returns>
|
||||
public bool LoadImage()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ImagePath) && System.IO.File.Exists(ImagePath))
|
||||
{
|
||||
LoadedImage?.Dispose();
|
||||
var originalImage = Image.FromFile(ImagePath);
|
||||
|
||||
// 이미지 크기 체크 및 리사이즈
|
||||
if (originalImage.Width > 256 || originalImage.Height > 256)
|
||||
{
|
||||
LoadedImage = ResizeImage(originalImage, 256, 256);
|
||||
originalImage.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadedImage = originalImage;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 이미지 로드 실패
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 리사이즈 (비율 유지)
|
||||
/// </summary>
|
||||
/// <param name="image">원본 이미지</param>
|
||||
/// <param name="maxWidth">최대 너비</param>
|
||||
/// <param name="maxHeight">최대 높이</param>
|
||||
/// <returns>리사이즈된 이미지</returns>
|
||||
private Image ResizeImage(Image image, int maxWidth, int maxHeight)
|
||||
{
|
||||
// 비율 계산
|
||||
double ratioX = (double)maxWidth / image.Width;
|
||||
double ratioY = (double)maxHeight / image.Height;
|
||||
double ratio = Math.Min(ratioX, ratioY);
|
||||
|
||||
// 새로운 크기 계산
|
||||
int newWidth = (int)(image.Width * ratio);
|
||||
int newHeight = (int)(image.Height * ratio);
|
||||
|
||||
// 리사이즈된 이미지 생성
|
||||
var resizedImage = new Bitmap(newWidth, newHeight);
|
||||
using (var graphics = Graphics.FromImage(resizedImage))
|
||||
{
|
||||
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
|
||||
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||||
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
||||
graphics.DrawImage(image, 0, 0, newWidth, newHeight);
|
||||
}
|
||||
|
||||
return resizedImage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실제 표시될 크기 계산
|
||||
/// </summary>
|
||||
/// <returns>실제 크기</returns>
|
||||
public Size GetDisplaySize()
|
||||
{
|
||||
if (LoadedImage == null) return Size.Empty;
|
||||
|
||||
return new Size(
|
||||
(int)(LoadedImage.Width * Scale.Width),
|
||||
(int)(LoadedImage.Height * Scale.Height)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{ImageId}: {System.IO.Path.GetFileName(ImagePath)} at ({Position.X}, {Position.Y})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 복사
|
||||
/// </summary>
|
||||
/// <returns>복사된 이미지</returns>
|
||||
public MapImage Clone()
|
||||
{
|
||||
var clone = new MapImage
|
||||
{
|
||||
ImageId = ImageId,
|
||||
ImagePath = ImagePath,
|
||||
Position = Position,
|
||||
Scale = Scale,
|
||||
Opacity = Opacity,
|
||||
Rotation = Rotation,
|
||||
Description = Description,
|
||||
CreatedDate = CreatedDate,
|
||||
ModifiedDate = ModifiedDate,
|
||||
IsActive = IsActive
|
||||
};
|
||||
|
||||
// 이미지는 복사하지 않음 (필요시 LoadImage() 호출)
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리소스 정리
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
LoadedImage?.Dispose();
|
||||
LoadedImage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
125
Cs_HMI/AGVMapEditor/Models/MapLabel.cs
Normal file
125
Cs_HMI/AGVMapEditor/Models/MapLabel.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 라벨 정보를 관리하는 클래스
|
||||
/// 디자인 요소용 텍스트 라벨
|
||||
/// </summary>
|
||||
public class MapLabel
|
||||
{
|
||||
/// <summary>
|
||||
/// 라벨 고유 ID
|
||||
/// </summary>
|
||||
public string LabelId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 텍스트
|
||||
/// </summary>
|
||||
public string Text { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 맵 상의 위치 좌표
|
||||
/// </summary>
|
||||
public Point Position { get; set; } = Point.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 폰트 정보
|
||||
/// </summary>
|
||||
public string FontFamily { get; set; } = "Arial";
|
||||
|
||||
/// <summary>
|
||||
/// 폰트 크기
|
||||
/// </summary>
|
||||
public float FontSize { get; set; } = 12;
|
||||
|
||||
/// <summary>
|
||||
/// 폰트 스타일 (Bold, Italic 등)
|
||||
/// </summary>
|
||||
public FontStyle FontStyle { get; set; } = FontStyle.Regular;
|
||||
|
||||
/// <summary>
|
||||
/// 글자 색상
|
||||
/// </summary>
|
||||
public Color ForeColor { get; set; } = Color.Black;
|
||||
|
||||
/// <summary>
|
||||
/// 배경 색상
|
||||
/// </summary>
|
||||
public Color BackColor { get; set; } = Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 배경 표시 여부
|
||||
/// </summary>
|
||||
public bool ShowBackground { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 생성 일자
|
||||
/// </summary>
|
||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 수정 일자
|
||||
/// </summary>
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 활성화 여부
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public MapLabel()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </summary>
|
||||
/// <param name="labelId">라벨 ID</param>
|
||||
/// <param name="text">라벨 텍스트</param>
|
||||
/// <param name="position">위치</param>
|
||||
public MapLabel(string labelId, string text, Point position)
|
||||
{
|
||||
LabelId = labelId;
|
||||
Text = text;
|
||||
Position = position;
|
||||
CreatedDate = DateTime.Now;
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{LabelId}: {Text} at ({Position.X}, {Position.Y})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 복사
|
||||
/// </summary>
|
||||
/// <returns>복사된 라벨</returns>
|
||||
public MapLabel Clone()
|
||||
{
|
||||
return new MapLabel
|
||||
{
|
||||
LabelId = LabelId,
|
||||
Text = Text,
|
||||
Position = Position,
|
||||
FontFamily = FontFamily,
|
||||
FontSize = FontSize,
|
||||
FontStyle = FontStyle,
|
||||
ForeColor = ForeColor,
|
||||
BackColor = BackColor,
|
||||
ShowBackground = ShowBackground,
|
||||
CreatedDate = CreatedDate,
|
||||
ModifiedDate = ModifiedDate,
|
||||
IsActive = IsActive
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,72 @@ namespace AGVMapEditor.Models
|
||||
/// </summary>
|
||||
public Color DisplayColor { get; set; } = Color.Blue;
|
||||
|
||||
/// <summary>
|
||||
/// RFID 태그 ID (이 노드에 매핑된 RFID)
|
||||
/// </summary>
|
||||
public string RfidId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 텍스트 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public string LabelText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 폰트 패밀리 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public string FontFamily { get; set; } = "Arial";
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 폰트 크기 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public float FontSize { get; set; } = 12.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 폰트 스타일 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public FontStyle FontStyle { get; set; } = FontStyle.Regular;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 전경색 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public Color ForeColor { get; set; } = Color.Black;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 배경색 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public Color BackColor { get; set; } = Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 배경 표시 여부 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public bool ShowBackground { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 파일 경로 (NodeType.Image인 경우 사용)
|
||||
/// </summary>
|
||||
public string ImagePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 크기 배율 (NodeType.Image인 경우 사용)
|
||||
/// </summary>
|
||||
public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f);
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 투명도 (NodeType.Image인 경우 사용, 0.0~1.0)
|
||||
/// </summary>
|
||||
public float Opacity { get; set; } = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 회전 각도 (NodeType.Image인 경우 사용, 도 단위)
|
||||
/// </summary>
|
||||
public float Rotation { get; set; } = 0.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 로딩된 이미지 (런타임에서만 사용, JSON 직렬화 제외)
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
public Image LoadedImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
@@ -130,6 +196,12 @@ namespace AGVMapEditor.Models
|
||||
case NodeType.Charging:
|
||||
DisplayColor = Color.Red;
|
||||
break;
|
||||
case NodeType.Label:
|
||||
DisplayColor = Color.Purple;
|
||||
break;
|
||||
case NodeType.Image:
|
||||
DisplayColor = Color.Brown;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +268,29 @@ namespace AGVMapEditor.Models
|
||||
return $"{NodeId}: {Name} ({Type}) at ({Position.X}, {Position.Y})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리스트박스 표시용 텍스트 (노드ID - 설명 - RFID 순서)
|
||||
/// </summary>
|
||||
public string DisplayText
|
||||
{
|
||||
get
|
||||
{
|
||||
var displayText = NodeId;
|
||||
|
||||
if (!string.IsNullOrEmpty(Description))
|
||||
{
|
||||
displayText += $" - {Description}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(RfidId))
|
||||
{
|
||||
displayText += $" - [{RfidId}]";
|
||||
}
|
||||
|
||||
return displayText;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 복사
|
||||
/// </summary>
|
||||
@@ -217,9 +312,111 @@ namespace AGVMapEditor.Models
|
||||
ModifiedDate = ModifiedDate,
|
||||
Description = Description,
|
||||
IsActive = IsActive,
|
||||
DisplayColor = DisplayColor
|
||||
DisplayColor = DisplayColor,
|
||||
RfidId = RfidId,
|
||||
LabelText = LabelText,
|
||||
FontFamily = FontFamily,
|
||||
FontSize = FontSize,
|
||||
FontStyle = FontStyle,
|
||||
ForeColor = ForeColor,
|
||||
BackColor = BackColor,
|
||||
ShowBackground = ShowBackground,
|
||||
ImagePath = ImagePath,
|
||||
Scale = Scale,
|
||||
Opacity = Opacity,
|
||||
Rotation = Rotation
|
||||
};
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 로드 (256x256 이상일 경우 자동 리사이즈)
|
||||
/// </summary>
|
||||
/// <returns>로드 성공 여부</returns>
|
||||
public bool LoadImage()
|
||||
{
|
||||
if (Type != NodeType.Image) return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ImagePath) && System.IO.File.Exists(ImagePath))
|
||||
{
|
||||
LoadedImage?.Dispose();
|
||||
var originalImage = Image.FromFile(ImagePath);
|
||||
|
||||
// 이미지 크기 체크 및 리사이즈
|
||||
if (originalImage.Width > 256 || originalImage.Height > 256)
|
||||
{
|
||||
LoadedImage = ResizeImage(originalImage, 256, 256);
|
||||
originalImage.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadedImage = originalImage;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 이미지 로드 실패
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 리사이즈 (비율 유지)
|
||||
/// </summary>
|
||||
/// <param name="image">원본 이미지</param>
|
||||
/// <param name="maxWidth">최대 너비</param>
|
||||
/// <param name="maxHeight">최대 높이</param>
|
||||
/// <returns>리사이즈된 이미지</returns>
|
||||
private Image ResizeImage(Image image, int maxWidth, int maxHeight)
|
||||
{
|
||||
// 비율 계산
|
||||
double ratioX = (double)maxWidth / image.Width;
|
||||
double ratioY = (double)maxHeight / image.Height;
|
||||
double ratio = Math.Min(ratioX, ratioY);
|
||||
|
||||
// 새로운 크기 계산
|
||||
int newWidth = (int)(image.Width * ratio);
|
||||
int newHeight = (int)(image.Height * ratio);
|
||||
|
||||
// 리사이즈된 이미지 생성
|
||||
var resizedImage = new Bitmap(newWidth, newHeight);
|
||||
using (var graphics = Graphics.FromImage(resizedImage))
|
||||
{
|
||||
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
|
||||
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||||
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
||||
graphics.DrawImage(image, 0, 0, newWidth, newHeight);
|
||||
}
|
||||
|
||||
return resizedImage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실제 표시될 크기 계산 (이미지 노드인 경우)
|
||||
/// </summary>
|
||||
/// <returns>실제 크기</returns>
|
||||
public Size GetDisplaySize()
|
||||
{
|
||||
if (Type != NodeType.Image || LoadedImage == null) return Size.Empty;
|
||||
|
||||
return new Size(
|
||||
(int)(LoadedImage.Width * Scale.Width),
|
||||
(int)(LoadedImage.Height * Scale.Height)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리소스 정리
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
LoadedImage?.Dispose();
|
||||
LoadedImage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
499
Cs_HMI/AGVMapEditor/Models/NodePropertyWrapper.cs
Normal file
499
Cs_HMI/AGVMapEditor/Models/NodePropertyWrapper.cs
Normal file
@@ -0,0 +1,499 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 노드 타입에 따른 PropertyWrapper 팩토리
|
||||
/// </summary>
|
||||
public static class NodePropertyWrapperFactory
|
||||
{
|
||||
public static object CreateWrapper(MapNode node, List<MapNode> mapNodes)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case NodeType.Label:
|
||||
return new LabelNodePropertyWrapper(node, mapNodes);
|
||||
case NodeType.Image:
|
||||
return new ImageNodePropertyWrapper(node, mapNodes);
|
||||
default:
|
||||
return new NodePropertyWrapper(node, mapNodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 노드 전용 PropertyWrapper
|
||||
/// </summary>
|
||||
public class LabelNodePropertyWrapper
|
||||
{
|
||||
private MapNode _node;
|
||||
private List<MapNode> _mapNodes;
|
||||
|
||||
public LabelNodePropertyWrapper(MapNode node, List<MapNode> mapNodes)
|
||||
{
|
||||
_node = node;
|
||||
_mapNodes = mapNodes;
|
||||
}
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 ID")]
|
||||
[Description("노드의 고유 식별자")]
|
||||
[ReadOnly(true)]
|
||||
public string NodeId
|
||||
{
|
||||
get => _node.NodeId;
|
||||
}
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 타입")]
|
||||
[Description("노드의 타입")]
|
||||
[ReadOnly(true)]
|
||||
public NodeType Type
|
||||
{
|
||||
get => _node.Type;
|
||||
}
|
||||
|
||||
[Category("라벨")]
|
||||
[DisplayName("텍스트")]
|
||||
[Description("표시할 텍스트")]
|
||||
public string LabelText
|
||||
{
|
||||
get => _node.LabelText;
|
||||
set
|
||||
{
|
||||
_node.LabelText = value ?? "";
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("라벨")]
|
||||
[DisplayName("폰트 패밀리")]
|
||||
[Description("폰트 패밀리")]
|
||||
public string FontFamily
|
||||
{
|
||||
get => _node.FontFamily;
|
||||
set
|
||||
{
|
||||
_node.FontFamily = value ?? "Arial";
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("라벨")]
|
||||
[DisplayName("폰트 크기")]
|
||||
[Description("폰트 크기")]
|
||||
public float FontSize
|
||||
{
|
||||
get => _node.FontSize;
|
||||
set
|
||||
{
|
||||
_node.FontSize = Math.Max(6, Math.Min(72, value));
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("라벨")]
|
||||
[DisplayName("폰트 스타일")]
|
||||
[Description("폰트 스타일")]
|
||||
public FontStyle FontStyle
|
||||
{
|
||||
get => _node.FontStyle;
|
||||
set
|
||||
{
|
||||
_node.FontStyle = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("라벨")]
|
||||
[DisplayName("전경색")]
|
||||
[Description("텍스트 색상")]
|
||||
public Color ForeColor
|
||||
{
|
||||
get => _node.ForeColor;
|
||||
set
|
||||
{
|
||||
_node.ForeColor = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("라벨")]
|
||||
[DisplayName("배경색")]
|
||||
[Description("배경 색상")]
|
||||
public Color BackColor
|
||||
{
|
||||
get => _node.BackColor;
|
||||
set
|
||||
{
|
||||
_node.BackColor = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("라벨")]
|
||||
[DisplayName("배경 표시")]
|
||||
[Description("배경을 표시할지 여부")]
|
||||
public bool ShowBackground
|
||||
{
|
||||
get => _node.ShowBackground;
|
||||
set
|
||||
{
|
||||
_node.ShowBackground = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("위치")]
|
||||
[DisplayName("X 좌표")]
|
||||
[Description("맵에서의 X 좌표")]
|
||||
public int PositionX
|
||||
{
|
||||
get => _node.Position.X;
|
||||
set
|
||||
{
|
||||
_node.Position = new Point(value, _node.Position.Y);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("위치")]
|
||||
[DisplayName("Y 좌표")]
|
||||
[Description("맵에서의 Y 좌표")]
|
||||
public int PositionY
|
||||
{
|
||||
get => _node.Position.Y;
|
||||
set
|
||||
{
|
||||
_node.Position = new Point(_node.Position.X, value);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("정보")]
|
||||
[DisplayName("생성 일시")]
|
||||
[Description("노드가 생성된 일시")]
|
||||
[ReadOnly(true)]
|
||||
public DateTime CreatedDate
|
||||
{
|
||||
get => _node.CreatedDate;
|
||||
}
|
||||
|
||||
[Category("정보")]
|
||||
[DisplayName("수정 일시")]
|
||||
[Description("노드가 마지막으로 수정된 일시")]
|
||||
[ReadOnly(true)]
|
||||
public DateTime ModifiedDate
|
||||
{
|
||||
get => _node.ModifiedDate;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 노드 전용 PropertyWrapper
|
||||
/// </summary>
|
||||
public class ImageNodePropertyWrapper
|
||||
{
|
||||
private MapNode _node;
|
||||
private List<MapNode> _mapNodes;
|
||||
|
||||
public ImageNodePropertyWrapper(MapNode node, List<MapNode> mapNodes)
|
||||
{
|
||||
_node = node;
|
||||
_mapNodes = mapNodes;
|
||||
}
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 ID")]
|
||||
[Description("노드의 고유 식별자")]
|
||||
[ReadOnly(true)]
|
||||
public string NodeId
|
||||
{
|
||||
get => _node.NodeId;
|
||||
}
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 타입")]
|
||||
[Description("노드의 타입")]
|
||||
[ReadOnly(true)]
|
||||
public NodeType Type
|
||||
{
|
||||
get => _node.Type;
|
||||
}
|
||||
|
||||
[Category("이미지")]
|
||||
[DisplayName("이미지 경로")]
|
||||
[Description("이미지 파일 경로")]
|
||||
// 파일 선택 에디터는 나중에 구현
|
||||
public string ImagePath
|
||||
{
|
||||
get => _node.ImagePath;
|
||||
set
|
||||
{
|
||||
_node.ImagePath = value ?? "";
|
||||
_node.LoadImage(); // 이미지 다시 로드
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("이미지")]
|
||||
[DisplayName("가로 배율")]
|
||||
[Description("가로 배율 (1.0 = 원본 크기)")]
|
||||
public float ScaleWidth
|
||||
{
|
||||
get => _node.Scale.Width;
|
||||
set
|
||||
{
|
||||
_node.Scale = new SizeF(Math.Max(0.1f, Math.Min(5.0f, value)), _node.Scale.Height);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("이미지")]
|
||||
[DisplayName("세로 배율")]
|
||||
[Description("세로 배율 (1.0 = 원본 크기)")]
|
||||
public float ScaleHeight
|
||||
{
|
||||
get => _node.Scale.Height;
|
||||
set
|
||||
{
|
||||
_node.Scale = new SizeF(_node.Scale.Width, Math.Max(0.1f, Math.Min(5.0f, value)));
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("이미지")]
|
||||
[DisplayName("투명도")]
|
||||
[Description("투명도 (0.0 = 투명, 1.0 = 불투명)")]
|
||||
public float Opacity
|
||||
{
|
||||
get => _node.Opacity;
|
||||
set
|
||||
{
|
||||
_node.Opacity = Math.Max(0.0f, Math.Min(1.0f, value));
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("이미지")]
|
||||
[DisplayName("회전각도")]
|
||||
[Description("회전 각도 (도 단위)")]
|
||||
public float Rotation
|
||||
{
|
||||
get => _node.Rotation;
|
||||
set
|
||||
{
|
||||
_node.Rotation = value % 360;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("위치")]
|
||||
[DisplayName("X 좌표")]
|
||||
[Description("맵에서의 X 좌표")]
|
||||
public int PositionX
|
||||
{
|
||||
get => _node.Position.X;
|
||||
set
|
||||
{
|
||||
_node.Position = new Point(value, _node.Position.Y);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("위치")]
|
||||
[DisplayName("Y 좌표")]
|
||||
[Description("맵에서의 Y 좌표")]
|
||||
public int PositionY
|
||||
{
|
||||
get => _node.Position.Y;
|
||||
set
|
||||
{
|
||||
_node.Position = new Point(_node.Position.X, value);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("정보")]
|
||||
[DisplayName("생성 일시")]
|
||||
[Description("노드가 생성된 일시")]
|
||||
[ReadOnly(true)]
|
||||
public DateTime CreatedDate
|
||||
{
|
||||
get => _node.CreatedDate;
|
||||
}
|
||||
|
||||
[Category("정보")]
|
||||
[DisplayName("수정 일시")]
|
||||
[Description("노드가 마지막으로 수정된 일시")]
|
||||
[ReadOnly(true)]
|
||||
public DateTime ModifiedDate
|
||||
{
|
||||
get => _node.ModifiedDate;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// PropertyGrid에서 사용할 노드 속성 래퍼 클래스
|
||||
/// </summary>
|
||||
public class NodePropertyWrapper
|
||||
{
|
||||
private MapNode _node;
|
||||
private List<MapNode> _mapNodes;
|
||||
|
||||
public NodePropertyWrapper(MapNode node, List<MapNode> mapNodes)
|
||||
{
|
||||
_node = node;
|
||||
_mapNodes = mapNodes;
|
||||
}
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 ID")]
|
||||
[Description("노드의 고유 식별자")]
|
||||
[ReadOnly(true)]
|
||||
public string NodeId
|
||||
{
|
||||
get => _node.NodeId;
|
||||
set => _node.NodeId = value;
|
||||
}
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 이름")]
|
||||
[Description("노드의 표시 이름")]
|
||||
public string Name
|
||||
{
|
||||
get => _node.Name;
|
||||
set
|
||||
{
|
||||
_node.Name = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 타입")]
|
||||
[Description("노드의 타입 (Normal, Rotation, Docking, Charging)")]
|
||||
public NodeType Type
|
||||
{
|
||||
get => _node.Type;
|
||||
set
|
||||
{
|
||||
_node.Type = value;
|
||||
_node.CanRotate = value == NodeType.Rotation;
|
||||
_node.SetDefaultColorByType(value);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[Category("위치")]
|
||||
[DisplayName("X 좌표")]
|
||||
[Description("맵에서의 X 좌표")]
|
||||
public int PositionX
|
||||
{
|
||||
get => _node.Position.X;
|
||||
set
|
||||
{
|
||||
_node.Position = new Point(value, _node.Position.Y);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("위치")]
|
||||
[DisplayName("Y 좌표")]
|
||||
[Description("맵에서의 Y 좌표")]
|
||||
public int PositionY
|
||||
{
|
||||
get => _node.Position.Y;
|
||||
set
|
||||
{
|
||||
_node.Position = new Point(_node.Position.X, value);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Category("고급")]
|
||||
[DisplayName("회전 가능")]
|
||||
[Description("이 노드에서 AGV가 회전할 수 있는지 여부")]
|
||||
public bool CanRotate
|
||||
{
|
||||
get => _node.CanRotate;
|
||||
set
|
||||
{
|
||||
_node.CanRotate = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("고급")]
|
||||
[DisplayName("RFID")]
|
||||
[Description("RFID ID")]
|
||||
public string RFID
|
||||
{
|
||||
get => _node.RfidId;
|
||||
set
|
||||
{
|
||||
_node.RfidId = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Category("고급")]
|
||||
[DisplayName("설명")]
|
||||
[Description("노드에 대한 추가 설명")]
|
||||
public string Description
|
||||
{
|
||||
get => _node.Description;
|
||||
set
|
||||
{
|
||||
_node.Description = value ?? "";
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("고급")]
|
||||
[DisplayName("활성화")]
|
||||
[Description("노드 활성화 여부")]
|
||||
public bool IsActive
|
||||
{
|
||||
get => _node.IsActive;
|
||||
set
|
||||
{
|
||||
_node.IsActive = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("정보")]
|
||||
[DisplayName("생성 일시")]
|
||||
[Description("노드가 생성된 일시")]
|
||||
[ReadOnly(true)]
|
||||
public DateTime CreatedDate
|
||||
{
|
||||
get => _node.CreatedDate;
|
||||
}
|
||||
|
||||
[Category("정보")]
|
||||
[DisplayName("수정 일시")]
|
||||
[Description("노드가 마지막으로 수정된 일시")]
|
||||
[ReadOnly(true)]
|
||||
public DateTime ModifiedDate
|
||||
{
|
||||
get => _node.ModifiedDate;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
|
||||
@@ -3,467 +3,266 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 전용 경로 계산기 (A* 알고리즘 기반)
|
||||
/// AGV의 방향성, 도킹 제약, 회전 제약을 고려한 경로 계산
|
||||
/// AGV 전용 경로 계산기 (AGVNavigationCore 래퍼)
|
||||
/// AGVMapEditor와 AGVNavigationCore 간의 호환성 제공
|
||||
/// RFID 기반 경로 계산을 우선 사용
|
||||
/// </summary>
|
||||
public class PathCalculator
|
||||
{
|
||||
#region Constants
|
||||
|
||||
private const float BASE_MOVE_COST = 1.0f; // 기본 이동 비용
|
||||
private const float ROTATION_COST = 0.5f; // 회전 비용
|
||||
private const float DOCKING_APPROACH_COST = 0.2f; // 도킹 접근 추가 비용
|
||||
private const float HEURISTIC_WEIGHT = 1.0f; // 휴리스틱 가중치
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
private List<MapNode> _mapNodes;
|
||||
private NodeResolver _nodeResolver;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
private AGVPathfinder _agvPathfinder;
|
||||
private AStarPathfinder _astarPathfinder;
|
||||
private RfidBasedPathfinder _rfidPathfinder;
|
||||
|
||||
/// <summary>
|
||||
/// 생성자
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
/// <param name="nodeResolver">노드 해결기</param>
|
||||
public PathCalculator(List<MapNode> mapNodes, NodeResolver nodeResolver)
|
||||
public PathCalculator()
|
||||
{
|
||||
_mapNodes = mapNodes ?? throw new ArgumentNullException(nameof(mapNodes));
|
||||
_nodeResolver = nodeResolver ?? throw new ArgumentNullException(nameof(nodeResolver));
|
||||
_agvPathfinder = new AGVPathfinder();
|
||||
_astarPathfinder = new AStarPathfinder();
|
||||
_rfidPathfinder = new RfidBasedPathfinder();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// 맵 노드 설정
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
public void SetMapNodes(List<MapNode> mapNodes)
|
||||
{
|
||||
_agvPathfinder.SetMapNodes(mapNodes);
|
||||
_astarPathfinder.SetMapNodes(mapNodes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 계산 (메인 메서드)
|
||||
/// 맵 데이터 설정
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
public void SetMapData(List<MapNode> mapNodes)
|
||||
{
|
||||
_agvPathfinder.SetMapNodes(mapNodes);
|
||||
_astarPathfinder.SetMapNodes(mapNodes);
|
||||
// RfidPathfinder는 MapNode의 RFID 정보를 직접 사용
|
||||
_rfidPathfinder.SetMapNodes(mapNodes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 경로 계산
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <param name="targetNodeId">목표 노드 ID</param>
|
||||
/// <param name="currentDirection">현재 AGV 방향</param>
|
||||
/// <param name="endNodeId">목적지 노드 ID</param>
|
||||
/// <param name="targetDirection">목적지 도착 방향</param>
|
||||
/// <returns>AGV 경로 계산 결과</returns>
|
||||
public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection = null)
|
||||
{
|
||||
return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 충전 스테이션으로의 경로 찾기
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <returns>AGV 경로 계산 결과</returns>
|
||||
public AGVPathResult FindPathToChargingStation(string startNodeId)
|
||||
{
|
||||
return _agvPathfinder.FindPathToChargingStation(startNodeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 스테이션으로의 경로 찾기
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <param name="stationType">장비 타입</param>
|
||||
/// <returns>AGV 경로 계산 결과</returns>
|
||||
public AGVPathResult FindPathToDockingStation(string startNodeId, StationType stationType)
|
||||
{
|
||||
return _agvPathfinder.FindPathToDockingStation(startNodeId, stationType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 여러 목적지 중 가장 가까운 노드로의 경로 찾기
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <param name="targetNodeIds">목적지 후보 노드 ID 목록</param>
|
||||
/// <returns>경로 계산 결과</returns>
|
||||
public PathResult CalculatePath(string startNodeId, string targetNodeId, AgvDirection currentDirection)
|
||||
public PathResult FindNearestPath(string startNodeId, List<string> targetNodeIds)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// 입력 검증
|
||||
var validationResult = ValidateInput(startNodeId, targetNodeId, currentDirection);
|
||||
if (!validationResult.Success)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
validationResult.CalculationTime = stopwatch.ElapsedMilliseconds;
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
// A* 알고리즘 실행
|
||||
var result = ExecuteAStar(startNodeId, targetNodeId, currentDirection);
|
||||
|
||||
stopwatch.Stop();
|
||||
result.CalculationTime = stopwatch.ElapsedMilliseconds;
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
return new PathResult($"경로 계산 중 오류 발생: {ex.Message}")
|
||||
{
|
||||
CalculationTime = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
}
|
||||
return _astarPathfinder.FindNearestPath(startNodeId, targetNodeIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 유효성 검증 (RFID 이탈 감지시 사용)
|
||||
/// 두 노드가 연결되어 있는지 확인
|
||||
/// </summary>
|
||||
/// <param name="currentPath">현재 경로</param>
|
||||
/// <param name="currentRfidId">현재 감지된 RFID</param>
|
||||
/// <returns>경로 유효성 여부</returns>
|
||||
public bool ValidateCurrentPath(PathResult currentPath, string currentRfidId)
|
||||
/// <param name="nodeId1">노드 1 ID</param>
|
||||
/// <param name="nodeId2">노드 2 ID</param>
|
||||
/// <returns>연결 여부</returns>
|
||||
public bool AreNodesConnected(string nodeId1, string nodeId2)
|
||||
{
|
||||
if (currentPath == null || !currentPath.Success)
|
||||
return false;
|
||||
|
||||
var currentNode = _nodeResolver.GetNodeByRfid(currentRfidId);
|
||||
if (currentNode == null)
|
||||
return false;
|
||||
|
||||
// 현재 노드가 계획된 경로에 포함되어 있는지 확인
|
||||
return currentPath.NodeSequence.Contains(currentNode.NodeId);
|
||||
return _astarPathfinder.AreNodesConnected(nodeId1, nodeId2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 동적 경로 재계산 (경로 이탈시 사용)
|
||||
/// 경로 유효성 검증
|
||||
/// </summary>
|
||||
/// <param name="currentRfidId">현재 RFID 위치</param>
|
||||
/// <param name="targetNodeId">목표 노드 ID</param>
|
||||
/// <param name="currentDirection">현재 방향</param>
|
||||
/// <param name="originalPath">원래 경로 (참고용)</param>
|
||||
/// <returns>새로운 경로</returns>
|
||||
public PathResult RecalculatePath(string currentRfidId, string targetNodeId,
|
||||
AgvDirection currentDirection, PathResult originalPath = null)
|
||||
/// <param name="path">검증할 경로</param>
|
||||
/// <returns>유효성 검증 결과</returns>
|
||||
public bool ValidatePath(List<string> path)
|
||||
{
|
||||
var currentNode = _nodeResolver.GetNodeByRfid(currentRfidId);
|
||||
if (currentNode == null)
|
||||
{
|
||||
return new PathResult("현재 위치를 확인할 수 없습니다.");
|
||||
}
|
||||
|
||||
// 새로운 경로 계산
|
||||
var result = CalculatePath(currentNode.NodeId, targetNodeId, currentDirection);
|
||||
|
||||
// 원래 경로와 비교 (로그용)
|
||||
if (originalPath != null && result.Success)
|
||||
{
|
||||
// TODO: 경로 변경 로그 기록
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods - Input Validation
|
||||
|
||||
/// <summary>
|
||||
/// 입력 값 검증
|
||||
/// </summary>
|
||||
private PathResult ValidateInput(string startNodeId, string targetNodeId, AgvDirection currentDirection)
|
||||
{
|
||||
if (string.IsNullOrEmpty(startNodeId))
|
||||
return new PathResult("시작 노드가 지정되지 않았습니다.");
|
||||
|
||||
if (string.IsNullOrEmpty(targetNodeId))
|
||||
return new PathResult("목표 노드가 지정되지 않았습니다.");
|
||||
|
||||
var startNode = _mapNodes.FirstOrDefault(n => n.NodeId == startNodeId);
|
||||
if (startNode == null)
|
||||
return new PathResult($"시작 노드를 찾을 수 없습니다: {startNodeId}");
|
||||
|
||||
var targetNode = _mapNodes.FirstOrDefault(n => n.NodeId == targetNodeId);
|
||||
if (targetNode == null)
|
||||
return new PathResult($"목표 노드를 찾을 수 없습니다: {targetNodeId}");
|
||||
|
||||
if (startNodeId == targetNodeId)
|
||||
return new PathResult("시작점과 목표점이 동일합니다.");
|
||||
|
||||
return new PathResult { Success = true };
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods - A* Algorithm
|
||||
|
||||
/// <summary>
|
||||
/// A* 알고리즘 실행
|
||||
/// </summary>
|
||||
private PathResult ExecuteAStar(string startNodeId, string targetNodeId, AgvDirection currentDirection)
|
||||
{
|
||||
var openSet = new SortedSet<PathNode>();
|
||||
var closedSet = new HashSet<string>();
|
||||
var gScore = new Dictionary<string, float>();
|
||||
|
||||
// 시작 노드 설정
|
||||
var startPathNode = new PathNode(startNodeId, currentDirection)
|
||||
{
|
||||
GCost = 0,
|
||||
HCost = CalculateHeuristic(startNodeId, targetNodeId)
|
||||
};
|
||||
|
||||
openSet.Add(startPathNode);
|
||||
gScore[startPathNode.GetKey()] = 0;
|
||||
|
||||
while (openSet.Count > 0)
|
||||
{
|
||||
// 가장 낮은 F 비용을 가진 노드 선택
|
||||
var current = openSet.Min;
|
||||
openSet.Remove(current);
|
||||
|
||||
// 목표 도달 확인
|
||||
if (current.NodeId == targetNodeId)
|
||||
{
|
||||
// 도킹 방향 검증
|
||||
if (IsValidDockingApproach(targetNodeId, current.Direction))
|
||||
{
|
||||
return ReconstructPath(current, startNodeId, targetNodeId, currentDirection);
|
||||
}
|
||||
// 도킹 방향이 맞지 않으면 계속 탐색
|
||||
}
|
||||
|
||||
closedSet.Add(current.GetKey());
|
||||
|
||||
// 인접 노드들 처리
|
||||
ProcessNeighbors(current, targetNodeId, openSet, closedSet, gScore);
|
||||
}
|
||||
|
||||
return new PathResult("경로를 찾을 수 없습니다.");
|
||||
return _agvPathfinder.ValidatePath(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 인접 노드들 처리
|
||||
/// 네비게이션 가능한 노드 목록 반환
|
||||
/// </summary>
|
||||
private void ProcessNeighbors(PathNode current, string targetNodeId,
|
||||
SortedSet<PathNode> openSet, HashSet<string> closedSet,
|
||||
Dictionary<string, float> gScore)
|
||||
/// <returns>노드 ID 목록</returns>
|
||||
public List<string> GetNavigationNodes()
|
||||
{
|
||||
var currentMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == current.NodeId);
|
||||
if (currentMapNode == null) return;
|
||||
|
||||
foreach (var neighborId in currentMapNode.ConnectedNodes)
|
||||
{
|
||||
var neighborMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == neighborId);
|
||||
if (neighborMapNode == null) continue;
|
||||
|
||||
// 가능한 모든 방향으로 이웃 노드 방문
|
||||
foreach (var direction in GetPossibleDirections(current, neighborMapNode))
|
||||
{
|
||||
var neighborPathNode = new PathNode(neighborId, direction);
|
||||
var neighborKey = neighborPathNode.GetKey();
|
||||
|
||||
if (closedSet.Contains(neighborKey))
|
||||
continue;
|
||||
|
||||
// 이동 비용 계산
|
||||
var moveCost = CalculateMoveCost(current, neighborPathNode, neighborMapNode);
|
||||
var tentativeGScore = current.GCost + moveCost;
|
||||
|
||||
// 더 좋은 경로인지 확인
|
||||
if (!gScore.ContainsKey(neighborKey) || tentativeGScore < gScore[neighborKey])
|
||||
{
|
||||
// 경로 정보 업데이트
|
||||
neighborPathNode.Parent = current;
|
||||
neighborPathNode.GCost = tentativeGScore;
|
||||
neighborPathNode.HCost = CalculateHeuristic(neighborId, targetNodeId);
|
||||
neighborPathNode.RotationCount = current.RotationCount +
|
||||
(current.Direction != direction ? 1 : 0);
|
||||
|
||||
// 이동 명령 시퀀스 구성
|
||||
neighborPathNode.MovementSequence = GenerateMovementSequence(current, neighborPathNode);
|
||||
|
||||
gScore[neighborKey] = tentativeGScore;
|
||||
|
||||
// openSet에 추가 (중복 제거)
|
||||
openSet.RemoveWhere(n => n.GetKey() == neighborKey);
|
||||
openSet.Add(neighborPathNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _astarPathfinder.GetNavigationNodes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 가능한 방향들 계산
|
||||
/// AGV 현재 방향 설정
|
||||
/// </summary>
|
||||
private List<AgvDirection> GetPossibleDirections(PathNode current, MapNode neighborNode)
|
||||
/// <param name="direction">현재 방향</param>
|
||||
public void SetCurrentDirection(AgvDirection direction)
|
||||
{
|
||||
var directions = new List<AgvDirection>();
|
||||
|
||||
// 기본적으로 전진/후진 가능
|
||||
directions.Add(AgvDirection.Forward);
|
||||
directions.Add(AgvDirection.Backward);
|
||||
|
||||
// 회전 가능한 노드에서만 방향 전환 가능
|
||||
if (CanRotateAt(current.NodeId))
|
||||
{
|
||||
// 현재 방향과 다른 방향도 고려
|
||||
if (current.Direction == AgvDirection.Forward)
|
||||
directions.Add(AgvDirection.Backward);
|
||||
else if (current.Direction == AgvDirection.Backward)
|
||||
directions.Add(AgvDirection.Forward);
|
||||
}
|
||||
|
||||
return directions;
|
||||
_agvPathfinder.CurrentDirection = direction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이동 비용 계산
|
||||
/// 회전 비용 가중치 설정
|
||||
/// </summary>
|
||||
private float CalculateMoveCost(PathNode from, PathNode to, MapNode toMapNode)
|
||||
/// <param name="weight">회전 비용 가중치</param>
|
||||
public void SetRotationCostWeight(float weight)
|
||||
{
|
||||
float cost = BASE_MOVE_COST;
|
||||
|
||||
// 방향 전환 비용
|
||||
if (from.Direction != to.Direction)
|
||||
{
|
||||
cost += ROTATION_COST;
|
||||
}
|
||||
|
||||
// 도킹 스테이션 접근 비용
|
||||
if (toMapNode.Type == NodeType.Docking || toMapNode.Type == NodeType.Charging)
|
||||
{
|
||||
cost += DOCKING_APPROACH_COST;
|
||||
}
|
||||
|
||||
// 실제 거리 기반 비용 (좌표가 있는 경우)
|
||||
var fromMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == from.NodeId);
|
||||
if (fromMapNode != null && toMapNode != null)
|
||||
{
|
||||
var distance = CalculateDistance(fromMapNode.Position, toMapNode.Position);
|
||||
cost *= (distance / 100.0f); // 좌표 단위를 거리 단위로 조정
|
||||
}
|
||||
|
||||
return cost;
|
||||
_agvPathfinder.RotationCostWeight = weight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 휴리스틱 함수 (목표까지의 추정 거리)
|
||||
/// 휴리스틱 가중치 설정
|
||||
/// </summary>
|
||||
private float CalculateHeuristic(string fromNodeId, string toNodeId)
|
||||
/// <param name="weight">휴리스틱 가중치</param>
|
||||
public void SetHeuristicWeight(float weight)
|
||||
{
|
||||
var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == fromNodeId);
|
||||
var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId);
|
||||
|
||||
if (fromNode == null || toNode == null)
|
||||
return 0;
|
||||
|
||||
// 유클리드 거리 계산
|
||||
var distance = CalculateDistance(fromNode.Position, toNode.Position);
|
||||
return distance * HEURISTIC_WEIGHT / 100.0f; // 좌표 단위 조정
|
||||
_astarPathfinder.HeuristicWeight = weight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 두 점 사이의 거리 계산
|
||||
/// 최대 탐색 노드 수 설정
|
||||
/// </summary>
|
||||
private float CalculateDistance(Point from, Point to)
|
||||
/// <param name="maxNodes">최대 탐색 노드 수</param>
|
||||
public void SetMaxSearchNodes(int maxNodes)
|
||||
{
|
||||
var dx = to.X - from.X;
|
||||
var dy = to.Y - from.Y;
|
||||
return (float)Math.Sqrt(dx * dx + dy * dy);
|
||||
_astarPathfinder.MaxSearchNodes = maxNodes;
|
||||
}
|
||||
|
||||
// ==================== RFID 기반 경로 계산 메서드들 ====================
|
||||
|
||||
/// <summary>
|
||||
/// RFID 기반 AGV 경로 계산
|
||||
/// </summary>
|
||||
/// <param name="startRfidId">시작 RFID</param>
|
||||
/// <param name="endRfidId">목적지 RFID</param>
|
||||
/// <param name="targetDirection">목적지 도착 방향</param>
|
||||
/// <returns>RFID 기반 AGV 경로 계산 결과</returns>
|
||||
public RfidPathResult FindAGVPathByRfid(string startRfidId, string endRfidId, AgvDirection? targetDirection = null)
|
||||
{
|
||||
return _rfidPathfinder.FindAGVPath(startRfidId, endRfidId, targetDirection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 회전 가능한 위치인지 확인
|
||||
/// RFID 기반 충전소 경로 찾기
|
||||
/// </summary>
|
||||
private bool CanRotateAt(string nodeId)
|
||||
/// <param name="startRfidId">시작 RFID</param>
|
||||
/// <returns>RFID 기반 경로 계산 결과</returns>
|
||||
public RfidPathResult FindPathToChargingStationByRfid(string startRfidId)
|
||||
{
|
||||
var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId);
|
||||
return node != null && (node.CanRotate || node.Type == NodeType.Rotation);
|
||||
return _rfidPathfinder.FindPathToChargingStation(startRfidId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 접근 방향이 유효한지 확인
|
||||
/// RFID 기반 도킹 스테이션 경로 찾기
|
||||
/// </summary>
|
||||
private bool IsValidDockingApproach(string nodeId, AgvDirection approachDirection)
|
||||
/// <param name="startRfidId">시작 RFID</param>
|
||||
/// <param name="stationType">장비 타입</param>
|
||||
/// <returns>RFID 기반 경로 계산 결과</returns>
|
||||
public RfidPathResult FindPathToDockingStationByRfid(string startRfidId, StationType stationType)
|
||||
{
|
||||
var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId);
|
||||
if (node == null) return true;
|
||||
|
||||
// 도킹/충전 스테이션이 아니면 방향 제약 없음
|
||||
if (node.Type != NodeType.Docking && node.Type != NodeType.Charging)
|
||||
return true;
|
||||
|
||||
// 도킹 방향 확인
|
||||
if (node.DockDirection == null)
|
||||
return true;
|
||||
|
||||
// 충전기는 전진으로만, 다른 장비는 후진으로만
|
||||
if (node.Type == NodeType.Charging)
|
||||
return approachDirection == AgvDirection.Forward;
|
||||
else
|
||||
return approachDirection == AgvDirection.Backward;
|
||||
return _rfidPathfinder.FindPathToDockingStation(startRfidId, stationType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이동 명령 시퀀스 생성
|
||||
/// 여러 RFID 목적지 중 가장 가까운 곳으로의 경로 찾기
|
||||
/// </summary>
|
||||
private List<AgvDirection> GenerateMovementSequence(PathNode from, PathNode to)
|
||||
/// <param name="startRfidId">시작 RFID</param>
|
||||
/// <param name="targetRfidIds">목적지 후보 RFID 목록</param>
|
||||
/// <returns>RFID 기반 경로 계산 결과</returns>
|
||||
public RfidPathResult FindNearestPathByRfid(string startRfidId, List<string> targetRfidIds)
|
||||
{
|
||||
var sequence = new List<AgvDirection>();
|
||||
|
||||
// 방향 전환이 필요한 경우
|
||||
if (from.Direction != to.Direction)
|
||||
{
|
||||
if (from.Direction == AgvDirection.Forward && to.Direction == AgvDirection.Backward)
|
||||
{
|
||||
sequence.Add(AgvDirection.Right); // 180도 회전 (마크센서까지)
|
||||
}
|
||||
else if (from.Direction == AgvDirection.Backward && to.Direction == AgvDirection.Forward)
|
||||
{
|
||||
sequence.Add(AgvDirection.Right); // 180도 회전 (마크센서까지)
|
||||
}
|
||||
}
|
||||
|
||||
// 이동 명령
|
||||
sequence.Add(to.Direction);
|
||||
|
||||
return sequence;
|
||||
return _rfidPathfinder.FindNearestPath(startRfidId, targetRfidIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 재구성
|
||||
/// RFID 매핑 정보 조회 (MapNode 반환)
|
||||
/// </summary>
|
||||
private PathResult ReconstructPath(PathNode goalNode, string startNodeId, string targetNodeId, AgvDirection startDirection)
|
||||
/// <param name="rfidId">RFID</param>
|
||||
/// <returns>MapNode 또는 null</returns>
|
||||
public MapNode GetRfidMapping(string rfidId)
|
||||
{
|
||||
var path = new List<PathNode>();
|
||||
var current = goalNode;
|
||||
|
||||
// 역순으로 경로 구성
|
||||
while (current != null)
|
||||
{
|
||||
path.Insert(0, current);
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
return new PathResult(path, startNodeId, targetNodeId, startDirection);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Utility Methods
|
||||
|
||||
/// <summary>
|
||||
/// 노드 간 직선 거리 계산
|
||||
/// </summary>
|
||||
public float GetDistance(string fromNodeId, string toNodeId)
|
||||
{
|
||||
var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == fromNodeId);
|
||||
var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId);
|
||||
|
||||
if (fromNode == null || toNode == null)
|
||||
return float.MaxValue;
|
||||
|
||||
return CalculateDistance(fromNode.Position, toNode.Position);
|
||||
return _rfidPathfinder.GetRfidMapping(rfidId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 노드에서 가능한 다음 노드들 조회
|
||||
/// RFID로 NodeId 조회
|
||||
/// </summary>
|
||||
public List<string> GetPossibleNextNodes(string currentNodeId, AgvDirection currentDirection)
|
||||
/// <param name="rfidId">RFID</param>
|
||||
/// <returns>NodeId 또는 null</returns>
|
||||
public string GetNodeIdByRfid(string rfidId)
|
||||
{
|
||||
var currentNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNodeId);
|
||||
if (currentNode == null)
|
||||
return new List<string>();
|
||||
|
||||
return currentNode.ConnectedNodes.ToList();
|
||||
return _rfidPathfinder.GetNodeIdByRfid(rfidId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 최적화 (선택적 기능)
|
||||
/// NodeId로 RFID 조회
|
||||
/// </summary>
|
||||
public PathResult OptimizePath(PathResult originalPath)
|
||||
/// <param name="nodeId">NodeId</param>
|
||||
/// <returns>RFID 또는 null</returns>
|
||||
public string GetRfidByNodeId(string nodeId)
|
||||
{
|
||||
if (originalPath == null || !originalPath.Success)
|
||||
return originalPath;
|
||||
|
||||
// TODO: 경로 최적화 로직 구현
|
||||
// - 불필요한 중간 정지점 제거
|
||||
// - 회전 최소화
|
||||
// - 경로 단순화
|
||||
|
||||
return originalPath;
|
||||
return _rfidPathfinder.GetRfidByNodeId(nodeId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
/// <summary>
|
||||
/// 활성화된 RFID 목록 반환
|
||||
/// </summary>
|
||||
/// <returns>활성화된 RFID 목록</returns>
|
||||
public List<string> GetActiveRfidList()
|
||||
{
|
||||
return _rfidPathfinder.GetActiveRfidList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID pathfinder의 AGV 현재 방향 설정
|
||||
/// </summary>
|
||||
/// <param name="direction">현재 방향</param>
|
||||
public void SetRfidPathfinderCurrentDirection(AgvDirection direction)
|
||||
{
|
||||
_rfidPathfinder.CurrentDirection = direction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID pathfinder의 회전 비용 가중치 설정
|
||||
/// </summary>
|
||||
/// <param name="weight">회전 비용 가중치</param>
|
||||
public void SetRfidPathfinderRotationCostWeight(float weight)
|
||||
{
|
||||
_rfidPathfinder.RotationCostWeight = weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user