mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-04-22 05:26:39 +00:00
more complex route
This commit is contained in:
@@ -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!");
|
||||
|
||||
@@ -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<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 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<SessionResponse>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RouteProcessingService> _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> storageConfig,
|
||||
ILogger<RouteProcessingService> 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<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;
|
||||
}
|
||||
|
||||
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<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(
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user