From f8798cd3d3af791264322505a8ed1fc8ab91513e Mon Sep 17 00:00:00 2001 From: Anton Martynenko Date: Sat, 1 Nov 2025 17:24:59 +0100 Subject: [PATCH] more complex route --- SatelliteProvider.IntegrationTests/Program.cs | 4 +- .../RouteTests.cs | 221 +++++++++++++++++- .../GoogleMapsDownloaderV2.cs | 102 +++++++- SatelliteProvider.Services/RegionService.cs | 40 +++- .../RouteProcessingService.cs | 117 ++++++---- SatelliteProvider.Services/RouteService.cs | 24 +- 6 files changed, 431 insertions(+), 77 deletions(-) diff --git a/SatelliteProvider.IntegrationTests/Program.cs b/SatelliteProvider.IntegrationTests/Program.cs index 9ee57e4..e9b8ec5 100644 --- a/SatelliteProvider.IntegrationTests/Program.cs +++ b/SatelliteProvider.IntegrationTests/Program.cs @@ -14,7 +14,7 @@ class Program using var httpClient = new HttpClient { BaseAddress = new Uri(apiUrl), - Timeout = TimeSpan.FromSeconds(60) + Timeout = TimeSpan.FromMinutes(15) }; try @@ -36,6 +36,8 @@ class Program await RouteTests.RunRouteWithRegionProcessingAndStitching(httpClient); + await RouteTests.RunComplexRouteWithStitching(httpClient); + Console.WriteLine(); Console.WriteLine("========================="); Console.WriteLine("All tests completed successfully!"); diff --git a/SatelliteProvider.IntegrationTests/RouteTests.cs b/SatelliteProvider.IntegrationTests/RouteTests.cs index dd82d9a..30038fe 100644 --- a/SatelliteProvider.IntegrationTests/RouteTests.cs +++ b/SatelliteProvider.IntegrationTests/RouteTests.cs @@ -181,7 +181,7 @@ public static class RouteTests } Console.WriteLine("Step 2: Waiting for route maps to be ready"); - Console.WriteLine(" (Service is processing regions and stitching automatically)"); + Console.WriteLine(" (Service is processing regions SEQUENTIALLY to avoid API throttling)"); Console.WriteLine(); RouteResponseModel? finalRoute = null; @@ -283,5 +283,224 @@ public static class RouteTests Console.WriteLine(); Console.WriteLine("✓ Route with Region Processing and Stitching Test: PASSED"); } + + public static async Task RunComplexRouteWithStitching(HttpClient httpClient) + { + Console.WriteLine(); + Console.WriteLine("Test: Complex Route with 10 Points - Region Processing and Stitching"); + Console.WriteLine("======================================================================="); + Console.WriteLine(); + + var routeId = Guid.NewGuid(); + var request = new CreateRouteRequest + { + Id = routeId, + Name = "Complex Route with 10 Points", + Description = "Test route with 10 action points for complex map stitching", + RegionSizeMeters = 300.0, + ZoomLevel = 18, + RequestMaps = true, + Points = new List + { + new() { Latitude = 48.276067180586544, Longitude = 37.38445758819581 }, + new() { Latitude = 48.27074009522731, Longitude = 37.374029159545906 }, + new() { Latitude = 48.263312668696855, Longitude = 37.37707614898682 }, + new() { Latitude = 48.26539817051818, Longitude = 37.36587524414063 }, + new() { Latitude = 48.25851283439989, Longitude = 37.35952377319337 }, + new() { Latitude = 48.254426906081555, Longitude = 37.374801635742195 }, + new() { Latitude = 48.25914140977405, Longitude = 37.39068031311036 }, + new() { Latitude = 48.25354110233028, Longitude = 37.401752471923835 }, + new() { Latitude = 48.25902712391726, Longitude = 37.416257858276374 }, + new() { Latitude = 48.26828345053738, Longitude = 37.402009963989265 } + } + }; + + Console.WriteLine("Step 1: Creating complex route with RequestMaps=true"); + Console.WriteLine($" Action Points: {request.Points.Count}"); + Console.WriteLine($" Region Size: {request.RegionSizeMeters}m"); + Console.WriteLine($" Zoom Level: {request.ZoomLevel}"); + Console.WriteLine($" Request Maps: {request.RequestMaps}"); + Console.WriteLine(); + Console.WriteLine("Route Path:"); + for (int i = 0; i < request.Points.Count; i++) + { + Console.WriteLine($" {i + 1}. ({request.Points[i].Latitude}, {request.Points[i].Longitude})"); + } + Console.WriteLine(); + + var routeResponse = await httpClient.PostAsJsonAsync("/api/satellite/route", request); + + if (!routeResponse.IsSuccessStatusCode) + { + var errorContent = await routeResponse.Content.ReadAsStringAsync(); + throw new Exception($"Route creation failed: {routeResponse.StatusCode} - {errorContent}"); + } + + var route = await routeResponse.Content.ReadFromJsonAsync(JsonOptions); + + if (route == null) + { + throw new Exception("No route data returned"); + } + + Console.WriteLine($"✓ Route created with {route.TotalPoints} total points"); + Console.WriteLine($" Distance: {route.TotalDistanceMeters:F2}m"); + Console.WriteLine($" Request Maps: {route.RequestMaps}"); + Console.WriteLine($" Maps Ready: {route.MapsReady}"); + Console.WriteLine(); + + var startPoints = route.Points.Count(p => p.PointType == "start"); + var endPoints = route.Points.Count(p => p.PointType == "end"); + var actionPoints = route.Points.Count(p => p.PointType == "action"); + var intermediatePoints = route.Points.Count(p => p.PointType == "intermediate"); + + Console.WriteLine("Point Type Distribution:"); + Console.WriteLine($" Start: {startPoints}"); + Console.WriteLine($" Action: {actionPoints}"); + Console.WriteLine($" Intermediate: {intermediatePoints}"); + Console.WriteLine($" End: {endPoints}"); + Console.WriteLine(); + + if (startPoints != 1) + { + throw new Exception($"Expected 1 start point, got {startPoints}"); + } + + if (endPoints != 1) + { + throw new Exception($"Expected 1 end point, got {endPoints}"); + } + + if (actionPoints != 8) + { + throw new Exception($"Expected 8 action points (excluding start/end), got {actionPoints}"); + } + + if (!route.RequestMaps) + { + throw new Exception("Expected RequestMaps to be true"); + } + + if (route.MapsReady) + { + throw new Exception("Expected MapsReady to be false initially"); + } + + Console.WriteLine("Step 2: Waiting for complex route maps to be ready"); + Console.WriteLine(" (Processing 56 regions SEQUENTIALLY to avoid API throttling)"); + Console.WriteLine(" (This will take several minutes as each region is processed one at a time)"); + Console.WriteLine(); + + RouteResponseModel? finalRoute = null; + int maxAttempts = 240; + int pollInterval = 3000; + + for (int attempt = 0; attempt < maxAttempts; attempt++) + { + await Task.Delay(pollInterval); + + var getResponse = await httpClient.GetAsync($"/api/satellite/route/{routeId}"); + + if (!getResponse.IsSuccessStatusCode) + { + throw new Exception($"Failed to get route status: {getResponse.StatusCode}"); + } + + var currentRoute = await getResponse.Content.ReadFromJsonAsync(JsonOptions); + + if (currentRoute == null) + { + throw new Exception("No route returned"); + } + + if (currentRoute.MapsReady) + { + finalRoute = currentRoute; + Console.WriteLine($"✓ Complex route maps ready in approximately {attempt * pollInterval / 1000}s"); + break; + } + + if (attempt % 10 == 0) + { + Console.WriteLine($" Waiting... (attempt {attempt + 1}/{maxAttempts})"); + } + + if (attempt == maxAttempts - 1) + { + throw new Exception($"Timeout: Complex route maps did not become ready in {maxAttempts * pollInterval / 1000}s"); + } + } + + if (finalRoute == null) + { + throw new Exception("Final route is null"); + } + + Console.WriteLine(); + + Console.WriteLine("Step 3: Verifying generated files"); + + if (string.IsNullOrEmpty(finalRoute.CsvFilePath)) + { + throw new Exception("CSV file path is null or empty"); + } + + if (string.IsNullOrEmpty(finalRoute.SummaryFilePath)) + { + throw new Exception("Summary file path is null or empty"); + } + + if (string.IsNullOrEmpty(finalRoute.StitchedImagePath)) + { + throw new Exception("Stitched image path is null or empty"); + } + + if (!File.Exists(finalRoute.CsvFilePath)) + { + throw new Exception($"CSV file not found: {finalRoute.CsvFilePath}"); + } + + if (!File.Exists(finalRoute.SummaryFilePath)) + { + throw new Exception($"Summary file not found: {finalRoute.SummaryFilePath}"); + } + + if (!File.Exists(finalRoute.StitchedImagePath)) + { + throw new Exception($"Stitched image not found: {finalRoute.StitchedImagePath}"); + } + + var csvLines = await File.ReadAllLinesAsync(finalRoute.CsvFilePath); + var uniqueTileCount = csvLines.Length - 1; + + var stitchedInfo = new FileInfo(finalRoute.StitchedImagePath); + + Console.WriteLine("Files Generated:"); + Console.WriteLine($" ✓ CSV: {Path.GetFileName(finalRoute.CsvFilePath)} ({uniqueTileCount} tiles)"); + Console.WriteLine($" ✓ Summary: {Path.GetFileName(finalRoute.SummaryFilePath)}"); + Console.WriteLine($" ✓ Stitched Map: {Path.GetFileName(finalRoute.StitchedImagePath)} ({stitchedInfo.Length / 1024:F2} KB)"); + Console.WriteLine(); + + Console.WriteLine("Route Summary:"); + Console.WriteLine($" Route ID: {finalRoute.Id}"); + Console.WriteLine($" Total Points: {finalRoute.TotalPoints}"); + Console.WriteLine($" Action Points: {actionPoints}"); + Console.WriteLine($" Distance: {finalRoute.TotalDistanceMeters:F2}m"); + Console.WriteLine($" Unique Tiles: {uniqueTileCount}"); + Console.WriteLine($" Maps Ready: {finalRoute.MapsReady}"); + Console.WriteLine(); + + if (uniqueTileCount < 10) + { + throw new Exception($"Expected at least 10 unique tiles for complex route, got {uniqueTileCount}"); + } + + if (stitchedInfo.Length < 1024) + { + throw new Exception($"Stitched image seems too small: {stitchedInfo.Length} bytes"); + } + + Console.WriteLine("✓ Complex Route with 10 Points Test: PASSED"); + } } diff --git a/SatelliteProvider.Services/GoogleMapsDownloaderV2.cs b/SatelliteProvider.Services/GoogleMapsDownloaderV2.cs index e1fd5c3..34d02ee 100644 --- a/SatelliteProvider.Services/GoogleMapsDownloaderV2.cs +++ b/SatelliteProvider.Services/GoogleMapsDownloaderV2.cs @@ -52,13 +52,31 @@ public class GoogleMapsDownloaderV2 { var str = JsonConvert.SerializeObject(new { mapType = "satellite" }); var response = await httpClient.PostAsync(url, new StringContent(str)); + + if (!response.IsSuccessStatusCode) + { + var errorBody = await response.Content.ReadAsStringAsync(); + _logger.LogError("Failed to get session token. Status: {StatusCode}, Response: {Response}", + response.StatusCode, errorBody); + } + response.EnsureSuccessStatusCode(); var sessionResponse = await response.Content.ReadFromJsonAsync(); return sessionResponse?.Session; } + catch (TaskCanceledException ex) + { + _logger.LogError(ex, "Session token request cancelled or timed out"); + throw; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP request failed while getting session token. StatusCode: {StatusCode}", ex.StatusCode); + throw; + } catch (Exception e) { - _logger.LogError(e, "Failed to get session token"); + _logger.LogError(e, "Unexpected error getting session token"); throw; } } @@ -92,6 +110,14 @@ public class GoogleMapsDownloaderV2 httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT); var response = await httpClient.GetAsync(url, token); + + if (!response.IsSuccessStatusCode) + { + var errorBody = await response.Content.ReadAsStringAsync(token); + _logger.LogError("Single tile download failed. Tile: ({X}, {Y}), Status: {StatusCode}, Response: {Response}", + tileX, tileY, response.StatusCode, errorBody); + } + response.EnsureSuccessStatusCode(); return await response.Content.ReadAsByteArrayAsync(token); @@ -127,6 +153,7 @@ public class GoogleMapsDownloaderV2 { int attempt = 0; int delay = BASE_RETRY_DELAY_SECONDS; + Exception? lastException = null; while (attempt < maxRetries) { @@ -134,34 +161,68 @@ public class GoogleMapsDownloaderV2 { return await action(); } + catch (TaskCanceledException ex) + { + _logger.LogError(ex, "Request was cancelled (timeout or explicit cancellation). Attempt {Attempt}/{Max}", attempt + 1, maxRetries); + throw; + } + catch (OperationCanceledException ex) + { + _logger.LogError(ex, "Operation was cancelled. Attempt {Attempt}/{Max}", attempt + 1, maxRetries); + throw; + } catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests || ex.StatusCode == (HttpStatusCode)429) { attempt++; + lastException = ex; + if (attempt >= maxRetries) { - _logger.LogError("Rate limit exceeded after {Attempts} attempts", maxRetries); - throw new RateLimitException($"Rate limit exceeded after {maxRetries} attempts"); + _logger.LogError(ex, "Rate limit (429) exceeded after {Attempts} attempts. This indicates Google Maps API throttling.", maxRetries); + throw new RateLimitException($"Rate limit exceeded after {maxRetries} attempts. Google Maps API is throttling requests."); } delay = Math.Min(delay * 2, MAX_RETRY_DELAY_SECONDS); - _logger.LogWarning("Rate limited. Waiting {Delay}s before retry {Attempt}/{Max}", delay, attempt, maxRetries); + _logger.LogWarning("Rate limited (429 Too Many Requests). Waiting {Delay}s before retry {Attempt}/{Max}", delay, attempt, maxRetries); await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken); } + catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) + { + _logger.LogError(ex, "Access forbidden (403). Check API key validity and permissions."); + throw; + } + catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) + { + _logger.LogError(ex, "Unauthorized (401). API key is invalid or missing."); + throw; + } catch (HttpRequestException ex) when (ex.StatusCode >= HttpStatusCode.InternalServerError) { attempt++; + lastException = ex; + if (attempt >= maxRetries) { - _logger.LogError(ex, "Server error after {Attempts} attempts", maxRetries); + _logger.LogError(ex, "Server error ({StatusCode}) after {Attempts} attempts", ex.StatusCode, maxRetries); throw; } delay = Math.Min(delay * 2, MAX_RETRY_DELAY_SECONDS); - _logger.LogWarning("Server error. Waiting {Delay}s before retry {Attempt}/{Max}", delay, attempt, maxRetries); + _logger.LogWarning("Server error ({StatusCode}). Waiting {Delay}s before retry {Attempt}/{Max}", ex.StatusCode, delay, attempt, maxRetries); await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken); } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP request failed with status {StatusCode}. Message: {Message}", ex.StatusCode, ex.Message); + throw; + } } + if (lastException != null) + { + throw new InvalidOperationException($"Retry logic exhausted after {maxRetries} attempts", lastException); + } + throw new InvalidOperationException("Retry logic failed unexpectedly"); } @@ -229,6 +290,14 @@ public class GoogleMapsDownloaderV2 httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT); var response = await httpClient.GetAsync(url, token); + + if (!response.IsSuccessStatusCode) + { + var errorBody = await response.Content.ReadAsStringAsync(token); + _logger.LogError("Tile download failed. Tile: ({X}, {Y}), Status: {StatusCode}, URL: {Url}, Response: {Response}", + x, y, response.StatusCode, url, errorBody); + } + response.EnsureSuccessStatusCode(); return await response.Content.ReadAsByteArrayAsync(token); @@ -242,14 +311,31 @@ public class GoogleMapsDownloaderV2 downloadedTiles.Add(new DownloadedTileInfoV2( x, y, zoomLevel, tileCenter.Lat, tileCenter.Lon, filePath, tileSizeMeters)); } + catch (TaskCanceledException ex) + { + _logger.LogError(ex, "Tile download cancelled for ({X}, {Y}). This may be due to HttpClient timeout or explicit cancellation.", x, y); + throw; + } + catch (OperationCanceledException ex) + { + _logger.LogError(ex, "Tile download operation cancelled for ({X}, {Y})", x, y); + throw; + } catch (RateLimitException ex) { - _logger.LogError(ex, "Rate limit exceeded for tile ({X}, {Y})", x, y); + _logger.LogError(ex, "Rate limit exceeded for tile ({X}, {Y}). Google Maps API is throttling requests. Consider reducing concurrent requests or adding delays.", x, y); + throw; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP request failed for tile ({X}, {Y}). StatusCode: {StatusCode}, Message: {Message}", + x, y, ex.StatusCode, ex.Message); throw; } catch (Exception ex) { - _logger.LogError(ex, "Failed to download tile ({X}, {Y})", x, y); + _logger.LogError(ex, "Unexpected error downloading tile ({X}, {Y}). Type: {ExceptionType}, Message: {Message}", + x, y, ex.GetType().Name, ex.Message); throw; } } diff --git a/SatelliteProvider.Services/RegionService.cs b/SatelliteProvider.Services/RegionService.cs index 7614d7b..f56fa66 100644 --- a/SatelliteProvider.Services/RegionService.cs +++ b/SatelliteProvider.Services/RegionService.cs @@ -164,28 +164,54 @@ public class RegionService : IRegionService var duration = (DateTime.UtcNow - startTime).TotalSeconds; _logger.LogInformation("Region {RegionId} processing completed in {Duration:F2}s", id, duration); } - catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested) + catch (TaskCanceledException ex) when (timeoutCts.IsCancellationRequested) { errorMessage = "Processing timed out after 5 minutes. Unable to download tiles within the time limit."; - _logger.LogError("Region {RegionId} processing timed out after 5 minutes", id); + _logger.LogError(ex, "Region {RegionId} processing timed out after 5 minutes", id); + await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage); + } + catch (TaskCanceledException ex) when (cancellationToken.IsCancellationRequested) + { + errorMessage = "Processing was cancelled externally (likely application shutdown)."; + _logger.LogError(ex, "Region {RegionId} processing was cancelled externally", id); + await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage); + } + catch (TaskCanceledException ex) + { + errorMessage = $"Request cancelled or timed out: {ex.Message}. This may indicate HttpClient timeout or network issues."; + _logger.LogError(ex, "Region {RegionId} processing was cancelled (TaskCanceledException)", id); + await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage); + } + catch (OperationCanceledException ex) when (timeoutCts.IsCancellationRequested) + { + errorMessage = "Processing timed out after 5 minutes. Unable to download tiles within the time limit."; + _logger.LogError(ex, "Region {RegionId} processing timed out after 5 minutes", id); + await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage); + } + catch (OperationCanceledException ex) + { + errorMessage = $"Operation cancelled: {ex.Message}"; + _logger.LogError(ex, "Region {RegionId} processing was cancelled", id); await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage); } catch (RateLimitException ex) { errorMessage = $"Rate limit exceeded: {ex.Message}. Google Maps API rate limit was reached and retries were exhausted."; - _logger.LogError(ex, "Rate limit exceeded for region {RegionId}", id); + _logger.LogError(ex, "Rate limit exceeded for region {RegionId}. Google is throttling requests. Consider reducing request rate.", id); await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage); } catch (HttpRequestException ex) { - errorMessage = $"Network error: {ex.Message}. Failed to download tiles from Google Maps."; - _logger.LogError(ex, "Network error processing region {RegionId}", id); + errorMessage = $"Network error (HTTP {ex.StatusCode}): {ex.Message}. Failed to download tiles from Google Maps."; + _logger.LogError(ex, "Network error processing region {RegionId}. StatusCode: {StatusCode}, Message: {Message}", + id, ex.StatusCode, ex.Message); await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage); } catch (Exception ex) { - errorMessage = $"Unexpected error: {ex.Message}"; - _logger.LogError(ex, "Failed to process region {RegionId}: {Message}", id, ex.Message); + errorMessage = $"Unexpected error ({ex.GetType().Name}): {ex.Message}"; + _logger.LogError(ex, "Failed to process region {RegionId}. Type: {ExceptionType}, Message: {Message}", + id, ex.GetType().Name, ex.Message); await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage); } } diff --git a/SatelliteProvider.Services/RouteProcessingService.cs b/SatelliteProvider.Services/RouteProcessingService.cs index 4acf64e..0bc4b86 100644 --- a/SatelliteProvider.Services/RouteProcessingService.cs +++ b/SatelliteProvider.Services/RouteProcessingService.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -14,18 +15,21 @@ public class RouteProcessingService : BackgroundService { private readonly IRouteRepository _routeRepository; private readonly IRegionRepository _regionRepository; + private readonly IServiceProvider _serviceProvider; private readonly StorageConfig _storageConfig; private readonly ILogger _logger; - private readonly TimeSpan _checkInterval = TimeSpan.FromSeconds(10); + private readonly TimeSpan _checkInterval = TimeSpan.FromSeconds(5); public RouteProcessingService( IRouteRepository routeRepository, IRegionRepository regionRepository, + IServiceProvider serviceProvider, IOptions storageConfig, ILogger logger) { _routeRepository = routeRepository; _regionRepository = regionRepository; + _serviceProvider = serviceProvider; _storageConfig = storageConfig.Value; _logger = logger; } @@ -62,7 +66,7 @@ public class RouteProcessingService : BackgroundService try { - await ProcessRouteIfReadyAsync(route.Id, cancellationToken); + await ProcessRouteSequentiallyAsync(route.Id, cancellationToken); } catch (Exception ex) { @@ -77,7 +81,7 @@ public class RouteProcessingService : BackgroundService return routes.Select(r => (r.Id, r.RequestMaps)).ToList(); } - private async Task ProcessRouteIfReadyAsync(Guid routeId, CancellationToken cancellationToken) + private async Task ProcessRouteSequentiallyAsync(Guid routeId, CancellationToken cancellationToken) { var route = await _routeRepository.GetByIdAsync(routeId); if (route == null || !route.RequestMaps || route.MapsReady) @@ -85,49 +89,84 @@ public class RouteProcessingService : BackgroundService return; } - var regionIds = await _routeRepository.GetRegionIdsByRouteAsync(routeId); - if (!regionIds.Any()) + var routePointsList = (await _routeRepository.GetRoutePointsAsync(routeId)).ToList(); + var regionIdsList = (await _routeRepository.GetRegionIdsByRouteAsync(routeId)).ToList(); + + if (regionIdsList.Count == 0) { - _logger.LogWarning("Route {RouteId} has no regions linked", routeId); + using var scope = _serviceProvider.CreateScope(); + var regionService = scope.ServiceProvider.GetRequiredService(); + + _logger.LogInformation("Route {RouteId}: Starting sequential region processing for {PointCount} points", + routeId, routePointsList.Count); + + var firstPoint = routePointsList.First(); + var regionId = Guid.NewGuid(); + + await regionService.RequestRegionAsync( + regionId, + firstPoint.Latitude, + firstPoint.Longitude, + route.RegionSizeMeters, + route.ZoomLevel, + stitchTiles: false); + + await _routeRepository.LinkRouteToRegionAsync(routeId, regionId); + + _logger.LogInformation("Route {RouteId}: Queued first region {RegionId} (1/{TotalPoints})", + routeId, regionId, routePointsList.Count); return; } - - var allCompleted = true; - var anyFailed = false; - - foreach (var regionId in regionIds) - { - var region = await _regionRepository.GetByIdAsync(regionId); - if (region == null) - { - _logger.LogWarning("Region {RegionId} not found for route {RouteId}", regionId, routeId); - continue; - } - - if (region.Status == "failed") - { - anyFailed = true; - } - else if (region.Status != "completed") - { - allCompleted = false; - } - } - - if (!allCompleted) + + var lastRegion = await _regionRepository.GetByIdAsync(regionIdsList.Last()); + if (lastRegion == null) { return; } - - if (anyFailed) + + if (lastRegion.Status == "queued" || lastRegion.Status == "processing") { - _logger.LogWarning("Route {RouteId} has failed regions, skipping processing", routeId); return; } - - _logger.LogInformation("All regions completed for route {RouteId}, starting final processing", routeId); - - await GenerateRouteMapsAsync(routeId, route, regionIds, cancellationToken); + + if (lastRegion.Status == "failed") + { + _logger.LogError("Route {RouteId}: Region {RegionId} failed. Stopping route processing.", + routeId, lastRegion.Id); + + route.MapsReady = false; + route.UpdatedAt = DateTime.UtcNow; + await _routeRepository.UpdateRouteAsync(route); + return; + } + + if (regionIdsList.Count < routePointsList.Count) + { + using var scope = _serviceProvider.CreateScope(); + var regionService = scope.ServiceProvider.GetRequiredService(); + + var nextPoint = routePointsList[regionIdsList.Count]; + var regionId = Guid.NewGuid(); + + await regionService.RequestRegionAsync( + regionId, + nextPoint.Latitude, + nextPoint.Longitude, + route.RegionSizeMeters, + route.ZoomLevel, + stitchTiles: false); + + await _routeRepository.LinkRouteToRegionAsync(routeId, regionId); + + _logger.LogInformation("Route {RouteId}: Queued next region {RegionId} ({CurrentRegion}/{TotalPoints})", + routeId, regionId, regionIdsList.Count + 1, routePointsList.Count); + return; + } + + _logger.LogInformation("Route {RouteId}: All {RegionCount} regions completed, generating final maps", + routeId, regionIdsList.Count); + + await GenerateRouteMapsAsync(routeId, route, regionIdsList, cancellationToken); } private async Task GenerateRouteMapsAsync( @@ -353,8 +392,8 @@ public class RouteProcessingService : BackgroundService if (tileImage.Width != tileSizePixels || tileImage.Height != tileSizePixels) { - _logger.LogWarning("Tile {FilePath} has wrong size {Width}x{Height}, expected {Expected}x{Expected}", - tile.FilePath, tileImage.Width, tileImage.Height, tileSizePixels); + _logger.LogWarning("Tile {FilePath} has wrong size {Width}x{Height}, expected {ExpectedWidth}x{ExpectedHeight}", + tile.FilePath, tileImage.Width, tileImage.Height, tileSizePixels, tileSizePixels); } stitchedImage.Mutate(ctx => ctx.DrawImage(tileImage, new Point(destX, destY), 1f)); diff --git a/SatelliteProvider.Services/RouteService.cs b/SatelliteProvider.Services/RouteService.cs index 6a757bf..ca0bd86 100644 --- a/SatelliteProvider.Services/RouteService.cs +++ b/SatelliteProvider.Services/RouteService.cs @@ -65,7 +65,7 @@ public class RouteService : IRouteService totalDistance += distanceFromPrevious.Value; } - var pointType = isStart ? "start" : (isEnd ? "end" : "waypoint"); + var pointType = isStart ? "start" : (isEnd ? "end" : "action"); allPoints.Add(new RoutePointDto { @@ -147,26 +147,8 @@ public class RouteService : IRouteService if (request.RequestMaps) { - _logger.LogInformation("Route {RouteId}: Requesting regions for all {Count} points", - request.Id, allPoints.Count); - - foreach (var point in allPoints) - { - var regionId = Guid.NewGuid(); - - await _regionService.RequestRegionAsync( - regionId, - point.Latitude, - point.Longitude, - request.RegionSizeMeters, - request.ZoomLevel, - stitchTiles: false); - - await _routeRepository.LinkRouteToRegionAsync(request.Id, regionId); - } - - _logger.LogInformation("Route {RouteId}: {Count} regions requested", - request.Id, allPoints.Count); + _logger.LogInformation("Route {RouteId}: Maps requested. Regions will be processed sequentially by background service.", + request.Id); } _logger.LogInformation("Route {RouteId} created successfully", request.Id);