364 lines
14 KiB
C#
364 lines
14 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
|