using RobotNet10.FleetManager.Shared.DTOs.RobotModel; using RobotNet10.MapEditor.Shared.DTOs.Edge; using RobotNet10.MapEditor.Shared.DTOs.Node; using RobotNet10.MapManager.Data; using RobotNet10.MapManager.Services; namespace RobotNet10.FleetManager.Services; /// /// Service implementation for managing robot model map data based on VehicleType filtering /// public class RobotModelMapService( IRobotModelService robotModelService, IVehicleTypeService vehicleTypeService, IMapQueryService mapQueryService, ILayoutService layoutService, Logger logger) : IRobotModelMapService { private readonly IRobotModelService _robotModelService = robotModelService; private readonly IVehicleTypeService _vehicleTypeService = vehicleTypeService; private readonly IMapQueryService _mapQueryService = mapQueryService; private readonly ILayoutService _layoutService = layoutService; private readonly Logger _logger = logger; public async Task> GetFilteredNodesAsync(Guid robotModelId) { // Get RobotModel var robotModel = await _robotModelService.GetByIdAsync(robotModelId) ?? throw new KeyNotFoundException($"RobotModel with ID {robotModelId} not found."); // Validate VehicleTypeId is set if (!robotModel.VehicleTypeId.HasValue) { throw new InvalidOperationException($"RobotModel '{robotModel.ModelName}' does not have VehicleTypeId set."); } var vehicleTypeId = robotModel.VehicleTypeId.Value; // Validate VehicleType exists var vehicleType = await _vehicleTypeService.GetByIdAsync(vehicleTypeId) ?? throw new KeyNotFoundException($"VehicleType with ID {vehicleTypeId} not found."); if (!vehicleType.IsActive) { _logger.Warning($"VehicleType '{vehicleType.VehicleTypeName}' is not active."); } // Get filtered nodes: nodes that have NodeVehicleProperties for this VehicleType var filteredNodes = await _mapQueryService.GetNodesByVehicleTypeAsync(vehicleTypeId); _logger.Info($"Found {filteredNodes.Count} filtered nodes for RobotModel '{robotModel.ModelName}' (VehicleType: '{vehicleType.VehicleTypeName}')"); return filteredNodes; } public async Task> GetFilteredEdgesAsync(Guid robotModelId) { // Get RobotModel var robotModel = await _robotModelService.GetByIdAsync(robotModelId) ?? throw new KeyNotFoundException($"RobotModel with ID {robotModelId} not found."); // Validate VehicleTypeId is set if (!robotModel.VehicleTypeId.HasValue) { throw new InvalidOperationException($"RobotModel '{robotModel.ModelName}' does not have VehicleTypeId set."); } var vehicleTypeId = robotModel.VehicleTypeId.Value; // Validate VehicleType exists var vehicleType = await _vehicleTypeService.GetByIdAsync(vehicleTypeId) ?? throw new KeyNotFoundException($"VehicleType with ID {vehicleTypeId} not found."); // Get filtered edges: edges that have EdgeVehicleProperties for this VehicleType var filteredEdges = await _mapQueryService.GetEdgesByVehicleTypeAsync(vehicleTypeId); _logger.Info($"Found {filteredEdges.Count} filtered edges for RobotModel '{robotModel.ModelName}' (VehicleType: '{vehicleType.VehicleTypeName}')"); return filteredEdges; } public async Task> GetFilteredNodesByLevelAsync(Guid robotModelId, Guid levelId) { // Get RobotModel var robotModel = await _robotModelService.GetByIdAsync(robotModelId) ?? throw new KeyNotFoundException($"RobotModel with ID {robotModelId} not found."); // Validate VehicleTypeId is set if (!robotModel.VehicleTypeId.HasValue) { throw new InvalidOperationException($"RobotModel '{robotModel.ModelName}' does not have VehicleTypeId set."); } var vehicleTypeId = robotModel.VehicleTypeId.Value; // Validate VehicleType exists var vehicleType = await _vehicleTypeService.GetByIdAsync(vehicleTypeId) ?? throw new KeyNotFoundException($"VehicleType with ID {vehicleTypeId} not found."); if (!vehicleType.IsActive) { _logger.Warning($"VehicleType '{vehicleType.VehicleTypeName}' is not active."); } // Get filtered nodes: nodes that have NodeVehicleProperties for this VehicleType and belong to the specified level var filteredNodes = await _mapQueryService.GetNodesByVehicleTypeAndLevelAsync(vehicleTypeId, levelId); _logger.Info($"Found {filteredNodes.Count} filtered nodes for RobotModel '{robotModel.ModelName}' (VehicleType: '{vehicleType.VehicleTypeName}') in LevelId={levelId}"); return filteredNodes; } public async Task> GetFilteredEdgesByLevelAsync(Guid robotModelId, Guid levelId) { // Get RobotModel var robotModel = await _robotModelService.GetByIdAsync(robotModelId) ?? throw new KeyNotFoundException($"RobotModel with ID {robotModelId} not found."); // Validate VehicleTypeId is set if (!robotModel.VehicleTypeId.HasValue) { throw new InvalidOperationException($"RobotModel '{robotModel.ModelName}' does not have VehicleTypeId set."); } var vehicleTypeId = robotModel.VehicleTypeId.Value; // Validate VehicleType exists var vehicleType = await _vehicleTypeService.GetByIdAsync(vehicleTypeId) ?? throw new KeyNotFoundException($"VehicleType with ID {vehicleTypeId} not found."); // Get filtered edges: edges that have EdgeVehicleProperties for this VehicleType and belong to the specified level var filteredEdges = await _mapQueryService.GetEdgesByVehicleTypeAndLevelAsync(vehicleTypeId, levelId); _logger.Info($"Found {filteredEdges.Count} filtered edges for RobotModel '{robotModel.ModelName}' (VehicleType: '{vehicleType.VehicleTypeName}') in LevelId={levelId}"); return filteredEdges; } public async Task GetValidatedMapDataAsync(Guid robotModelId) { // Get RobotModel var robotModel = await _robotModelService.GetByIdAsync(robotModelId) ?? throw new KeyNotFoundException($"RobotModel with ID {robotModelId} not found."); // Validate VehicleTypeId is set if (!robotModel.VehicleTypeId.HasValue) { throw new InvalidOperationException($"RobotModel '{robotModel.ModelName}' does not have VehicleTypeId set."); } var vehicleTypeId = robotModel.VehicleTypeId.Value; // Get VehicleType var vehicleType = await _vehicleTypeService.GetByIdAsync(vehicleTypeId) ?? throw new KeyNotFoundException($"VehicleType with ID {vehicleTypeId} not found."); // Get filtered nodes and edges (from all levels that match VehicleType) // Note: If RobotModel needs to filter by specific LevelId, MapId should be added to RobotModel var filteredNodes = await GetFilteredNodesAsync(robotModelId); var filteredEdges = await GetFilteredEdgesAsync(robotModelId); // Get total counts across all levels (for reference) // In the future, if RobotModel has MapId (LevelId), we can filter by specific level int totalNodesInLevel = await _mapQueryService.GetTotalNodesCountAsync(); int totalEdgesInLevel = await _mapQueryService.GetTotalEdgesCountAsync(); string? levelName = null; Guid? levelId = null; // If we have filtered nodes, get the level from first node (for display) if (filteredNodes.Count > 0) { var firstNode = filteredNodes.First(); var level = await _layoutService.GetLevelAsync(firstNode.LevelId); levelName = level?.LayoutLevelId; levelId = level?.Id; } // Validate: Remove nodes without edges, edges without both nodes var nodeIds = new HashSet(filteredNodes.Select(n => n.Id)); var validNodeIds = new HashSet(); var validEdges = new List(); // First pass: Find edges that have both start and end nodes in filtered nodes foreach (var edge in filteredEdges) { if (nodeIds.Contains(edge.StartNodeId) && nodeIds.Contains(edge.EndNodeId)) { validEdges.Add(edge); validNodeIds.Add(edge.StartNodeId); validNodeIds.Add(edge.EndNodeId); } } // Second pass: Keep only nodes that are connected by valid edges var validNodes = filteredNodes.Where(n => validNodeIds.Contains(n.Id)).ToList(); // Count removed items int nodesRemoved = filteredNodes.Count - validNodes.Count; int edgesRemoved = filteredEdges.Count - validEdges.Count; // Build validation result var validationResult = new MapValidationResultDto { IsValid = nodesRemoved == 0 && edgesRemoved == 0, NodesRemoved = nodesRemoved, EdgesRemoved = edgesRemoved }; // Add errors for removed nodes foreach (var node in filteredNodes.Where(n => !validNodeIds.Contains(n.Id))) { validationResult.Errors.Add(new ValidationError { Code = "NODE_NO_EDGES", Message = $"Node '{node.NodeId}' has no connected edges in the filtered map", EntityId = node.Id.ToString(), EntityType = "Node" }); } // Add errors for removed edges foreach (var edge in filteredEdges.Where(e => !validEdges.Contains(e))) { var missingStart = !nodeIds.Contains(edge.StartNodeId); var missingEnd = !nodeIds.Contains(edge.EndNodeId); if (missingStart && missingEnd) { validationResult.Errors.Add(new ValidationError { Code = "EDGE_MISSING_BOTH_NODES", Message = $"Edge '{edge.EdgeId}' is missing both start and end nodes", EntityId = edge.Id.ToString(), EntityType = "Edge" }); } else if (missingStart) { validationResult.Errors.Add(new ValidationError { Code = "EDGE_MISSING_START_NODE", Message = $"Edge '{edge.EdgeId}' is missing start node", EntityId = edge.Id.ToString(), EntityType = "Edge" }); } else if (missingEnd) { validationResult.Errors.Add(new ValidationError { Code = "EDGE_MISSING_END_NODE", Message = $"Edge '{edge.EdgeId}' is missing end node", EntityId = edge.Id.ToString(), EntityType = "Edge" }); } } // Convert to DTOs var nodeDtos = validNodes.Select(n => new NodeDto { Id = n.Id, LevelId = n.LevelId, NodeId = n.NodeId, NodeName = n.NodeName, NodeDescription = n.NodeDescription, MapId = n.MapId, X = n.X, Y = n.Y, VehicleProperties = [.. n.VehicleProperties .Where(vp => vp.VehicleTypeId == vehicleTypeId) .Select(vp => new NodeVehiclePropertyDto { Id = vp.Id, NodeId = vp.NodeId, VehicleTypeId = vp.VehicleTypeId, Theta = vp.Theta, Actions = vp.Actions })] }).ToList(); var edgeDtos = validEdges.Select(e => new EdgeDto { Id = e.Id, LevelId = e.LevelId, EdgeId = e.EdgeId, EdgeName = e.EdgeName, EdgeDescription = e.EdgeDescription, StartNodeId = e.StartNodeId, EndNodeId = e.EndNodeId, StartNode = nodeDtos.FirstOrDefault(n => n.Id == e.StartNodeId), EndNode = nodeDtos.FirstOrDefault(n => n.Id == e.EndNodeId), VehicleProperties = [.. e.VehicleProperties .Where(vp => vp.VehicleTypeId == vehicleTypeId) .Select(vp => new EdgeVehiclePropertyDto { Id = vp.Id, EdgeId = vp.EdgeId, VehicleTypeId = vp.VehicleTypeId, VehicleOrientation = vp.VehicleOrientation, OrientationType = vp.OrientationType, RotationAllowed = vp.RotationAllowed, RotationAtStartNodeAllowed = vp.RotationAtStartNodeAllowed, RotationAtEndNodeAllowed = vp.RotationAtEndNodeAllowed, MaxSpeed = vp.MaxSpeed, MaxRotationSpeed = vp.MaxRotationSpeed, MinHeight = vp.MinHeight, MaxHeight = vp.MaxHeight, LoadRestriction = null, Actions = vp.Actions })] }).ToList(); var result = new RobotModelMapDataDto { RobotModelId = robotModel.Id, RobotModelName = robotModel.ModelName, VehicleTypeId = vehicleTypeId, VehicleTypeName = vehicleType.VehicleTypeName, LevelId = levelId, LevelName = levelName, ValidNodes = nodeDtos, ValidEdges = edgeDtos, TotalNodesInLevel = totalNodesInLevel, TotalEdgesInLevel = totalEdgesInLevel, FilteredNodesCount = filteredNodes.Count, FilteredEdgesCount = filteredEdges.Count, ValidationResult = validationResult }; _logger.Info($"Validated map data for RobotModel '{robotModel.ModelName}': {validNodes.Count} valid nodes, {validEdges.Count} valid edges (removed {nodesRemoved} nodes, {edgesRemoved} edges)"); return result; } public async Task ValidateMapForRobotModelAsync(Guid robotModelId) { var mapData = await GetValidatedMapDataAsync(robotModelId); return mapData.ValidationResult; } public async Task HasValidMapConfigurationAsync(Guid robotModelId) { try { var robotModel = await _robotModelService.GetByIdAsync(robotModelId); if (robotModel == null) return false; if (!robotModel.VehicleTypeId.HasValue) return false; // Check if VehicleType exists and is active var vehicleType = await _vehicleTypeService.GetByIdAsync(robotModel.VehicleTypeId.Value); if (vehicleType == null || !vehicleType.IsActive) return false; // Validate map data var validationResult = await ValidateMapForRobotModelAsync(robotModelId); return validationResult.IsValid; } catch (Exception ex) { _logger.Error($"Error checking valid map configuration for RobotModel {robotModelId}: {ex.Message}"); return false; } } }