using Microsoft.EntityFrameworkCore; using RobotNet.VDA5050; using RobotNet10.MapEditor.Shared.DTOs.Requests; using RobotNet10.MapManager.Data; namespace RobotNet10.MapManager.Services; /// /// Service implementation for managing edges with complex node detection logic /// 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 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); } /// /// Only remove nodes that were newly created during this operation, not pre-existing ones. /// 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> 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 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 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 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 edgeIds) { using var transaction = await _context.Database.BeginTransactionAsync(); try { var nodesToCheck = new HashSet(); 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; } } }