fix: RFID duplicate validation and correct magnet direction calculation
- Add real-time RFID duplicate validation in map editor with automatic rollback - Remove RFID auto-assignment to maintain data consistency between editor and simulator - Fix magnet direction calculation to use actual forward direction angles instead of arbitrary assignment - Add node names to simulator combo boxes for better identification - Improve UI layout by drawing connection lines before text for better visibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
162
Cs_HMI/AGVMapEditor/Models/EditorSettings.cs
Normal file
162
Cs_HMI/AGVMapEditor/Models/EditorSettings.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 맵 에디터의 환경설정을 관리하는 클래스
|
||||
/// </summary>
|
||||
public class EditorSettings
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// 마지막으로 열었던 맵 파일의 경로
|
||||
/// </summary>
|
||||
public string LastMapFilePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 프로그램 시작시 마지막 맵 파일을 자동으로 로드할지 여부
|
||||
/// </summary>
|
||||
public bool AutoLoadLastMapFile { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 설정이 마지막으로 저장된 시간
|
||||
/// </summary>
|
||||
public DateTime LastSaved { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 맵 파일 저장 디렉토리
|
||||
/// </summary>
|
||||
public string DefaultMapDirectory { get; set; } = string.Empty;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constants
|
||||
|
||||
private static readonly string SettingsFileName = "EditorSettings.json";
|
||||
private static readonly string SettingsDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AGVMapEditor");
|
||||
|
||||
private static readonly string SettingsFilePath = Path.Combine(SettingsDirectory, SettingsFileName);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Instance
|
||||
|
||||
private static EditorSettings _instance;
|
||||
private static readonly object _lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// 싱글톤 인스턴스
|
||||
/// </summary>
|
||||
public static EditorSettings Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = LoadSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// 설정을 파일에서 로드
|
||||
/// </summary>
|
||||
private static EditorSettings LoadSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(SettingsFilePath))
|
||||
{
|
||||
string jsonContent = File.ReadAllText(SettingsFilePath);
|
||||
var settings = JsonConvert.DeserializeObject<EditorSettings>(jsonContent);
|
||||
return settings ?? new EditorSettings();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 설정 로드 실패시 기본 설정 사용
|
||||
System.Diagnostics.Debug.WriteLine($"설정 로드 실패: {ex.Message}");
|
||||
}
|
||||
|
||||
return new EditorSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 설정을 파일에 저장
|
||||
/// </summary>
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 디렉토리가 없으면 생성
|
||||
if (!Directory.Exists(SettingsDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(SettingsDirectory);
|
||||
}
|
||||
|
||||
LastSaved = DateTime.Now;
|
||||
string jsonContent = JsonConvert.SerializeObject(this, Formatting.Indented);
|
||||
File.WriteAllText(SettingsFilePath, jsonContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"설정 저장 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 맵 파일 경로 업데이트
|
||||
/// </summary>
|
||||
/// <param name="filePath">맵 파일 경로</param>
|
||||
public void UpdateLastMapFile(string filePath)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
|
||||
{
|
||||
LastMapFilePath = filePath;
|
||||
|
||||
// 기본 디렉토리도 업데이트
|
||||
DefaultMapDirectory = Path.GetDirectoryName(filePath);
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 맵 파일이 존재하는지 확인
|
||||
/// </summary>
|
||||
public bool HasValidLastMapFile()
|
||||
{
|
||||
return !string.IsNullOrEmpty(LastMapFilePath) && File.Exists(LastMapFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 설정 초기화
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
LastMapFilePath = string.Empty;
|
||||
AutoLoadLastMapFile = true;
|
||||
DefaultMapDirectory = string.Empty;
|
||||
LastSaved = DateTime.Now;
|
||||
Save();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 노드 타입 열거형
|
||||
/// </summary>
|
||||
public enum NodeType
|
||||
{
|
||||
/// <summary>일반 경로 노드</summary>
|
||||
Normal,
|
||||
/// <summary>회전 가능 지점</summary>
|
||||
Rotation,
|
||||
/// <summary>도킹 스테이션</summary>
|
||||
Docking,
|
||||
/// <summary>충전 스테이션</summary>
|
||||
Charging,
|
||||
/// <summary>라벨 (UI 요소)</summary>
|
||||
Label,
|
||||
/// <summary>이미지 (UI 요소)</summary>
|
||||
Image
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향 열거형
|
||||
/// </summary>
|
||||
public enum DockingDirection
|
||||
{
|
||||
/// <summary>전진 도킹 (충전기)</summary>
|
||||
Forward,
|
||||
/// <summary>후진 도킹 (로더, 클리너, 오프로더, 버퍼)</summary>
|
||||
Backward
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 이동 방향 열거형
|
||||
/// </summary>
|
||||
public enum AgvDirection
|
||||
{
|
||||
/// <summary>전진 (모니터 방향)</summary>
|
||||
Forward,
|
||||
/// <summary>후진 (리프트 방향)</summary>
|
||||
Backward,
|
||||
/// <summary>좌회전</summary>
|
||||
Left,
|
||||
/// <summary>우회전</summary>
|
||||
Right,
|
||||
/// <summary>정지</summary>
|
||||
Stop
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 장비 타입 열거형
|
||||
/// </summary>
|
||||
public enum StationType
|
||||
{
|
||||
/// <summary>로더</summary>
|
||||
Loader,
|
||||
/// <summary>클리너</summary>
|
||||
Cleaner,
|
||||
/// <summary>오프로더</summary>
|
||||
Offloader,
|
||||
/// <summary>버퍼</summary>
|
||||
Buffer,
|
||||
/// <summary>충전기</summary>
|
||||
Charger
|
||||
}
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 노드 정보를 관리하는 클래스
|
||||
/// 논리적 노드로서 실제 맵의 위치와 속성을 정의
|
||||
/// </summary>
|
||||
public class MapNode
|
||||
{
|
||||
/// <summary>
|
||||
/// 논리적 노드 ID (맵 에디터에서 관리하는 고유 ID)
|
||||
/// 예: "N001", "N002", "LOADER1", "CHARGER1"
|
||||
/// </summary>
|
||||
public string NodeId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 표시 이름 (사용자 친화적)
|
||||
/// 예: "로더1", "충전기1", "교차점A", "회전지점1"
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 맵 상의 위치 좌표 (픽셀 단위)
|
||||
/// </summary>
|
||||
public Point Position { get; set; } = Point.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 타입
|
||||
/// </summary>
|
||||
public NodeType Type { get; set; } = NodeType.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향 (도킹/충전 노드인 경우만 사용)
|
||||
/// </summary>
|
||||
public DockingDirection? DockDirection { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 연결된 노드 ID 목록 (경로 정보)
|
||||
/// </summary>
|
||||
public List<string> ConnectedNodes { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 회전 가능 여부 (180도 회전 가능한 지점)
|
||||
/// </summary>
|
||||
public bool CanRotate { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 장비 ID (도킹/충전 스테이션인 경우)
|
||||
/// 예: "LOADER1", "CLEANER1", "BUFFER1", "CHARGER1"
|
||||
/// </summary>
|
||||
public string StationId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 장비 타입 (도킹/충전 스테이션인 경우)
|
||||
/// </summary>
|
||||
public StationType? StationType { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 생성 일자
|
||||
/// </summary>
|
||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 수정 일자
|
||||
/// </summary>
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 설명 (추가 정보)
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 활성화 여부
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 색상 (맵 에디터 표시용)
|
||||
/// </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>
|
||||
public MapNode()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <param name="name">노드 이름</param>
|
||||
/// <param name="position">위치</param>
|
||||
/// <param name="type">노드 타입</param>
|
||||
public MapNode(string nodeId, string name, Point position, NodeType type)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
Name = name;
|
||||
Position = position;
|
||||
Type = type;
|
||||
CreatedDate = DateTime.Now;
|
||||
ModifiedDate = DateTime.Now;
|
||||
|
||||
// 타입별 기본 색상 설정
|
||||
SetDefaultColorByType(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 타입에 따른 기본 색상 설정
|
||||
/// </summary>
|
||||
/// <param name="type">노드 타입</param>
|
||||
public void SetDefaultColorByType(NodeType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case NodeType.Normal:
|
||||
DisplayColor = Color.Blue;
|
||||
break;
|
||||
case NodeType.Rotation:
|
||||
DisplayColor = Color.Orange;
|
||||
break;
|
||||
case NodeType.Docking:
|
||||
DisplayColor = Color.Green;
|
||||
break;
|
||||
case NodeType.Charging:
|
||||
DisplayColor = Color.Red;
|
||||
break;
|
||||
case NodeType.Label:
|
||||
DisplayColor = Color.Purple;
|
||||
break;
|
||||
case NodeType.Image:
|
||||
DisplayColor = Color.Brown;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다른 노드와의 연결 추가
|
||||
/// </summary>
|
||||
/// <param name="nodeId">연결할 노드 ID</param>
|
||||
public void AddConnection(string nodeId)
|
||||
{
|
||||
if (!ConnectedNodes.Contains(nodeId))
|
||||
{
|
||||
ConnectedNodes.Add(nodeId);
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다른 노드와의 연결 제거
|
||||
/// </summary>
|
||||
/// <param name="nodeId">연결 해제할 노드 ID</param>
|
||||
public void RemoveConnection(string nodeId)
|
||||
{
|
||||
if (ConnectedNodes.Remove(nodeId))
|
||||
{
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 스테이션 설정
|
||||
/// </summary>
|
||||
/// <param name="stationId">장비 ID</param>
|
||||
/// <param name="stationType">장비 타입</param>
|
||||
/// <param name="dockDirection">도킹 방향</param>
|
||||
public void SetDockingStation(string stationId, StationType stationType, DockingDirection dockDirection)
|
||||
{
|
||||
Type = NodeType.Docking;
|
||||
StationId = stationId;
|
||||
StationType = stationType;
|
||||
DockDirection = dockDirection;
|
||||
SetDefaultColorByType(NodeType.Docking);
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 충전 스테이션 설정
|
||||
/// </summary>
|
||||
/// <param name="stationId">충전기 ID</param>
|
||||
public void SetChargingStation(string stationId)
|
||||
{
|
||||
Type = NodeType.Charging;
|
||||
StationId = stationId;
|
||||
StationType = Models.StationType.Charger;
|
||||
DockDirection = DockingDirection.Forward; // 충전기는 항상 전진 도킹
|
||||
SetDefaultColorByType(NodeType.Charging);
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
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>
|
||||
/// <returns>복사된 노드</returns>
|
||||
public MapNode Clone()
|
||||
{
|
||||
var clone = new MapNode
|
||||
{
|
||||
NodeId = NodeId,
|
||||
Name = Name,
|
||||
Position = Position,
|
||||
Type = Type,
|
||||
DockDirection = DockDirection,
|
||||
ConnectedNodes = new List<string>(ConnectedNodes),
|
||||
CanRotate = CanRotate,
|
||||
StationId = StationId,
|
||||
StationType = StationType,
|
||||
CreatedDate = CreatedDate,
|
||||
ModifiedDate = ModifiedDate,
|
||||
Description = Description,
|
||||
IsActive = IsActive,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,11 @@ namespace AGVMapEditor.Models
|
||||
_mapNodes = mapNodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 래핑된 MapNode 인스턴스 접근
|
||||
/// </summary>
|
||||
public MapNode WrappedNode => _node;
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 ID")]
|
||||
[Description("노드의 고유 식별자")]
|
||||
@@ -207,6 +212,11 @@ namespace AGVMapEditor.Models
|
||||
_mapNodes = mapNodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 래핑된 MapNode 인스턴스 접근
|
||||
/// </summary>
|
||||
public MapNode WrappedNode => _node;
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 ID")]
|
||||
[Description("노드의 고유 식별자")]
|
||||
@@ -350,6 +360,11 @@ namespace AGVMapEditor.Models
|
||||
_mapNodes = mapNodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 래핑된 MapNode 인스턴스 접근
|
||||
/// </summary>
|
||||
public MapNode WrappedNode => _node;
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 ID")]
|
||||
[Description("노드의 고유 식별자")]
|
||||
@@ -450,18 +465,6 @@ namespace AGVMapEditor.Models
|
||||
|
||||
|
||||
|
||||
[Category("고급")]
|
||||
[DisplayName("설명")]
|
||||
[Description("노드에 대한 추가 설명")]
|
||||
public string Description
|
||||
{
|
||||
get => _node.Description;
|
||||
set
|
||||
{
|
||||
_node.Description = value ?? "";
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("고급")]
|
||||
[DisplayName("활성화")]
|
||||
|
||||
@@ -49,6 +49,8 @@ namespace AGVMapEditor.Models
|
||||
_astarPathfinder.SetMapNodes(mapNodes);
|
||||
// RfidPathfinder는 MapNode의 RFID 정보를 직접 사용
|
||||
_rfidPathfinder.SetMapNodes(mapNodes);
|
||||
// 도킹 조건 검색용 내부 노드 목록 업데이트
|
||||
UpdateInternalMapNodes(mapNodes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -63,6 +65,32 @@ namespace AGVMapEditor.Models
|
||||
return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 경로 계산 (옵션 지정 가능)
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <param name="endNodeId">목적지 노드 ID</param>
|
||||
/// <param name="targetDirection">목적지 도착 방향</param>
|
||||
/// <param name="options">경로 탐색 옵션</param>
|
||||
/// <returns>AGV 경로 계산 결과</returns>
|
||||
public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection, PathfindingOptions options)
|
||||
{
|
||||
return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 경로 계산 (현재 방향 및 PathfindingOptions 지원)
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <param name="endNodeId">목적지 노드 ID</param>
|
||||
/// <param name="currentDirection">현재 AGV 방향</param>
|
||||
/// <param name="targetDirection">목적지 도착 방향</param>
|
||||
/// <param name="options">경로 탐색 옵션</param>
|
||||
/// <returns>AGV 경로 계산 결과</returns>
|
||||
public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? currentDirection, AgvDirection? targetDirection, PathfindingOptions options)
|
||||
{
|
||||
return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, currentDirection, targetDirection, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 충전 스테이션으로의 경로 찾기
|
||||
@@ -264,5 +292,141 @@ namespace AGVMapEditor.Models
|
||||
{
|
||||
_rfidPathfinder.RotationCostWeight = weight;
|
||||
}
|
||||
|
||||
#region 도킹 조건 검색 기능
|
||||
|
||||
// 내부 노드 목록 저장
|
||||
private List<MapNode> _mapNodes;
|
||||
|
||||
/// <summary>
|
||||
/// 맵 노드 설정 (도킹 조건 검색용)
|
||||
/// </summary>
|
||||
private void UpdateInternalMapNodes(List<MapNode> mapNodes)
|
||||
{
|
||||
_mapNodes = mapNodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향 기반 노드 검색
|
||||
/// </summary>
|
||||
/// <param name="dockingDirection">도킹 방향</param>
|
||||
/// <returns>해당 도킹 방향의 노드 목록</returns>
|
||||
public List<MapNode> GetNodesByDockingDirection(DockingDirection dockingDirection)
|
||||
{
|
||||
if (_mapNodes == null) return new List<MapNode>();
|
||||
|
||||
var result = new List<MapNode>();
|
||||
|
||||
foreach (var node in _mapNodes)
|
||||
{
|
||||
if (!node.IsActive) continue;
|
||||
|
||||
var nodeDockingDirection = GetNodeDockingDirection(node);
|
||||
if (nodeDockingDirection == dockingDirection)
|
||||
{
|
||||
result.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드의 도킹 방향 결정
|
||||
/// </summary>
|
||||
/// <param name="node">노드</param>
|
||||
/// <returns>도킹 방향</returns>
|
||||
public DockingDirection GetNodeDockingDirection(MapNode node)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case NodeType.Charging:
|
||||
return DockingDirection.Forward; // 충전기: 전진 도킹
|
||||
case NodeType.Docking:
|
||||
return DockingDirection.Backward; // 장비: 후진 도킹
|
||||
default:
|
||||
return DockingDirection.Forward; // 기본값: 전진
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향과 장비 타입에 맞는 노드들로의 경로 검색
|
||||
/// </summary>
|
||||
/// <param name="startRfidId">시작 RFID</param>
|
||||
/// <param name="dockingDirection">필요한 도킹 방향</param>
|
||||
/// <param name="stationType">장비 타입 (선택사항)</param>
|
||||
/// <returns>경로 계산 결과 목록 (거리 순 정렬)</returns>
|
||||
public List<RfidPathResult> FindPathsByDockingCondition(string startRfidId, DockingDirection dockingDirection, StationType? stationType = null)
|
||||
{
|
||||
var targetNodes = GetNodesByDockingDirection(dockingDirection);
|
||||
var results = new List<RfidPathResult>();
|
||||
|
||||
// 장비 타입 필터링 (필요시)
|
||||
if (stationType.HasValue && dockingDirection == DockingDirection.Backward)
|
||||
{
|
||||
// 후진 도킹이면서 특정 장비 타입이 지정된 경우
|
||||
// 이 부분은 추후 StationMapping 정보가 있을 때 구현
|
||||
// 현재는 모든 도킹 노드를 대상으로 함
|
||||
}
|
||||
|
||||
foreach (var targetNode in targetNodes)
|
||||
{
|
||||
if (!targetNode.HasRfid()) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var pathResult = _rfidPathfinder.FindAGVPath(startRfidId, targetNode.RfidId);
|
||||
if (pathResult.Success)
|
||||
{
|
||||
results.Add(pathResult);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 개별 경로 계산 실패는 무시하고 계속 진행
|
||||
System.Diagnostics.Debug.WriteLine($"Path calculation failed from {startRfidId} to {targetNode.RfidId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 거리 순으로 정렬
|
||||
return results.OrderBy(r => r.TotalDistance).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 가장 가까운 충전기 경로 찾기 (전진 도킹)
|
||||
/// </summary>
|
||||
/// <param name="startRfidId">시작 RFID</param>
|
||||
/// <returns>가장 가까운 충전기로의 경로</returns>
|
||||
public RfidPathResult FindNearestChargingStationPath(string startRfidId)
|
||||
{
|
||||
var chargingPaths = FindPathsByDockingCondition(startRfidId, DockingDirection.Forward);
|
||||
var chargingNodes = chargingPaths.Where(p => p.Success).ToList();
|
||||
|
||||
return chargingNodes.FirstOrDefault() ?? new RfidPathResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "충전 가능한 충전기를 찾을 수 없습니다."
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 가장 가까운 장비 도킹 경로 찾기 (후진 도킹)
|
||||
/// </summary>
|
||||
/// <param name="startRfidId">시작 RFID</param>
|
||||
/// <param name="stationType">장비 타입 (선택사항)</param>
|
||||
/// <returns>가장 가까운 장비로의 경로</returns>
|
||||
public RfidPathResult FindNearestEquipmentPath(string startRfidId, StationType? stationType = null)
|
||||
{
|
||||
var equipmentPaths = FindPathsByDockingCondition(startRfidId, DockingDirection.Backward, stationType);
|
||||
var equipmentNodes = equipmentPaths.Where(p => p.Success).ToList();
|
||||
|
||||
return equipmentNodes.FirstOrDefault() ?? new RfidPathResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = $"도킹 가능한 장비를 찾을 수 없습니다. ({stationType?.ToString() ?? "모든 타입"})"
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A* 알고리즘에서 사용되는 경로 노드
|
||||
/// </summary>
|
||||
public class PathNode : IComparable<PathNode>
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 노드 ID
|
||||
/// </summary>
|
||||
public string NodeId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// AGV의 현재 방향 (이 노드에 도달했을 때의 방향)
|
||||
/// </summary>
|
||||
public AgvDirection Direction { get; set; } = AgvDirection.Forward;
|
||||
|
||||
/// <summary>
|
||||
/// 시작점에서 이 노드까지의 실제 비용 (G)
|
||||
/// </summary>
|
||||
public float GCost { get; set; } = float.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// 이 노드에서 목표까지의 추정 비용 (H)
|
||||
/// </summary>
|
||||
public float HCost { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 총 비용 (F = G + H)
|
||||
/// </summary>
|
||||
public float FCost => GCost + HCost;
|
||||
|
||||
/// <summary>
|
||||
/// 이전 노드 (경로 추적용)
|
||||
/// </summary>
|
||||
public PathNode Parent { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 회전 횟수 (방향 전환 비용 계산용)
|
||||
/// </summary>
|
||||
public int RotationCount { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 이 노드에 도달하기 위한 이동 명령 시퀀스
|
||||
/// </summary>
|
||||
public List<AgvDirection> MovementSequence { get; set; } = new List<AgvDirection>();
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public PathNode()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <param name="direction">AGV 방향</param>
|
||||
public PathNode(string nodeId, AgvDirection direction)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
Direction = direction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 우선순위 큐를 위한 비교 (FCost 기준)
|
||||
/// </summary>
|
||||
public int CompareTo(PathNode other)
|
||||
{
|
||||
if (other == null) return 1;
|
||||
|
||||
int compare = FCost.CompareTo(other.FCost);
|
||||
if (compare == 0)
|
||||
{
|
||||
// FCost가 같으면 HCost가 낮은 것을 우선
|
||||
compare = HCost.CompareTo(other.HCost);
|
||||
}
|
||||
if (compare == 0)
|
||||
{
|
||||
// 그것도 같으면 회전 횟수가 적은 것을 우선
|
||||
compare = RotationCount.CompareTo(other.RotationCount);
|
||||
}
|
||||
|
||||
return compare;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 상태 복사
|
||||
/// </summary>
|
||||
public PathNode Clone()
|
||||
{
|
||||
return new PathNode
|
||||
{
|
||||
NodeId = NodeId,
|
||||
Direction = Direction,
|
||||
GCost = GCost,
|
||||
HCost = HCost,
|
||||
Parent = Parent,
|
||||
RotationCount = RotationCount,
|
||||
MovementSequence = new List<AgvDirection>(MovementSequence)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 고유 키 생성 (노드ID + 방향)
|
||||
/// </summary>
|
||||
public string GetKey()
|
||||
{
|
||||
return $"{NodeId}_{Direction}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{NodeId}({Direction}) F:{FCost:F1} G:{GCost:F1} H:{HCost:F1} R:{RotationCount}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 해시코드 (딕셔너리 키용)
|
||||
/// </summary>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return GetKey().GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 동등성 비교
|
||||
/// </summary>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is PathNode other)
|
||||
{
|
||||
return NodeId == other.NodeId && Direction == other.Direction;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 계산 결과
|
||||
/// </summary>
|
||||
public class PathResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 계산 성공 여부
|
||||
/// </summary>
|
||||
public bool Success { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 경로상의 노드 ID 시퀀스
|
||||
/// </summary>
|
||||
public List<string> NodeSequence { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// AGV 이동 명령 시퀀스
|
||||
/// </summary>
|
||||
public List<AgvDirection> MovementSequence { get; set; } = new List<AgvDirection>();
|
||||
|
||||
/// <summary>
|
||||
/// 총 이동 거리 (비용)
|
||||
/// </summary>
|
||||
public float TotalDistance { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 총 회전 횟수
|
||||
/// </summary>
|
||||
public int TotalRotations { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 예상 소요 시간 (초)
|
||||
/// </summary>
|
||||
public float EstimatedTime { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 시작 노드 ID
|
||||
/// </summary>
|
||||
public string StartNodeId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 목표 노드 ID
|
||||
/// </summary>
|
||||
public string TargetNodeId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 시작시 AGV 방향
|
||||
/// </summary>
|
||||
public AgvDirection StartDirection { get; set; } = AgvDirection.Forward;
|
||||
|
||||
/// <summary>
|
||||
/// 도착시 AGV 방향
|
||||
/// </summary>
|
||||
public AgvDirection EndDirection { get; set; } = AgvDirection.Forward;
|
||||
|
||||
/// <summary>
|
||||
/// 경로 계산에 걸린 시간 (밀리초)
|
||||
/// </summary>
|
||||
public long CalculationTime { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 오류 메시지 (실패시)
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 경로상의 상세 정보 (디버깅용)
|
||||
/// </summary>
|
||||
public List<PathNode> DetailedPath { get; set; } = new List<PathNode>();
|
||||
|
||||
/// <summary>
|
||||
/// 회전이 발생하는 노드들
|
||||
/// </summary>
|
||||
public List<string> RotationNodes { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public PathResult()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 성공 결과 생성자
|
||||
/// </summary>
|
||||
public PathResult(List<PathNode> path, string startNodeId, string targetNodeId, AgvDirection startDirection)
|
||||
{
|
||||
if (path == null || path.Count == 0)
|
||||
{
|
||||
Success = false;
|
||||
ErrorMessage = "빈 경로입니다.";
|
||||
return;
|
||||
}
|
||||
|
||||
Success = true;
|
||||
StartNodeId = startNodeId;
|
||||
TargetNodeId = targetNodeId;
|
||||
StartDirection = startDirection;
|
||||
DetailedPath = new List<PathNode>(path);
|
||||
|
||||
// 노드 시퀀스 구성
|
||||
NodeSequence = path.Select(p => p.NodeId).ToList();
|
||||
|
||||
// 이동 명령 시퀀스 구성
|
||||
MovementSequence = new List<AgvDirection>();
|
||||
for (int i = 0; i < path.Count; i++)
|
||||
{
|
||||
MovementSequence.AddRange(path[i].MovementSequence);
|
||||
}
|
||||
|
||||
// 통계 계산
|
||||
if (path.Count > 0)
|
||||
{
|
||||
TotalDistance = path[path.Count - 1].GCost;
|
||||
EndDirection = path[path.Count - 1].Direction;
|
||||
}
|
||||
|
||||
TotalRotations = MovementSequence.Count(cmd =>
|
||||
cmd == AgvDirection.Left || cmd == AgvDirection.Right);
|
||||
|
||||
// 회전 노드 추출
|
||||
var previousDirection = startDirection;
|
||||
for (int i = 0; i < path.Count; i++)
|
||||
{
|
||||
if (path[i].Direction != previousDirection)
|
||||
{
|
||||
RotationNodes.Add(path[i].NodeId);
|
||||
}
|
||||
previousDirection = path[i].Direction;
|
||||
}
|
||||
|
||||
// 예상 소요 시간 계산 (단순 추정)
|
||||
EstimatedTime = CalculateEstimatedTime();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실패 결과 생성자
|
||||
/// </summary>
|
||||
public PathResult(string errorMessage)
|
||||
{
|
||||
Success = false;
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 예상 소요 시간 계산
|
||||
/// </summary>
|
||||
private float CalculateEstimatedTime()
|
||||
{
|
||||
// 기본 이동 속도 및 회전 시간 가정
|
||||
const float MOVE_SPEED = 1.0f; // 단위/초
|
||||
const float ROTATION_TIME = 2.0f; // 초/회전
|
||||
|
||||
float moveTime = TotalDistance / MOVE_SPEED;
|
||||
float rotationTime = TotalRotations * ROTATION_TIME;
|
||||
|
||||
return moveTime + rotationTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 요약 정보
|
||||
/// </summary>
|
||||
public string GetSummary()
|
||||
{
|
||||
if (!Success)
|
||||
{
|
||||
return $"경로 계산 실패: {ErrorMessage}";
|
||||
}
|
||||
|
||||
return $"경로: {NodeSequence.Count}개 노드, " +
|
||||
$"거리: {TotalDistance:F1}, " +
|
||||
$"회전: {TotalRotations}회, " +
|
||||
$"예상시간: {EstimatedTime:F1}초";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 상세 경로 정보
|
||||
/// </summary>
|
||||
public List<string> GetDetailedSteps()
|
||||
{
|
||||
var steps = new List<string>();
|
||||
|
||||
if (!Success)
|
||||
{
|
||||
steps.Add($"경로 계산 실패: {ErrorMessage}");
|
||||
return steps;
|
||||
}
|
||||
|
||||
steps.Add($"시작: {StartNodeId} (방향: {StartDirection})");
|
||||
|
||||
for (int i = 0; i < DetailedPath.Count; i++)
|
||||
{
|
||||
var node = DetailedPath[i];
|
||||
var step = $"{i + 1}. {node.NodeId}";
|
||||
|
||||
if (node.MovementSequence.Count > 0)
|
||||
{
|
||||
step += $" [명령: {string.Join(",", node.MovementSequence)}]";
|
||||
}
|
||||
|
||||
step += $" (F:{node.FCost:F1}, 방향:{node.Direction})";
|
||||
steps.Add(step);
|
||||
}
|
||||
|
||||
steps.Add($"도착: {TargetNodeId} (최종 방향: {EndDirection})");
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID 시퀀스 추출 (실제 AGV 제어용)
|
||||
/// </summary>
|
||||
public List<string> GetRfidSequence(NodeResolver nodeResolver)
|
||||
{
|
||||
var rfidSequence = new List<string>();
|
||||
|
||||
foreach (var nodeId in NodeSequence)
|
||||
{
|
||||
var rfidId = nodeResolver.GetRfidByNodeId(nodeId);
|
||||
if (!string.IsNullOrEmpty(rfidId))
|
||||
{
|
||||
rfidSequence.Add(rfidId);
|
||||
}
|
||||
}
|
||||
|
||||
return rfidSequence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 유효성 검증
|
||||
/// </summary>
|
||||
public bool ValidatePath(List<MapNode> mapNodes)
|
||||
{
|
||||
if (!Success || NodeSequence.Count == 0)
|
||||
return false;
|
||||
|
||||
// 모든 노드가 존재하는지 확인
|
||||
foreach (var nodeId in NodeSequence)
|
||||
{
|
||||
if (!mapNodes.Any(n => n.NodeId == nodeId))
|
||||
{
|
||||
ErrorMessage = $"존재하지 않는 노드: {nodeId}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 연결성 확인
|
||||
for (int i = 0; i < NodeSequence.Count - 1; i++)
|
||||
{
|
||||
var currentNode = mapNodes.FirstOrDefault(n => n.NodeId == NodeSequence[i]);
|
||||
var nextNodeId = NodeSequence[i + 1];
|
||||
|
||||
if (currentNode != null && !currentNode.ConnectedNodes.Contains(nextNodeId))
|
||||
{
|
||||
ErrorMessage = $"연결되지 않은 노드: {currentNode.NodeId} → {nextNodeId}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 직렬화를 위한 문자열 변환
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return GetSummary();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
using System;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// RFID와 논리적 노드 ID를 매핑하는 클래스
|
||||
/// 물리적 RFID는 의미없는 고유값, 논리적 노드는 맵 에디터에서 관리
|
||||
/// </summary>
|
||||
public class RfidMapping
|
||||
{
|
||||
/// <summary>
|
||||
/// 물리적 RFID 값 (의미 없는 고유 식별자)
|
||||
/// 예: "1234567890", "ABCDEF1234" 등
|
||||
/// </summary>
|
||||
public string RfidId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 논리적 노드 ID (맵 에디터에서 관리)
|
||||
/// 예: "N001", "N002", "LOADER1", "CHARGER1" 등
|
||||
/// </summary>
|
||||
public string LogicalNodeId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 매핑 생성 일자
|
||||
/// </summary>
|
||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 수정 일자
|
||||
/// </summary>
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 설치 위치 설명 (현장 작업자용)
|
||||
/// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// RFID 상태 (정상, 손상, 교체예정 등)
|
||||
/// </summary>
|
||||
public string Status { get; set; } = "정상";
|
||||
|
||||
/// <summary>
|
||||
/// 매핑 활성화 여부
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public RfidMapping()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </summary>
|
||||
/// <param name="rfidId">물리적 RFID ID</param>
|
||||
/// <param name="logicalNodeId">논리적 노드 ID</param>
|
||||
/// <param name="description">설치 위치 설명</param>
|
||||
public RfidMapping(string rfidId, string logicalNodeId, string description = "")
|
||||
{
|
||||
RfidId = rfidId;
|
||||
LogicalNodeId = logicalNodeId;
|
||||
Description = description;
|
||||
CreatedDate = DateTime.Now;
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{RfidId} → {LogicalNodeId} ({Description})";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user