- Add AGVMapEditor: Visual map editing with drag-and-drop node placement * RFID mapping separation (physical ID ↔ logical node mapping) * A* pathfinding algorithm with AGV directional constraints * JSON map data persistence with structured format * Interactive map canvas with zoom/pan functionality - Add AGVSimulator: Real-time AGV movement simulation * Virtual AGV with state machine (Idle, Moving, Rotating, Docking, Charging, Error) * Path execution and visualization from calculated routes * Real-time position tracking and battery simulation * Integration with map editor data format - Update solution structure and build configuration - Add comprehensive documentation in CLAUDE.md - Implement AGV-specific constraints (forward/backward docking, rotation limits) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
277 lines
8.3 KiB
C#
277 lines
8.3 KiB
C#
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();
|
|
}
|
|
}
|
|
} |