[AZ-371] Refactor C18: magic numbers to ProcessingConfig/MapConfig

Promotes 8 operational levers into config keys with defaults that match
the prior source literals byte-for-byte:
  ProcessingConfig: RegionProcessingTimeoutSeconds (300),
  RouteProcessingPollIntervalSeconds (5),
  MaxRoutePointSpacingMeters (200), LatLonTolerance (0.0001).
  MapConfig: TileSizePixels (256), AllowedZoomLevels ([15..19]),
  RetryBaseDelaySeconds (1), RetryMaxDelaySeconds (30).

Sites updated: RegionService, RouteProcessingService,
RoutePointGraphBuilder, RouteValidator, RouteService 4-arg ctor,
RouteImageRenderer, GoogleMapsDownloaderV2, TileService. Closes LF-2 by
forwarding HttpContext.RequestAborted from GetTileByLatLon into the
downloader. appsettings.json gains the 8 new keys at default values.

Tests: 141 / 141 unit + 5 / 5 smoke green. New ConfigDefaultsTests pins
defaults to original literals; new TileService unit test asserts CT
identity from caller to downloader (AZ-371 AC-3).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 03:30:07 +03:00
parent ee42b1716b
commit 1dcd089d39
21 changed files with 404 additions and 68 deletions
@@ -15,15 +15,12 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
{
private const string TILE_URL_TEMPLATE = "https://mt{0}.google.com/vt/lyrs=s&x={1}&y={2}&z={3}&token={4}";
private const string USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36";
private const int TILE_SIZE_PIXELS = 256;
private const int MAX_RETRY_DELAY_SECONDS = 30;
private const int BASE_RETRY_DELAY_SECONDS = 1;
private static readonly int[] ALLOWED_ZOOM_LEVELS = { 15, 16, 17, 18, 19 };
private readonly ILogger<GoogleMapsDownloaderV2> _logger;
private readonly string _apiKey;
private readonly StorageConfig _storageConfig;
private readonly ProcessingConfig _processingConfig;
private readonly MapConfig _mapConfig;
private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _downloadSemaphore;
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, Task<DownloadedTileInfoV2>> _activeDownloads = new();
@@ -36,7 +33,8 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
IHttpClientFactory httpClientFactory)
{
_logger = logger;
_apiKey = mapConfig.Value.ApiKey;
_mapConfig = mapConfig.Value;
_apiKey = _mapConfig.ApiKey;
_storageConfig = storageConfig.Value;
_processingConfig = processingConfig.Value;
_httpClientFactory = httpClientFactory;
@@ -84,9 +82,9 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
public async Task<DownloadedTileInfoV2> DownloadSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken token = default)
{
if (!ALLOWED_ZOOM_LEVELS.Contains(zoomLevel))
if (!_mapConfig.AllowedZoomLevels.Contains(zoomLevel))
{
throw new ArgumentException($"Zoom level {zoomLevel} is not allowed. Allowed zoom levels are: {string.Join(", ", ALLOWED_ZOOM_LEVELS)}", nameof(zoomLevel));
throw new ArgumentException($"Zoom level {zoomLevel} is not allowed. Allowed zoom levels are: {string.Join(", ", _mapConfig.AllowedZoomLevels)}", nameof(zoomLevel));
}
var geoPoint = new GeoPoint(latitude, longitude);
@@ -137,18 +135,19 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
);
}
private static double CalculateTileSizeInMeters(int zoomLevel, double latitude)
private double CalculateTileSizeInMeters(int zoomLevel, double latitude)
{
const double EARTH_CIRCUMFERENCE_METERS = 40075016.686;
var tileSizePixels = _mapConfig.TileSizePixels;
var latRad = latitude * Math.PI / 180.0;
var metersPerPixel = (EARTH_CIRCUMFERENCE_METERS * Math.Cos(latRad)) / (Math.Pow(2, zoomLevel) * TILE_SIZE_PIXELS);
return metersPerPixel * TILE_SIZE_PIXELS;
var metersPerPixel = (EARTH_CIRCUMFERENCE_METERS * Math.Cos(latRad)) / (Math.Pow(2, zoomLevel) * tileSizePixels);
return metersPerPixel * tileSizePixels;
}
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> action, int maxRetries = 5, CancellationToken cancellationToken = default)
{
int attempt = 0;
int delay = BASE_RETRY_DELAY_SECONDS;
int delay = _mapConfig.RetryBaseDelaySeconds;
Exception? lastException = null;
while (attempt < maxRetries)
@@ -178,7 +177,7 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
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, _mapConfig.RetryMaxDelaySeconds);
_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);
}
@@ -203,7 +202,7 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
throw;
}
delay = Math.Min(delay * 2, MAX_RETRY_DELAY_SECONDS);
delay = Math.Min(delay * 2, _mapConfig.RetryMaxDelaySeconds);
_logger.LogWarning("Server error ({StatusCode}). Waiting {Delay}s before retry {Attempt}/{Max}", ex.StatusCode, delay, attempt, maxRetries);
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken);
}
@@ -229,9 +228,9 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
IEnumerable<ExistingTileInfo> existingTiles,
CancellationToken token = default)
{
if (!ALLOWED_ZOOM_LEVELS.Contains(zoomLevel))
if (!_mapConfig.AllowedZoomLevels.Contains(zoomLevel))
{
throw new ArgumentException($"Zoom level {zoomLevel} is not allowed. Allowed zoom levels are: {string.Join(", ", ALLOWED_ZOOM_LEVELS)}", nameof(zoomLevel));
throw new ArgumentException($"Zoom level {zoomLevel} is not allowed. Allowed zoom levels are: {string.Join(", ", _mapConfig.AllowedZoomLevels)}", nameof(zoomLevel));
}
var (latMin, latMax, lonMin, lonMax) = GeoUtils.GetBoundingBox(centerGeoPoint, radiusM);
@@ -248,9 +247,10 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
{
var tileCenter = GeoUtils.TileToWorldPos(x, y, zoomLevel);
var tolerance = _processingConfig.LatLonTolerance;
var existingTile = existingTiles.FirstOrDefault(t =>
Math.Abs(t.Latitude - tileCenter.Lat) < 0.0001 &&
Math.Abs(t.Longitude - tileCenter.Lon) < 0.0001 &&
Math.Abs(t.Latitude - tileCenter.Lat) < tolerance &&
Math.Abs(t.Longitude - tileCenter.Lon) < tolerance &&
t.TileZoom == zoomLevel);
if (existingTile != null)
@@ -1,5 +1,7 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Common.Interfaces;
using SatelliteProvider.Common.Utils;
@@ -18,17 +20,20 @@ public class TileService : ITileService
private readonly ISatelliteDownloader _downloader;
private readonly ITileRepository _tileRepository;
private readonly IMemoryCache _cache;
private readonly MapConfig _mapConfig;
private readonly ILogger<TileService> _logger;
public TileService(
ISatelliteDownloader downloader,
ITileRepository tileRepository,
IMemoryCache cache,
IOptions<MapConfig> mapConfig,
ILogger<TileService> logger)
{
_downloader = downloader;
_tileRepository = tileRepository;
_cache = cache;
_mapConfig = mapConfig.Value;
_logger = logger;
}
@@ -135,7 +140,7 @@ public class TileService : ITileService
return MapToMetadata(entity);
}
private static TileEntity BuildTileEntity(DownloadedTileInfoV2 downloaded)
private TileEntity BuildTileEntity(DownloadedTileInfoV2 downloaded)
{
var now = DateTime.UtcNow;
return new TileEntity
@@ -147,7 +152,7 @@ public class TileService : ITileService
Latitude = downloaded.CenterLatitude,
Longitude = downloaded.CenterLongitude,
TileSizeMeters = downloaded.TileSizeMeters,
TileSizePixels = 256,
TileSizePixels = _mapConfig.TileSizePixels,
ImageType = "jpg",
MapsVersion = $"downloaded_{now:yyyy-MM-dd}",
Version = null,