- PathValidationResult 클래스를 Validation 폴더에 적절히 배치 - BacktrackingPattern 클래스로 A→B→A 패턴 상세 검출 - DirectionChangePlanner에서 되돌아가기 패턴 자동 검증 - CLAUDE.md에 AGVNavigationCore 프로젝트 구조 가이드 추가 - 빌드 시스템 오류 모두 해결 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
481 lines
16 KiB
C#
481 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.Drawing.Drawing2D;
|
|
|
|
namespace AGVNavigationCore.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>
|
|
/// 도킹 방향 (도킹/충전 노드인 경우에만 Forward/Backward, 일반 노드는 DontCare)
|
|
/// </summary>
|
|
public DockingDirection DockDirection { get; set; } = DockingDirection.DontCare;
|
|
|
|
/// <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 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>
|
|
/// RFID 상태 (정상, 손상, 교체예정 등)
|
|
/// </summary>
|
|
public string RfidStatus { get; set; } = "정상";
|
|
|
|
/// <summary>
|
|
/// RFID 설치 위치 설명 (현장 작업자용)
|
|
/// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등
|
|
/// </summary>
|
|
public string RfidDescription { 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(Name))
|
|
{
|
|
displayText += $" - {Name}";
|
|
}
|
|
|
|
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,
|
|
IsActive = IsActive,
|
|
DisplayColor = DisplayColor,
|
|
RfidId = RfidId,
|
|
RfidStatus = RfidStatus,
|
|
RfidDescription = RfidDescription,
|
|
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 = CompositingQuality.HighQuality;
|
|
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
|
graphics.SmoothingMode = 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 경로 찾기에 사용 가능한 노드인지 확인
|
|
/// (라벨, 이미지 노드는 경로 찾기에서 제외)
|
|
/// </summary>
|
|
public bool IsNavigationNode()
|
|
{
|
|
return Type != NodeType.Label && Type != NodeType.Image && IsActive;
|
|
}
|
|
|
|
/// <summary>
|
|
/// RFID가 할당되어 있는지 확인
|
|
/// </summary>
|
|
public bool HasRfid()
|
|
{
|
|
return !string.IsNullOrEmpty(RfidId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// RFID 정보 설정
|
|
/// </summary>
|
|
/// <param name="rfidId">RFID ID</param>
|
|
/// <param name="rfidDescription">설치 위치 설명</param>
|
|
/// <param name="rfidStatus">RFID 상태</param>
|
|
public void SetRfidInfo(string rfidId, string rfidDescription = "", string rfidStatus = "정상")
|
|
{
|
|
RfidId = rfidId;
|
|
RfidDescription = rfidDescription;
|
|
RfidStatus = rfidStatus;
|
|
ModifiedDate = DateTime.Now;
|
|
}
|
|
|
|
/// <summary>
|
|
/// RFID 정보 삭제
|
|
/// </summary>
|
|
public void ClearRfidInfo()
|
|
{
|
|
RfidId = string.Empty;
|
|
RfidDescription = string.Empty;
|
|
RfidStatus = "정상";
|
|
ModifiedDate = DateTime.Now;
|
|
}
|
|
|
|
/// <summary>
|
|
/// RFID 기반 표시 텍스트 (RFID ID 우선, 없으면 노드ID)
|
|
/// </summary>
|
|
public string GetRfidDisplayText()
|
|
{
|
|
return HasRfid() ? RfidId : NodeId;
|
|
}
|
|
}
|
|
} |