Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View File

@@ -0,0 +1,363 @@
using Microsoft.EntityFrameworkCore;
using RobotNet.VDA5050;
using RobotNet10.MapEditor.Shared.DTOs.Requests;
using RobotNet10.MapManager.Data;
namespace RobotNet10.MapManager.Services;
/// <summary>
/// Service implementation for managing edges with complex node detection logic
/// </summary>
public class EdgeService(
MapDbContext context,
INodeService nodeService,
LayoutLevelNamingService namingService) : IEdgeService
{
private readonly MapDbContext _context = context;
private readonly INodeService _nodeService = nodeService;
private readonly LayoutLevelNamingService _namingService = namingService;
public async Task<Edge> CreateAsync(CreateEdgeRequest request)
{
// Get editor settings for validation and proximity radius
var settings = await _context.LayoutLevelEditorSettings
.FirstOrDefaultAsync(s => s.LevelId == request.LayoutLevelId);
var proximityRadius = settings?.NodeProximityRadius ?? 0.35;
var minEdgeLength = settings?.EdgeMinLengthCreate ?? 0.1;
// Calculate edge length
var dx = request.X2 - request.X1;
var dy = request.Y2 - request.Y1;
var edgeLength = Math.Sqrt(dx * dx + dy * dy);
// Validate edge length
if (edgeLength < minEdgeLength)
{
throw new InvalidOperationException(
$"Edge length ({edgeLength:F3}m) is less than minimum required ({minEdgeLength:F3}m)");
}
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Find or create start node
var (startNode, startNodeIsNew) = await FindOrCreateNodeAsync(
request.LayoutLevelId,
request.X1,
request.Y1,
proximityRadius);
// Find or create end node
var (endNode, endNodeIsNew) = await FindOrCreateNodeAsync(
request.LayoutLevelId,
request.X2,
request.Y2,
proximityRadius);
// Validate coordinates within bounds
if (!await _nodeService.ValidateCoordinatesAsync(request.LayoutLevelId, startNode.X, startNode.Y))
{
await CleanupNewNodesAsync(startNode, startNodeIsNew, endNode, endNodeIsNew);
throw new InvalidOperationException($"Start coordinates ({startNode.X}, {startNode.Y}) are outside valid bounds");
}
if (!await _nodeService.ValidateCoordinatesAsync(request.LayoutLevelId, endNode.X, endNode.Y))
{
await CleanupNewNodesAsync(startNode, startNodeIsNew, endNode, endNodeIsNew);
throw new InvalidOperationException($"End coordinates ({endNode.X}, {endNode.Y}) are outside valid bounds");
}
if (await _context.Edges.AnyAsync(e => e.StartNodeId == startNode.Id && e.EndNodeId == endNode.Id))
{
await CleanupNewNodesAsync(startNode, startNodeIsNew, endNode, endNodeIsNew);
throw new InvalidOperationException($"Edge with StartNode {startNode.NodeId} and EndNode {endNode.NodeId} already exists");
}
// Generate edge name if not provided
var edgeName = request.EdgeName;
if (string.IsNullOrEmpty(edgeName) && settings?.EdgeNameAutoGenerate == true)
{
edgeName = await _namingService.GenerateEdgeNameAsync(request.LayoutLevelId);
}
// Generate unique EdgeId
var edgeId = Guid.NewGuid().ToString("N")[..16];
// Create edge
var edge = new Edge
{
LevelId = request.LayoutLevelId,
EdgeId = edgeId,
EdgeName = edgeName,
EdgeDescription = request.EdgeDescription,
StartNodeId = startNode.Id,
EndNodeId = endNode.Id,
};
_context.Edges.Add(edge);
await _context.SaveChangesAsync();
// Add vehicle properties if provided
if (request.VehicleProperties != null && request.VehicleProperties.Count != 0)
{
foreach (var propDto in request.VehicleProperties)
{
var prop = new EdgeVehicleProperty
{
EdgeId = edge.Id,
VehicleTypeId = propDto.VehicleTypeId,
VehicleOrientation = propDto.VehicleOrientation,
OrientationType = propDto.OrientationType,
RotationAllowed = propDto.RotationAllowed,
RotationAtStartNodeAllowed = propDto.RotationAtStartNodeAllowed,
RotationAtEndNodeAllowed = propDto.RotationAtEndNodeAllowed,
MaxSpeed = propDto.MaxSpeed,
MaxRotationSpeed = propDto.MaxRotationSpeed,
MinHeight = propDto.MinHeight,
MaxHeight = propDto.MaxHeight,
LoadRestriction_Unloaded = propDto.LoadRestriction?.Unloaded,
LoadRestriction_Loaded = propDto.LoadRestriction?.Loaded,
LoadRestriction_LoadSetNames = propDto.LoadRestriction?.LoadSetNames != null && propDto.LoadRestriction.LoadSetNames.Count > 0
? System.Text.Json.JsonSerializer.Serialize(propDto.LoadRestriction.LoadSetNames)
: null,
TrajectoryDegree = propDto.TrajectoryDegree,
TrajectoryControlPoint1X = propDto.TrajectoryControlPoint1X,
TrajectoryControlPoint1Y = propDto.TrajectoryControlPoint1Y,
TrajectoryControlPoint2X = propDto.TrajectoryControlPoint2X,
TrajectoryControlPoint2Y = propDto.TrajectoryControlPoint2Y,
CorridorLeftWidth = propDto.CorridorLeftWidth,
CorridorRightWidth = propDto.CorridorRightWidth,
CorridorRefPoint = propDto.CorridorRefPoint
};
_context.EdgeVehicleProperties.Add(prop);
}
await _context.SaveChangesAsync();
}
await transaction.CommitAsync();
// Reload with full details
return (await GetByIdAsync(edge.Id, includeNodes: true, includeVehicleProperties: true))!;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
private async Task<(Node Node, bool IsNew)> FindOrCreateNodeAsync(Guid layoutLevelId, double x, double y, double proximityRadius)
{
// Find nodes within proximity radius
var nearbyNodes = await _nodeService.FindNodesNearCoordinatesAsync(layoutLevelId, x, y, proximityRadius);
if (nearbyNodes.Count != 0)
{
// Use closest existing node
return (nearbyNodes.First(), false);
}
// Create new node at exact coordinates
var nodeId = Guid.NewGuid().ToString("N")[..16];
var nodeName = await _namingService.GenerateNodeNameAsync(layoutLevelId);
var newNode = new Node
{
LevelId = layoutLevelId,
NodeId = nodeId,
NodeName = nodeName,
X = x,
Y = y
};
_context.Nodes.Add(newNode);
await _context.SaveChangesAsync();
return (newNode, true);
}
/// <summary>
/// Only remove nodes that were newly created during this operation, not pre-existing ones.
/// </summary>
private async Task CleanupNewNodesAsync(Node startNode, bool startNodeIsNew, Node endNode, bool endNodeIsNew)
{
if (startNodeIsNew) _context.Nodes.Remove(startNode);
if (endNodeIsNew) _context.Nodes.Remove(endNode);
if (startNodeIsNew || endNodeIsNew) await _context.SaveChangesAsync();
}
public async Task<List<Edge>> GetEdgesByLevelAsync(Guid layoutLevelId, bool includeNodes = true, bool includeVehicleProperties = true)
{
var query = _context.Edges.Where(e => e.LevelId == layoutLevelId);
if (includeNodes)
{
query = query.Include(e => e.StartNode).Include(e => e.EndNode);
}
if (includeVehicleProperties)
{
query = query.Include(e => e.VehicleProperties).ThenInclude(vp => vp.VehicleType);
}
return await query.OrderBy(e => e.EdgeId).ToListAsync();
}
public async Task<Edge?> GetByIdAsync(Guid edgeId, bool includeNodes = true, bool includeVehicleProperties = true)
{
var query = _context.Edges.Where(e => e.Id == edgeId);
if (includeNodes)
{
query = query.Include(e => e.StartNode).Include(e => e.EndNode);
}
if (includeVehicleProperties)
{
query = query.Include(e => e.VehicleProperties).ThenInclude(vp => vp.VehicleType);
}
return await query.FirstOrDefaultAsync();
}
public async Task<Edge> UpdateAsync(Guid edgeId, UpdateEdgeRequest request)
{
var edge = await GetByIdAsync(edgeId, includeNodes: false, includeVehicleProperties: true) ??
throw new InvalidOperationException($"Edge with ID '{edgeId}' not found");
// Update properties
if (request.EdgeName != null)
edge.EdgeName = request.EdgeName;
if (request.EdgeDescription != null)
edge.EdgeDescription = request.EdgeDescription;
// Update vehicle properties if provided
if (request.VehicleProperties != null)
{
// Remove existing properties
var existingProps = await _context.EdgeVehicleProperties
.Where(evp => evp.EdgeId == edgeId)
.ToListAsync();
_context.EdgeVehicleProperties.RemoveRange(existingProps);
// Add new properties
foreach (var propDto in request.VehicleProperties)
{
var prop = new EdgeVehicleProperty
{
EdgeId = edgeId,
VehicleTypeId = propDto.VehicleTypeId,
VehicleOrientation = propDto.VehicleOrientation,
OrientationType = propDto.OrientationType,
RotationAllowed = propDto.RotationAllowed,
RotationAtStartNodeAllowed = propDto.RotationAtStartNodeAllowed,
RotationAtEndNodeAllowed = propDto.RotationAtEndNodeAllowed,
MaxSpeed = propDto.MaxSpeed,
MaxRotationSpeed = propDto.MaxRotationSpeed,
MinHeight = propDto.MinHeight,
MaxHeight = propDto.MaxHeight,
LoadRestriction_Unloaded = propDto.LoadRestriction?.Unloaded,
LoadRestriction_Loaded = propDto.LoadRestriction?.Loaded,
LoadRestriction_LoadSetNames = propDto.LoadRestriction?.LoadSetNames != null && propDto.LoadRestriction.LoadSetNames.Count > 0
? System.Text.Json.JsonSerializer.Serialize(propDto.LoadRestriction.LoadSetNames)
: null,
TrajectoryDegree = propDto.TrajectoryDegree,
TrajectoryControlPoint1X = propDto.TrajectoryControlPoint1X,
TrajectoryControlPoint1Y = propDto.TrajectoryControlPoint1Y,
TrajectoryControlPoint2X = propDto.TrajectoryControlPoint2X,
TrajectoryControlPoint2Y = propDto.TrajectoryControlPoint2Y,
CorridorLeftWidth = propDto.CorridorLeftWidth,
CorridorRightWidth = propDto.CorridorRightWidth,
CorridorRefPoint = propDto.CorridorRefPoint
};
_context.EdgeVehicleProperties.Add(prop);
}
}
await _context.SaveChangesAsync();
return (await GetByIdAsync(edgeId, includeNodes: true, includeVehicleProperties: true))!;
}
public async Task<bool> DeleteAsync(Guid edgeId)
{
var edge = await GetByIdAsync(edgeId, includeNodes: false, includeVehicleProperties: false);
if (edge == null)
{
return false;
}
// Delete edge
_context.Edges.Remove(edge);
await _context.SaveChangesAsync();
// Check and delete orphan nodes
await DeleteOrphanNodeAsync(edge.StartNodeId);
await DeleteOrphanNodeAsync(edge.EndNodeId);
return true;
}
private async Task DeleteOrphanNodeAsync(Guid nodeId)
{
// Check if node is still referenced by any edge
var hasEdges = await _context.Edges
.AnyAsync(e => e.StartNodeId == nodeId || e.EndNodeId == nodeId);
if (!hasEdges)
{
// Node is orphan, delete it and its interaction nodes
var stationInteractions = await _context.StationInteractionNodes
.Where(sin => sin.NodeId == nodeId)
.ToListAsync();
_context.StationInteractionNodes.RemoveRange(stationInteractions);
var node = await _context.Nodes.FindAsync(nodeId);
if (node != null)
{
_context.Nodes.Remove(node);
await _context.SaveChangesAsync();
}
}
}
public async Task DeleteBatchAsync(List<Guid> edgeIds)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var nodesToCheck = new HashSet<Guid>();
foreach (var edgeId in edgeIds)
{
var edge = await _context.Edges.FindAsync(edgeId);
if (edge != null)
{
nodesToCheck.Add(edge.StartNodeId);
nodesToCheck.Add(edge.EndNodeId);
_context.Edges.Remove(edge);
}
}
await _context.SaveChangesAsync();
// Check and delete orphan nodes
foreach (var nodeId in nodesToCheck)
{
await DeleteOrphanNodeAsync(nodeId);
}
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
}