Spaces:
Paused
Paused
| using Application.Abstractions.Interfaces; | |
| using Application.DTOs.OrderDTOs; | |
| using Entities.Enums; | |
| using Entities.Interfaces; | |
| using Entities.Models; | |
| using Microsoft.Extensions.Logging; | |
| namespace Application.Services | |
| { | |
| public class RoutingService : IRoutingService | |
| { | |
| private readonly INodeRepository _nodeRepository; | |
| private readonly ILogger<RoutingService> _logger; | |
| public RoutingService(INodeRepository nodeRepository, ILogger<RoutingService> logger) | |
| { | |
| _nodeRepository = nodeRepository; | |
| _logger = logger; | |
| } | |
| public async Task<(bool CanComplete, List<RouteSegmentDTO> Route, double TotalDistance, double BatteryUsage, List<string> Logs)> CalculateDroneRouteAsync( | |
| Robot drone, | |
| Node currentNode, | |
| Node pickupNode, | |
| Node dropoffNode, | |
| double packageWeight) | |
| { | |
| var logs = new List<string>(); | |
| void Log(string message) | |
| { | |
| logs.Add(message); | |
| _logger.LogInformation(message); | |
| } | |
| Log($"Routing calculation started for drone {drone.Id} ({drone.Name})"); | |
| Log($"Origin: {currentNode.Name}, Pickup: {pickupNode.Name}, Dropoff: {dropoffNode.Name}"); | |
| Log($"Package Weight: {packageWeight}kg"); | |
| var route = new List<RouteSegmentDTO>(); | |
| double totalDistance = 0; | |
| int segmentNumber = 1; | |
| // Calculate battery metrics | |
| double maxRange = drone.MaxFlightRangeMeters; | |
| double metersPerBatteryPercentEmpty = maxRange / 100.0; | |
| double percentPerSecondHover = (drone.HoverConsumptionPerSecondJoules / drone.BatteryCapacityJoules) * 100.0; | |
| double actionDurationSeconds = 30.0; // Assume 30 seconds for loading/unloading | |
| double batteryUsagePerAction = percentPerSecondHover * actionDurationSeconds; | |
| Log($"Drone {drone.Id} specs: Battery={drone.BatteryLevel}%, MaxRange={Math.Round(maxRange, 0)}m, HoverUsage={Math.Round(percentPerSecondHover, 4)}%/s"); | |
| Log($"Action Cost: {Math.Round(batteryUsagePerAction, 2)}% per pickup/delivery"); | |
| // Step 1: Calculate route from current position to pickup (without package) | |
| Log("--- PHASE 1: TRAVEL TO PICKUP ---"); | |
| var (canReachPickup, routeToPickup, distanceToPickup, batteryToPickup) = await CalculateRouteSegmentWithCharging( | |
| currentNode, | |
| pickupNode, | |
| drone.BatteryLevel, | |
| metersPerBatteryPercentEmpty, | |
| segmentNumber, | |
| "Travel to Pickup", | |
| logs | |
| ); | |
| if (!canReachPickup) | |
| { | |
| Log("FAILURE: Could not establish path to pickup node."); | |
| return (false, null, 0, 0, logs); | |
| } | |
| route.AddRange(routeToPickup); | |
| totalDistance += distanceToPickup; | |
| segmentNumber += routeToPickup.Count; | |
| // Pickup package action | |
| route.Add(new RouteSegmentDTO | |
| { | |
| SegmentNumber = segmentNumber++, | |
| FromNodeName = pickupNode.Name, | |
| ToNodeName = pickupNode.Name, | |
| FromLatitude = pickupNode.Latitude, | |
| FromLongitude = pickupNode.Longitude, | |
| ToLatitude = pickupNode.Latitude, | |
| ToLongitude = pickupNode.Longitude, | |
| DistanceMeters = 0, | |
| Action = "Pickup" | |
| }); | |
| // Calculate remaining battery after reaching pickup | |
| double remainingBatteryAfterPickup = drone.BatteryLevel - batteryToPickup; | |
| // If we charged during route, battery is 100% minus last segment usage | |
| if (routeToPickup.Any(r => r.Action == "Charge")) | |
| { | |
| var lastChargeIndex = routeToPickup.FindLastIndex(r => r.Action == "Charge"); | |
| double distanceAfterLastCharge = routeToPickup.Skip(lastChargeIndex + 1).Sum(r => r.DistanceMeters); | |
| remainingBatteryAfterPickup = 100 - (distanceAfterLastCharge / metersPerBatteryPercentEmpty); | |
| } | |
| Log($"Arrival at pickup. Battery remaining before loading: {Math.Round(remainingBatteryAfterPickup, 1)}%"); | |
| remainingBatteryAfterPickup -= batteryUsagePerAction; | |
| Log($"Battery after loading: {Math.Round(remainingBatteryAfterPickup, 1)}%"); | |
| // Step 2: Calculate route from pickup to dropoff WITH package weight | |
| Log("--- PHASE 2: DELIVERY TO DROPOFF ---"); | |
| double weightFactor = 1.0 / (1.0 + (packageWeight * 0.1)); | |
| weightFactor = Math.Max(0.2, weightFactor); | |
| double metersPerBatteryPercentWithLoad = metersPerBatteryPercentEmpty * weightFactor; | |
| Log($"Applied weight factor {Math.Round(weightFactor, 2)}x for {packageWeight}kg load. New efficiency: {Math.Round(metersPerBatteryPercentWithLoad, 1)}m/%"); | |
| var (canReachDropoff, routeToDropoff, distanceToDropoff, batteryToDropoff) = await CalculateRouteSegmentWithCharging( | |
| pickupNode, | |
| dropoffNode, | |
| remainingBatteryAfterPickup, | |
| metersPerBatteryPercentWithLoad, | |
| segmentNumber, | |
| "Travel", | |
| logs | |
| ); | |
| if (!canReachDropoff) | |
| { | |
| Log("FAILURE: Could not establish path to dropoff node with current payload."); | |
| return (false, null, 0, 0, logs); | |
| } | |
| route.AddRange(routeToDropoff); | |
| totalDistance += distanceToDropoff; | |
| segmentNumber += routeToDropoff.Count; | |
| // Delivery action | |
| route.Add(new RouteSegmentDTO | |
| { | |
| SegmentNumber = segmentNumber++, | |
| FromNodeName = dropoffNode.Name, | |
| ToNodeName = dropoffNode.Name, | |
| FromLatitude = dropoffNode.Latitude, | |
| FromLongitude = dropoffNode.Longitude, | |
| ToLatitude = dropoffNode.Latitude, | |
| ToLongitude = dropoffNode.Longitude, | |
| DistanceMeters = 0, | |
| Action = "Deliver" | |
| }); | |
| // Step 3: Calculate return route to nearest charging station after delivery | |
| Log("--- PHASE 3: RETURN TO BASE ---"); | |
| double batteryAfterDropoff = remainingBatteryAfterPickup - batteryToDropoff - batteryUsagePerAction; | |
| // Search for nearest charging station from dropoff | |
| var nearestStation = await _nodeRepository.FindNearestNodeAsync( | |
| dropoffNode.Latitude, | |
| dropoffNode.Longitude, | |
| NodeType.ChargingStation | |
| ); | |
| if (nearestStation != null) | |
| { | |
| var (canReturn, returnRoute, returnDistance, returnBattery) = await CalculateRouteSegmentWithCharging( | |
| dropoffNode, | |
| nearestStation, | |
| batteryAfterDropoff, | |
| metersPerBatteryPercentEmpty, // Return empty | |
| segmentNumber, | |
| "Return to Base", | |
| logs | |
| ); | |
| if (canReturn) | |
| { | |
| route.AddRange(returnRoute); | |
| totalDistance += returnDistance; | |
| // Final charge action at the end | |
| route.Add(new RouteSegmentDTO | |
| { | |
| SegmentNumber = segmentNumber + returnRoute.Count, | |
| FromNodeName = nearestStation.Name, | |
| ToNodeName = nearestStation.Name, | |
| FromLatitude = nearestStation.Latitude, | |
| FromLongitude = nearestStation.Longitude, | |
| ToLatitude = nearestStation.Latitude, | |
| ToLongitude = nearestStation.Longitude, | |
| DistanceMeters = 0, | |
| Action = "Charge" | |
| }); | |
| Log($"Added return trip to {nearestStation.Name}. Final Battery: {Math.Round(batteryAfterDropoff - returnBattery, 1)}%"); | |
| } | |
| else | |
| { | |
| Log("WARNING: Cannot guarantee safe return to charging station after delivery!"); | |
| } | |
| } | |
| double totalBatteryUsage = batteryToPickup + batteryToDropoff + (batteryUsagePerAction * 2); // Pickup + Deliver | |
| Log($"SUCCESS: Full route planned. Total Distance: {Math.Round(totalDistance, 0)}m. Estimated battery usage: {Math.Round(totalBatteryUsage, 1)}%"); | |
| return (true, route, totalDistance, totalBatteryUsage, logs); | |
| } | |
| private async Task<(bool canComplete, List<RouteSegmentDTO> route, double distance, double batteryUsed)> CalculateRouteSegmentWithCharging( | |
| Node fromNode, | |
| Node toNode, | |
| double currentBattery, | |
| double metersPerBatteryPercent, | |
| int startSegmentNumber, | |
| string travelAction, | |
| List<string> logs) | |
| { | |
| void Log(string message) => logs.Add(message); | |
| var route = new List<RouteSegmentDTO>(); | |
| double totalDistance = 0; | |
| double totalBatteryUsed = 0; | |
| int segmentNumber = startSegmentNumber; | |
| // If we're starting at a charging station, we can top up to 100% first | |
| if (fromNode.Type == NodeType.ChargingStation && currentBattery < 99) | |
| { | |
| Log($"Station {fromNode.Name} is a charging hub. Topping up from {Math.Round(currentBattery, 1)}% to 100%."); | |
| route.Add(new RouteSegmentDTO | |
| { | |
| SegmentNumber = segmentNumber++, | |
| FromNodeName = fromNode.Name, | |
| ToNodeName = fromNode.Name, | |
| FromLatitude = fromNode.Latitude, | |
| FromLongitude = fromNode.Longitude, | |
| ToLatitude = fromNode.Latitude, | |
| ToLongitude = fromNode.Longitude, | |
| DistanceMeters = 0, | |
| Action = "Charge" | |
| }); | |
| currentBattery = 100; | |
| } | |
| double distanceToDestination = CalculateDistance(fromNode, toNode); | |
| double requiredBattery = distanceToDestination / metersPerBatteryPercent; | |
| Log($"Segment Planning: {fromNode.Name} -> {toNode.Name}. Distance: {Math.Round(distanceToDestination, 0)}m, Battery needed: {Math.Round(requiredBattery, 1)}%, Current: {Math.Round(currentBattery, 1)}%"); | |
| // Check if can reach destination directly with safety margin | |
| if (requiredBattery <= currentBattery) | |
| { | |
| double remainingBatteryAtDestination = currentBattery - requiredBattery; | |
| // Safety check: Can the drone reach the nearest charging station FROM the destination? | |
| var nearestChargeFromDestination = await _nodeRepository.FindNearestNodeAsync( | |
| toNode.Latitude, | |
| toNode.Longitude, | |
| NodeType.ChargingStation, | |
| new[] { toNode.Id } | |
| ); | |
| double distanceToNearestCharge = nearestChargeFromDestination != null | |
| ? CalculateDistance(toNode, nearestChargeFromDestination) | |
| : 10000; | |
| double batteryNeededForSafety = distanceToNearestCharge / metersPerBatteryPercent; | |
| Log($"Safety Check at {toNode.Name}: Battery remaining {Math.Round(remainingBatteryAtDestination, 1)}%. Nearest charge is {Math.Round(distanceToNearestCharge, 0)}m away (needs {Math.Round(batteryNeededForSafety, 1)}% to reach)."); | |
| if (remainingBatteryAtDestination >= batteryNeededForSafety) | |
| { | |
| Log($"Direct path confirmed for {travelAction}. Safe to proceed."); | |
| route.Add(new RouteSegmentDTO | |
| { | |
| SegmentNumber = segmentNumber++, | |
| FromNodeName = fromNode.Name, | |
| ToNodeName = toNode.Name, | |
| FromLatitude = fromNode.Latitude, | |
| FromLongitude = fromNode.Longitude, | |
| ToLatitude = toNode.Latitude, | |
| ToLongitude = toNode.Longitude, | |
| DistanceMeters = distanceToDestination, | |
| Action = travelAction | |
| }); | |
| return (true, route, distanceToDestination, requiredBattery); | |
| } | |
| Log($"Insufficient safety margin! Remaining {Math.Round(remainingBatteryAtDestination, 1)}% < Needed {Math.Round(batteryNeededForSafety, 1)}%. Searching for intermediate charging station..."); | |
| } | |
| else | |
| { | |
| Log($"Cannot reach {toNode.Name} directly. Battery deficit of {Math.Round(requiredBattery - currentBattery, 1)}%. Searching for intermediate charging station..."); | |
| } | |
| // Get all charging stations | |
| var allChargingStations = await _nodeRepository.GetByTypeAsync(NodeType.ChargingStation); | |
| var chargingStationsList = allChargingStations.Where(s => s.Id != fromNode.Id && s.Id != toNode.Id).ToList(); | |
| if (!chargingStationsList.Any()) | |
| { | |
| Log("CRITICAL: No intermediate charging stations found in database!"); | |
| return (false, new List<RouteSegmentDTO>(), 0, 0); | |
| } | |
| Node? optimalStation = null; | |
| double minTotalRouteDistance = double.MaxValue; | |
| Log($"Evaluating {chargingStationsList.Count} charging hubs..."); | |
| foreach (var station in chargingStationsList) | |
| { | |
| double distanceToStation = CalculateDistance(fromNode, station); | |
| double batteryToStation = distanceToStation / metersPerBatteryPercent; | |
| if (batteryToStation <= currentBattery) | |
| { | |
| double distanceFromStationToDestination = CalculateDistance(station, toNode); | |
| double batteryFromStationToDestination = distanceFromStationToDestination / metersPerBatteryPercent; | |
| if (batteryFromStationToDestination <= 100) | |
| { | |
| var chargeFromDestination = await _nodeRepository.FindNearestNodeAsync( | |
| toNode.Latitude, | |
| toNode.Longitude, | |
| NodeType.ChargingStation, | |
| new[] { toNode.Id, station.Id } | |
| ); | |
| if (chargeFromDestination != null) | |
| { | |
| double distanceDestToCharge = CalculateDistance(toNode, chargeFromDestination); | |
| double batteryDestToCharge = distanceDestToCharge / metersPerBatteryPercent; | |
| double remainingBatteryAtDest = 100 - batteryFromStationToDestination; | |
| if (remainingBatteryAtDest >= batteryDestToCharge) | |
| { | |
| double totalRouteDistance = distanceToStation + distanceFromStationToDestination; | |
| if (totalRouteDistance < minTotalRouteDistance) | |
| { | |
| minTotalRouteDistance = totalRouteDistance; | |
| optimalStation = station; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| if (optimalStation == null) | |
| { | |
| Log($"FAIL: No reachable charging stations found that allow reaching {toNode.Name} with safety margin."); | |
| return (false, new List<RouteSegmentDTO>(), 0, 0); | |
| } | |
| Log($"Found hub: {optimalStation.Name} (+{Math.Round(CalculateDistance(fromNode, optimalStation), 0)}m). Rerouting..."); | |
| double distanceToOptimalStation = CalculateDistance(fromNode, optimalStation); | |
| double batteryToOptimalStation = distanceToOptimalStation / metersPerBatteryPercent; | |
| route.Add(new RouteSegmentDTO | |
| { | |
| SegmentNumber = segmentNumber++, | |
| FromNodeName = fromNode.Name, | |
| ToNodeName = optimalStation.Name, | |
| FromLatitude = fromNode.Latitude, | |
| FromLongitude = fromNode.Longitude, | |
| ToLatitude = optimalStation.Latitude, | |
| ToLongitude = optimalStation.Longitude, | |
| DistanceMeters = distanceToOptimalStation, | |
| Action = travelAction | |
| }); | |
| totalDistance += distanceToOptimalStation; | |
| totalBatteryUsed += batteryToOptimalStation; | |
| route.Add(new RouteSegmentDTO | |
| { | |
| SegmentNumber = segmentNumber++, | |
| FromNodeName = optimalStation.Name, | |
| ToNodeName = optimalStation.Name, | |
| FromLatitude = optimalStation.Latitude, | |
| FromLongitude = optimalStation.Longitude, | |
| ToLatitude = optimalStation.Latitude, | |
| ToLongitude = optimalStation.Longitude, | |
| DistanceMeters = 0, | |
| Action = "Charge" | |
| }); | |
| var (canCompleteSub, remainingRoute, remainingDistance, remainingBattery) = await CalculateRouteSegmentWithCharging( | |
| optimalStation, | |
| toNode, | |
| 100, | |
| metersPerBatteryPercent, | |
| segmentNumber, | |
| travelAction, | |
| logs | |
| ); | |
| if (!canCompleteSub) | |
| { | |
| return (false, new List<RouteSegmentDTO>(), 0, 0); | |
| } | |
| route.AddRange(remainingRoute); | |
| totalDistance += remainingDistance; | |
| totalBatteryUsed += remainingBattery; | |
| return (true, route, totalDistance, totalBatteryUsed); | |
| } | |
| public double CalculateDistance(Node node1, Node node2) | |
| { | |
| return CalculateDistance(node1.Latitude, node1.Longitude, node2.Latitude, node2.Longitude); | |
| } | |
| public double CalculateDistance(double lat1, double lon1, double lat2, double lon2) | |
| { | |
| // Haversine formula | |
| const double R = 6371000; // Earth radius in meters | |
| var dLat = ToRadians(lat2 - lat1); | |
| var dLon = ToRadians(lon2 - lon1); | |
| var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + | |
| Math.Cos(ToRadians(lat1)) * Math.Cos(ToRadians(lat2)) * | |
| Math.Sin(dLon / 2) * Math.Sin(dLon / 2); | |
| var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); | |
| return R * c; | |
| } | |
| private static double ToRadians(double degrees) | |
| { | |
| return degrees * Math.PI / 180.0; | |
| } | |
| } | |
| } | |