using Microsoft.EntityFrameworkCore; using RobotNet10.MapEditor.Shared.DTOs.Requests; using RobotNet10.MapManager.Data; namespace RobotNet10.MapManager.Services; /// /// Service implementation for managing nodes /// public class NodeService(MapDbContext context) : INodeService { private readonly MapDbContext _context = context; public async Task> GetNodesByLevelAsync(Guid layoutLevelId, bool includeVehicleProperties = true) { var query = _context.Nodes.Where(n => n.LevelId == layoutLevelId); if (includeVehicleProperties) { query = query.Include(n => n.VehicleProperties) .ThenInclude(vp => vp.VehicleType); } return await query.OrderBy(n => n.NodeId).ToListAsync(); } public async Task GetByIdAsync(Guid nodeId, bool includeVehicleProperties = true) { var query = _context.Nodes.Where(n => n.Id == nodeId); if (includeVehicleProperties) { query = query.Include(n => n.VehicleProperties) .ThenInclude(vp => vp.VehicleType); } return await query.FirstOrDefaultAsync(); } public async Task UpdateAsync(Guid nodeId, UpdateNodeRequest request) { var node = await GetByIdAsync(nodeId, includeVehicleProperties: true) ?? throw new InvalidOperationException($"Node with ID '{nodeId}' not found"); // Update coordinates if provided if (request.X.HasValue) { // Validate bounds if (!await ValidateCoordinatesAsync(node.LevelId, request.X.Value, node.Y)) { throw new InvalidOperationException($"Coordinates ({request.X.Value}, {node.Y}) are outside valid bounds"); } node.X = request.X.Value; } if (request.Y.HasValue) { // Validate bounds if (!await ValidateCoordinatesAsync(node.LevelId, node.X, request.Y.Value)) { throw new InvalidOperationException($"Coordinates ({node.X}, {request.Y.Value}) are outside valid bounds"); } node.Y = request.Y.Value; } // Update other properties if (request.NodeName != null) node.NodeName = request.NodeName; if (request.NodeDescription != null) node.NodeDescription = request.NodeDescription; if (request.MapId != null) node.MapId = request.MapId; // Update vehicle properties if provided if (request.VehicleProperties != null) { // Remove existing properties var existingProps = await _context.NodeVehicleProperties .Where(nvp => nvp.NodeId == nodeId) .ToListAsync(); _context.NodeVehicleProperties.RemoveRange(existingProps); // Add new properties foreach (var propDto in request.VehicleProperties) { var prop = new NodeVehicleProperty { NodeId = nodeId, VehicleTypeId = propDto.VehicleTypeId, Theta = propDto.Theta, Actions = propDto.Actions, AllowedDeviationXY = propDto.AllowedDeviationXY, AllowedDeviationTheta = propDto.AllowedDeviationTheta }; _context.NodeVehicleProperties.Add(prop); } } await _context.SaveChangesAsync(); // Reload with vehicle properties return (await GetByIdAsync(nodeId, includeVehicleProperties: true))!; } public async Task ValidateCoordinatesAsync(Guid layoutLevelId, double x, double y) { var settings = await _context.LayoutLevelEditorSettings .FirstOrDefaultAsync(s => s.LevelId == layoutLevelId); if (settings == null) return true; // No bounds set, allow any coordinates // Check bounds if (settings.BoundsMinX.HasValue && x < settings.BoundsMinX.Value) return false; if (settings.BoundsMaxX.HasValue && x > settings.BoundsMaxX.Value) return false; if (settings.BoundsMinY.HasValue && y < settings.BoundsMinY.Value) return false; if (settings.BoundsMaxY.HasValue && y > settings.BoundsMaxY.Value) return false; return true; } public async Task> FindNodesNearCoordinatesAsync(Guid layoutLevelId, double x, double y, double radius) { // Pre-filter with bounding box at database level, then refine with Euclidean distance in-memory var nodes = await _context.Nodes .Where(n => n.LevelId == layoutLevelId && n.X >= x - radius && n.X <= x + radius && n.Y >= y - radius && n.Y <= y + radius) .ToListAsync(); return [.. nodes .Where(n => { var dx = n.X - x; var dy = n.Y - y; return dx * dx + dy * dy <= radius * radius; }) .OrderBy(n => (n.X - x) * (n.X - x) + (n.Y - y) * (n.Y - y))]; } }