more complex route

This commit is contained in:
Anton Martynenko
2025-11-01 17:24:59 +01:00
parent 11395ec913
commit f8798cd3d3
6 changed files with 431 additions and 77 deletions
@@ -14,7 +14,7 @@ class Program
using var httpClient = new HttpClient using var httpClient = new HttpClient
{ {
BaseAddress = new Uri(apiUrl), BaseAddress = new Uri(apiUrl),
Timeout = TimeSpan.FromSeconds(60) Timeout = TimeSpan.FromMinutes(15)
}; };
try try
@@ -36,6 +36,8 @@ class Program
await RouteTests.RunRouteWithRegionProcessingAndStitching(httpClient); await RouteTests.RunRouteWithRegionProcessingAndStitching(httpClient);
await RouteTests.RunComplexRouteWithStitching(httpClient);
Console.WriteLine(); Console.WriteLine();
Console.WriteLine("========================="); Console.WriteLine("=========================");
Console.WriteLine("All tests completed successfully!"); Console.WriteLine("All tests completed successfully!");
@@ -181,7 +181,7 @@ public static class RouteTests
} }
Console.WriteLine("Step 2: Waiting for route maps to be ready"); 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(); Console.WriteLine();
RouteResponseModel? finalRoute = null; RouteResponseModel? finalRoute = null;
@@ -283,5 +283,224 @@ public static class RouteTests
Console.WriteLine(); Console.WriteLine();
Console.WriteLine("✓ Route with Region Processing and Stitching Test: PASSED"); 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<RoutePointInput>
{
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<RouteResponseModel>(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<RouteResponseModel>(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");
}
} }
@@ -52,13 +52,31 @@ public class GoogleMapsDownloaderV2
{ {
var str = JsonConvert.SerializeObject(new { mapType = "satellite" }); var str = JsonConvert.SerializeObject(new { mapType = "satellite" });
var response = await httpClient.PostAsync(url, new StringContent(str)); 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(); response.EnsureSuccessStatusCode();
var sessionResponse = await response.Content.ReadFromJsonAsync<SessionResponse>(); var sessionResponse = await response.Content.ReadFromJsonAsync<SessionResponse>();
return sessionResponse?.Session; 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) catch (Exception e)
{ {
_logger.LogError(e, "Failed to get session token"); _logger.LogError(e, "Unexpected error getting session token");
throw; throw;
} }
} }
@@ -92,6 +110,14 @@ public class GoogleMapsDownloaderV2
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT); httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT);
var response = await httpClient.GetAsync(url, token); 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(); response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync(token); return await response.Content.ReadAsByteArrayAsync(token);
@@ -127,6 +153,7 @@ public class GoogleMapsDownloaderV2
{ {
int attempt = 0; int attempt = 0;
int delay = BASE_RETRY_DELAY_SECONDS; int delay = BASE_RETRY_DELAY_SECONDS;
Exception? lastException = null;
while (attempt < maxRetries) while (attempt < maxRetries)
{ {
@@ -134,32 +161,66 @@ public class GoogleMapsDownloaderV2
{ {
return await action(); 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) catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests || ex.StatusCode == (HttpStatusCode)429)
{ {
attempt++; attempt++;
lastException = ex;
if (attempt >= maxRetries) if (attempt >= maxRetries)
{ {
_logger.LogError("Rate limit exceeded after {Attempts} attempts", maxRetries); _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"); throw new RateLimitException($"Rate limit exceeded after {maxRetries} attempts. Google Maps API is throttling requests.");
} }
delay = Math.Min(delay * 2, MAX_RETRY_DELAY_SECONDS); 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); 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) catch (HttpRequestException ex) when (ex.StatusCode >= HttpStatusCode.InternalServerError)
{ {
attempt++; attempt++;
lastException = ex;
if (attempt >= maxRetries) 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; throw;
} }
delay = Math.Min(delay * 2, MAX_RETRY_DELAY_SECONDS); 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); 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"); throw new InvalidOperationException("Retry logic failed unexpectedly");
@@ -229,6 +290,14 @@ public class GoogleMapsDownloaderV2
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT); httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT);
var response = await httpClient.GetAsync(url, token); 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(); response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync(token); return await response.Content.ReadAsByteArrayAsync(token);
@@ -242,14 +311,31 @@ public class GoogleMapsDownloaderV2
downloadedTiles.Add(new DownloadedTileInfoV2( downloadedTiles.Add(new DownloadedTileInfoV2(
x, y, zoomLevel, tileCenter.Lat, tileCenter.Lon, filePath, tileSizeMeters)); 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) 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; throw;
} }
catch (Exception ex) 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; throw;
} }
} }
+33 -7
View File
@@ -164,28 +164,54 @@ public class RegionService : IRegionService
var duration = (DateTime.UtcNow - startTime).TotalSeconds; var duration = (DateTime.UtcNow - startTime).TotalSeconds;
_logger.LogInformation("Region {RegionId} processing completed in {Duration:F2}s", id, duration); _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."; 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); await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
} }
catch (RateLimitException ex) catch (RateLimitException ex)
{ {
errorMessage = $"Rate limit exceeded: {ex.Message}. Google Maps API rate limit was reached and retries were exhausted."; 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); await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
} }
catch (HttpRequestException ex) catch (HttpRequestException ex)
{ {
errorMessage = $"Network error: {ex.Message}. Failed to download tiles from Google Maps."; errorMessage = $"Network error (HTTP {ex.StatusCode}): {ex.Message}. Failed to download tiles from Google Maps.";
_logger.LogError(ex, "Network error processing region {RegionId}", id); _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); await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
} }
catch (Exception ex) catch (Exception ex)
{ {
errorMessage = $"Unexpected error: {ex.Message}"; errorMessage = $"Unexpected error ({ex.GetType().Name}): {ex.Message}";
_logger.LogError(ex, "Failed to process region {RegionId}: {Message}", id, 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); await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
} }
} }
@@ -1,3 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -14,18 +15,21 @@ public class RouteProcessingService : BackgroundService
{ {
private readonly IRouteRepository _routeRepository; private readonly IRouteRepository _routeRepository;
private readonly IRegionRepository _regionRepository; private readonly IRegionRepository _regionRepository;
private readonly IServiceProvider _serviceProvider;
private readonly StorageConfig _storageConfig; private readonly StorageConfig _storageConfig;
private readonly ILogger<RouteProcessingService> _logger; private readonly ILogger<RouteProcessingService> _logger;
private readonly TimeSpan _checkInterval = TimeSpan.FromSeconds(10); private readonly TimeSpan _checkInterval = TimeSpan.FromSeconds(5);
public RouteProcessingService( public RouteProcessingService(
IRouteRepository routeRepository, IRouteRepository routeRepository,
IRegionRepository regionRepository, IRegionRepository regionRepository,
IServiceProvider serviceProvider,
IOptions<StorageConfig> storageConfig, IOptions<StorageConfig> storageConfig,
ILogger<RouteProcessingService> logger) ILogger<RouteProcessingService> logger)
{ {
_routeRepository = routeRepository; _routeRepository = routeRepository;
_regionRepository = regionRepository; _regionRepository = regionRepository;
_serviceProvider = serviceProvider;
_storageConfig = storageConfig.Value; _storageConfig = storageConfig.Value;
_logger = logger; _logger = logger;
} }
@@ -62,7 +66,7 @@ public class RouteProcessingService : BackgroundService
try try
{ {
await ProcessRouteIfReadyAsync(route.Id, cancellationToken); await ProcessRouteSequentiallyAsync(route.Id, cancellationToken);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -77,7 +81,7 @@ public class RouteProcessingService : BackgroundService
return routes.Select(r => (r.Id, r.RequestMaps)).ToList(); 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); var route = await _routeRepository.GetByIdAsync(routeId);
if (route == null || !route.RequestMaps || route.MapsReady) if (route == null || !route.RequestMaps || route.MapsReady)
@@ -85,49 +89,84 @@ public class RouteProcessingService : BackgroundService
return; return;
} }
var regionIds = await _routeRepository.GetRegionIdsByRouteAsync(routeId); var routePointsList = (await _routeRepository.GetRoutePointsAsync(routeId)).ToList();
if (!regionIds.Any()) 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<Common.Interfaces.IRegionService>();
_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; return;
} }
var allCompleted = true; var lastRegion = await _regionRepository.GetByIdAsync(regionIdsList.Last());
var anyFailed = false; if (lastRegion == null)
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)
{ {
return; return;
} }
if (anyFailed) if (lastRegion.Status == "queued" || lastRegion.Status == "processing")
{ {
_logger.LogWarning("Route {RouteId} has failed regions, skipping processing", routeId);
return; return;
} }
_logger.LogInformation("All regions completed for route {RouteId}, starting final processing", routeId); if (lastRegion.Status == "failed")
{
_logger.LogError("Route {RouteId}: Region {RegionId} failed. Stopping route processing.",
routeId, lastRegion.Id);
await GenerateRouteMapsAsync(routeId, route, regionIds, cancellationToken); 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<Common.Interfaces.IRegionService>();
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( private async Task GenerateRouteMapsAsync(
@@ -353,8 +392,8 @@ public class RouteProcessingService : BackgroundService
if (tileImage.Width != tileSizePixels || tileImage.Height != tileSizePixels) if (tileImage.Width != tileSizePixels || tileImage.Height != tileSizePixels)
{ {
_logger.LogWarning("Tile {FilePath} has wrong size {Width}x{Height}, expected {Expected}x{Expected}", _logger.LogWarning("Tile {FilePath} has wrong size {Width}x{Height}, expected {ExpectedWidth}x{ExpectedHeight}",
tile.FilePath, tileImage.Width, tileImage.Height, tileSizePixels); tile.FilePath, tileImage.Width, tileImage.Height, tileSizePixels, tileSizePixels);
} }
stitchedImage.Mutate(ctx => ctx.DrawImage(tileImage, new Point(destX, destY), 1f)); stitchedImage.Mutate(ctx => ctx.DrawImage(tileImage, new Point(destX, destY), 1f));
+3 -21
View File
@@ -65,7 +65,7 @@ public class RouteService : IRouteService
totalDistance += distanceFromPrevious.Value; totalDistance += distanceFromPrevious.Value;
} }
var pointType = isStart ? "start" : (isEnd ? "end" : "waypoint"); var pointType = isStart ? "start" : (isEnd ? "end" : "action");
allPoints.Add(new RoutePointDto allPoints.Add(new RoutePointDto
{ {
@@ -147,26 +147,8 @@ public class RouteService : IRouteService
if (request.RequestMaps) if (request.RequestMaps)
{ {
_logger.LogInformation("Route {RouteId}: Requesting regions for all {Count} points", _logger.LogInformation("Route {RouteId}: Maps requested. Regions will be processed sequentially by background service.",
request.Id, allPoints.Count); request.Id);
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} created successfully", request.Id); _logger.LogInformation("Route {RouteId} created successfully", request.Id);