mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 07:01:15 +00:00
[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:
@@ -161,9 +161,9 @@ async Task<IResult> ServeTile(int z, int x, int y, HttpContext httpContext, ITil
|
|||||||
return Results.Bytes(tile.Bytes, tile.ContentType);
|
return Results.Bytes(tile.Bytes, tile.ContentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<IResult> GetTileByLatLon([FromQuery] double Latitude, [FromQuery] double Longitude, [FromQuery] int ZoomLevel, ITileService tileService)
|
async Task<IResult> GetTileByLatLon([FromQuery] double Latitude, [FromQuery] double Longitude, [FromQuery] int ZoomLevel, HttpContext httpContext, ITileService tileService)
|
||||||
{
|
{
|
||||||
var tile = await tileService.DownloadAndStoreSingleTileAsync(Latitude, Longitude, ZoomLevel);
|
var tile = await tileService.DownloadAndStoreSingleTileAsync(Latitude, Longitude, ZoomLevel, httpContext.RequestAborted);
|
||||||
|
|
||||||
var response = new DownloadTileResponse
|
var response = new DownloadTileResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,7 +25,11 @@
|
|||||||
},
|
},
|
||||||
"MapConfig": {
|
"MapConfig": {
|
||||||
"Service": "GoogleMaps",
|
"Service": "GoogleMaps",
|
||||||
"ApiKey": ""
|
"ApiKey": "",
|
||||||
|
"TileSizePixels": 256,
|
||||||
|
"AllowedZoomLevels": [ 15, 16, 17, 18, 19 ],
|
||||||
|
"RetryBaseDelaySeconds": 1,
|
||||||
|
"RetryMaxDelaySeconds": 30
|
||||||
},
|
},
|
||||||
"StorageConfig": {
|
"StorageConfig": {
|
||||||
"TilesDirectory": "./tiles",
|
"TilesDirectory": "./tiles",
|
||||||
@@ -37,7 +41,11 @@
|
|||||||
"DefaultZoomLevel": 20,
|
"DefaultZoomLevel": 20,
|
||||||
"QueueCapacity": 1000,
|
"QueueCapacity": 1000,
|
||||||
"DelayBetweenRequestsMs": 50,
|
"DelayBetweenRequestsMs": 50,
|
||||||
"SessionTokenReuseCount": 100
|
"SessionTokenReuseCount": 100,
|
||||||
|
"RegionProcessingTimeoutSeconds": 300,
|
||||||
|
"RouteProcessingPollIntervalSeconds": 5,
|
||||||
|
"MaxRoutePointSpacingMeters": 200.0,
|
||||||
|
"LatLonTolerance": 0.0001
|
||||||
},
|
},
|
||||||
"CorsConfig": {
|
"CorsConfig": {
|
||||||
"AllowedOrigins": []
|
"AllowedOrigins": []
|
||||||
|
|||||||
@@ -4,4 +4,10 @@ public class MapConfig
|
|||||||
{
|
{
|
||||||
public string Service { get; set; } = null!;
|
public string Service { get; set; } = null!;
|
||||||
public string ApiKey { get; set; } = null!;
|
public string ApiKey { get; set; } = null!;
|
||||||
}
|
|
||||||
|
// AZ-371 / C18 — Google Maps tile constants promoted from source literals.
|
||||||
|
public int TileSizePixels { get; set; } = 256;
|
||||||
|
public int[] AllowedZoomLevels { get; set; } = new[] { 15, 16, 17, 18, 19 };
|
||||||
|
public int RetryBaseDelaySeconds { get; set; } = 1;
|
||||||
|
public int RetryMaxDelaySeconds { get; set; } = 30;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,5 +8,11 @@ public class ProcessingConfig
|
|||||||
public int QueueCapacity { get; set; } = 100;
|
public int QueueCapacity { get; set; } = 100;
|
||||||
public int DelayBetweenRequestsMs { get; set; } = 50;
|
public int DelayBetweenRequestsMs { get; set; } = 50;
|
||||||
public int SessionTokenReuseCount { get; set; } = 100;
|
public int SessionTokenReuseCount { get; set; } = 100;
|
||||||
|
|
||||||
|
// AZ-371 / C18 — operational levers promoted from source literals.
|
||||||
|
public int RegionProcessingTimeoutSeconds { get; set; } = 300;
|
||||||
|
public int RouteProcessingPollIntervalSeconds { get; set; } = 5;
|
||||||
|
public double MaxRoutePointSpacingMeters { get; set; } = 200.0;
|
||||||
|
public double LatLonTolerance { get; set; } = 0.0001;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public class RegionService : IRegionService
|
|||||||
private readonly IRegionRequestQueue _queue;
|
private readonly IRegionRequestQueue _queue;
|
||||||
private readonly ITileService _tileService;
|
private readonly ITileService _tileService;
|
||||||
private readonly StorageConfig _storageConfig;
|
private readonly StorageConfig _storageConfig;
|
||||||
|
private readonly ProcessingConfig _processingConfig;
|
||||||
private readonly ILogger<RegionService> _logger;
|
private readonly ILogger<RegionService> _logger;
|
||||||
|
|
||||||
public RegionService(
|
public RegionService(
|
||||||
@@ -26,12 +27,14 @@ public class RegionService : IRegionService
|
|||||||
IRegionRequestQueue queue,
|
IRegionRequestQueue queue,
|
||||||
ITileService tileService,
|
ITileService tileService,
|
||||||
IOptions<StorageConfig> storageConfig,
|
IOptions<StorageConfig> storageConfig,
|
||||||
|
IOptions<ProcessingConfig> processingConfig,
|
||||||
ILogger<RegionService> logger)
|
ILogger<RegionService> logger)
|
||||||
{
|
{
|
||||||
_regionRepository = regionRepository;
|
_regionRepository = regionRepository;
|
||||||
_queue = queue;
|
_queue = queue;
|
||||||
_tileService = tileService;
|
_tileService = tileService;
|
||||||
_storageConfig = storageConfig.Value;
|
_storageConfig = storageConfig.Value;
|
||||||
|
_processingConfig = processingConfig.Value;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +105,7 @@ public class RegionService : IRegionService
|
|||||||
region.UpdatedAt = DateTime.UtcNow;
|
region.UpdatedAt = DateTime.UtcNow;
|
||||||
await _regionRepository.UpdateAsync(region);
|
await _regionRepository.UpdateAsync(region);
|
||||||
|
|
||||||
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
|
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(_processingConfig.RegionProcessingTimeoutSeconds));
|
||||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||||
|
|
||||||
string? errorMessage = null;
|
string? errorMessage = null;
|
||||||
|
|||||||
@@ -18,15 +18,16 @@ namespace SatelliteProvider.Services.RouteManagement;
|
|||||||
// AC-2 (pixel-for-pixel identical output for existing scenarios).
|
// AC-2 (pixel-for-pixel identical output for existing scenarios).
|
||||||
public class RouteImageRenderer
|
public class RouteImageRenderer
|
||||||
{
|
{
|
||||||
private const int TileSizePixels = 256;
|
|
||||||
|
|
||||||
private readonly StorageConfig _storageConfig;
|
private readonly StorageConfig _storageConfig;
|
||||||
|
private readonly int _tileSizePixels;
|
||||||
private readonly ILogger<RouteImageRenderer> _logger;
|
private readonly ILogger<RouteImageRenderer> _logger;
|
||||||
|
|
||||||
public RouteImageRenderer(IOptions<StorageConfig> storageConfig, ILogger<RouteImageRenderer> logger)
|
public RouteImageRenderer(IOptions<StorageConfig> storageConfig, IOptions<MapConfig> mapConfig, ILogger<RouteImageRenderer> logger)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(storageConfig);
|
ArgumentNullException.ThrowIfNull(storageConfig);
|
||||||
|
ArgumentNullException.ThrowIfNull(mapConfig);
|
||||||
_storageConfig = storageConfig.Value;
|
_storageConfig = storageConfig.Value;
|
||||||
|
_tileSizePixels = mapConfig.Value.TileSizePixels;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ public class RouteImageRenderer
|
|||||||
var stitcher = new TileGridStitcher();
|
var stitcher = new TileGridStitcher();
|
||||||
var result = await stitcher.StitchAsync(
|
var result = await stitcher.StitchAsync(
|
||||||
placements,
|
placements,
|
||||||
TileSizePixels,
|
_tileSizePixels,
|
||||||
deduplicateByTileCoords: true,
|
deduplicateByTileCoords: true,
|
||||||
swallowTileLoadErrors: true,
|
swallowTileLoadErrors: true,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
@@ -97,10 +98,10 @@ public class RouteImageRenderer
|
|||||||
|
|
||||||
foreach (var (geoMinX, geoMinY, geoMaxX, geoMaxY) in geofencePolygonBounds)
|
foreach (var (geoMinX, geoMinY, geoMaxX, geoMaxY) in geofencePolygonBounds)
|
||||||
{
|
{
|
||||||
var x1 = (geoMinX - minX) * TileSizePixels;
|
var x1 = (geoMinX - minX) * _tileSizePixels;
|
||||||
var y1 = (geoMinY - minY + 1) * TileSizePixels;
|
var y1 = (geoMinY - minY + 1) * _tileSizePixels;
|
||||||
var x2 = (geoMaxX - minX + 2) * TileSizePixels - 1;
|
var x2 = (geoMaxX - minX + 2) * _tileSizePixels - 1;
|
||||||
var y2 = (geoMaxY - minY + 1) * TileSizePixels - 1;
|
var y2 = (geoMaxY - minY + 1) * _tileSizePixels - 1;
|
||||||
|
|
||||||
x1 = Math.Max(0, Math.Min(x1, imageWidth - 1));
|
x1 = Math.Max(0, Math.Min(x1, imageWidth - 1));
|
||||||
y1 = Math.Max(0, Math.Min(y1, imageHeight - 1));
|
y1 = Math.Max(0, Math.Min(y1, imageHeight - 1));
|
||||||
@@ -121,8 +122,8 @@ public class RouteImageRenderer
|
|||||||
var geoPoint = new GeoPoint(point.Latitude, point.Longitude);
|
var geoPoint = new GeoPoint(point.Latitude, point.Longitude);
|
||||||
var (tileX, tileY) = GeoUtils.WorldToTilePos(geoPoint, zoomLevel);
|
var (tileX, tileY) = GeoUtils.WorldToTilePos(geoPoint, zoomLevel);
|
||||||
|
|
||||||
var pixelX = (tileX - minX) * TileSizePixels + TileSizePixels / 2;
|
var pixelX = (tileX - minX) * _tileSizePixels + _tileSizePixels / 2;
|
||||||
var pixelY = (tileY - minY) * TileSizePixels + TileSizePixels / 2;
|
var pixelY = (tileY - minY) * _tileSizePixels + _tileSizePixels / 2;
|
||||||
|
|
||||||
if (pixelX >= 0 && pixelX < imageWidth && pixelY >= 0 && pixelY < imageHeight)
|
if (pixelX >= 0 && pixelX < imageWidth && pixelY >= 0 && pixelY < imageHeight)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SatelliteProvider.Common.Configs;
|
||||||
using SatelliteProvider.Common.DTO;
|
using SatelliteProvider.Common.DTO;
|
||||||
using SatelliteProvider.Common.Utils;
|
using SatelliteProvider.Common.Utils;
|
||||||
|
|
||||||
@@ -5,7 +7,13 @@ namespace SatelliteProvider.Services.RouteManagement;
|
|||||||
|
|
||||||
public class RoutePointGraphBuilder
|
public class RoutePointGraphBuilder
|
||||||
{
|
{
|
||||||
public const double MaxPointSpacingMeters = 200.0;
|
private readonly double _maxPointSpacingMeters;
|
||||||
|
|
||||||
|
public RoutePointGraphBuilder(IOptions<ProcessingConfig> processingConfig)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(processingConfig);
|
||||||
|
_maxPointSpacingMeters = processingConfig.Value.MaxRoutePointSpacingMeters;
|
||||||
|
}
|
||||||
|
|
||||||
public RoutePointGraph Build(IReadOnlyList<RoutePoint> userPoints)
|
public RoutePointGraph Build(IReadOnlyList<RoutePoint> userPoints)
|
||||||
{
|
{
|
||||||
@@ -53,7 +61,7 @@ public class RoutePointGraphBuilder
|
|||||||
|
|
||||||
var next = userPoints[segmentIndex + 1];
|
var next = userPoints[segmentIndex + 1];
|
||||||
var nextGeo = new GeoPoint(next.Latitude, next.Longitude);
|
var nextGeo = new GeoPoint(next.Latitude, next.Longitude);
|
||||||
var intermediates = GeoUtils.CalculateIntermediatePoints(currentGeo, nextGeo, MaxPointSpacingMeters);
|
var intermediates = GeoUtils.CalculateIntermediatePoints(currentGeo, nextGeo, _maxPointSpacingMeters);
|
||||||
|
|
||||||
foreach (var intermediateGeo in intermediates)
|
foreach (var intermediateGeo in intermediates)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SatelliteProvider.Common.Configs;
|
||||||
using SatelliteProvider.Common.DTO;
|
using SatelliteProvider.Common.DTO;
|
||||||
using SatelliteProvider.Common.Interfaces;
|
using SatelliteProvider.Common.Interfaces;
|
||||||
using SatelliteProvider.Common.Utils;
|
using SatelliteProvider.Common.Utils;
|
||||||
@@ -26,7 +28,7 @@ public class RouteProcessingService : BackgroundService
|
|||||||
private readonly RegionFileCleaner _regionFileCleaner;
|
private readonly RegionFileCleaner _regionFileCleaner;
|
||||||
private readonly RouteRegionMatcher _routeRegionMatcher;
|
private readonly RouteRegionMatcher _routeRegionMatcher;
|
||||||
private readonly ILogger<RouteProcessingService> _logger;
|
private readonly ILogger<RouteProcessingService> _logger;
|
||||||
private readonly TimeSpan _checkInterval = TimeSpan.FromSeconds(5);
|
private readonly TimeSpan _checkInterval;
|
||||||
|
|
||||||
public RouteProcessingService(
|
public RouteProcessingService(
|
||||||
IRouteRepository routeRepository,
|
IRouteRepository routeRepository,
|
||||||
@@ -37,6 +39,7 @@ public class RouteProcessingService : BackgroundService
|
|||||||
RouteImageRenderer routeImageRenderer,
|
RouteImageRenderer routeImageRenderer,
|
||||||
TilesZipBuilder tilesZipBuilder,
|
TilesZipBuilder tilesZipBuilder,
|
||||||
RegionFileCleaner regionFileCleaner,
|
RegionFileCleaner regionFileCleaner,
|
||||||
|
IOptions<ProcessingConfig> processingConfig,
|
||||||
ILogger<RouteProcessingService> logger)
|
ILogger<RouteProcessingService> logger)
|
||||||
{
|
{
|
||||||
_routeRepository = routeRepository;
|
_routeRepository = routeRepository;
|
||||||
@@ -48,6 +51,7 @@ public class RouteProcessingService : BackgroundService
|
|||||||
_tilesZipBuilder = tilesZipBuilder;
|
_tilesZipBuilder = tilesZipBuilder;
|
||||||
_regionFileCleaner = regionFileCleaner;
|
_regionFileCleaner = regionFileCleaner;
|
||||||
_routeRegionMatcher = new RouteRegionMatcher();
|
_routeRegionMatcher = new RouteRegionMatcher();
|
||||||
|
_checkInterval = TimeSpan.FromSeconds(processingConfig.Value.RouteProcessingPollIntervalSeconds);
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SatelliteProvider.Common.Configs;
|
||||||
using SatelliteProvider.Common.DTO;
|
using SatelliteProvider.Common.DTO;
|
||||||
using SatelliteProvider.Common.Interfaces;
|
using SatelliteProvider.Common.Interfaces;
|
||||||
using SatelliteProvider.DataAccess.Models;
|
using SatelliteProvider.DataAccess.Models;
|
||||||
@@ -19,10 +21,11 @@ public class RouteService : IRouteService
|
|||||||
public RouteService(
|
public RouteService(
|
||||||
IRouteRepository routeRepository,
|
IRouteRepository routeRepository,
|
||||||
IRegionService regionService,
|
IRegionService regionService,
|
||||||
|
IOptions<ProcessingConfig> processingConfig,
|
||||||
ILogger<RouteService> logger)
|
ILogger<RouteService> logger)
|
||||||
: this(routeRepository, regionService, logger,
|
: this(routeRepository, regionService, logger,
|
||||||
new RouteValidator(),
|
new RouteValidator(processingConfig),
|
||||||
new RoutePointGraphBuilder(),
|
new RoutePointGraphBuilder(processingConfig),
|
||||||
new GeofenceGridCalculator(),
|
new GeofenceGridCalculator(),
|
||||||
new RouteResponseMapper())
|
new RouteResponseMapper())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SatelliteProvider.Common.Configs;
|
||||||
using SatelliteProvider.Common.DTO;
|
using SatelliteProvider.Common.DTO;
|
||||||
|
|
||||||
namespace SatelliteProvider.Services.RouteManagement;
|
namespace SatelliteProvider.Services.RouteManagement;
|
||||||
|
|
||||||
public class RouteValidator
|
public class RouteValidator
|
||||||
{
|
{
|
||||||
|
private readonly double _latLonTolerance;
|
||||||
|
|
||||||
|
public RouteValidator(IOptions<ProcessingConfig> processingConfig)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(processingConfig);
|
||||||
|
_latLonTolerance = processingConfig.Value.LatLonTolerance;
|
||||||
|
}
|
||||||
|
|
||||||
public void Validate(CreateRouteRequest request)
|
public void Validate(CreateRouteRequest request)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
@@ -39,7 +49,7 @@ public class RouteValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ValidatePolygon(GeofencePolygon polygon, List<string> errors)
|
private void ValidatePolygon(GeofencePolygon polygon, List<string> errors)
|
||||||
{
|
{
|
||||||
if (polygon.NorthWest is null || polygon.SouthEast is null)
|
if (polygon.NorthWest is null || polygon.SouthEast is null)
|
||||||
{
|
{
|
||||||
@@ -50,8 +60,8 @@ public class RouteValidator
|
|||||||
var nw = polygon.NorthWest;
|
var nw = polygon.NorthWest;
|
||||||
var se = polygon.SouthEast;
|
var se = polygon.SouthEast;
|
||||||
|
|
||||||
if ((Math.Abs(nw.Lat) < 0.0001 && Math.Abs(nw.Lon) < 0.0001) ||
|
if ((Math.Abs(nw.Lat) < _latLonTolerance && Math.Abs(nw.Lon) < _latLonTolerance) ||
|
||||||
(Math.Abs(se.Lat) < 0.0001 && Math.Abs(se.Lon) < 0.0001))
|
(Math.Abs(se.Lat) < _latLonTolerance && Math.Abs(se.Lon) < _latLonTolerance))
|
||||||
{
|
{
|
||||||
errors.Add("Geofence polygon coordinates cannot be (0,0)");
|
errors.Add("Geofence polygon coordinates cannot be (0,0)");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 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 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 ILogger<GoogleMapsDownloaderV2> _logger;
|
||||||
private readonly string _apiKey;
|
private readonly string _apiKey;
|
||||||
private readonly StorageConfig _storageConfig;
|
private readonly StorageConfig _storageConfig;
|
||||||
private readonly ProcessingConfig _processingConfig;
|
private readonly ProcessingConfig _processingConfig;
|
||||||
|
private readonly MapConfig _mapConfig;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly SemaphoreSlim _downloadSemaphore;
|
private readonly SemaphoreSlim _downloadSemaphore;
|
||||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, Task<DownloadedTileInfoV2>> _activeDownloads = new();
|
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, Task<DownloadedTileInfoV2>> _activeDownloads = new();
|
||||||
@@ -36,7 +33,8 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
|
|||||||
IHttpClientFactory httpClientFactory)
|
IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_apiKey = mapConfig.Value.ApiKey;
|
_mapConfig = mapConfig.Value;
|
||||||
|
_apiKey = _mapConfig.ApiKey;
|
||||||
_storageConfig = storageConfig.Value;
|
_storageConfig = storageConfig.Value;
|
||||||
_processingConfig = processingConfig.Value;
|
_processingConfig = processingConfig.Value;
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
@@ -84,9 +82,9 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
|
|||||||
|
|
||||||
public async Task<DownloadedTileInfoV2> DownloadSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken token = default)
|
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);
|
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;
|
const double EARTH_CIRCUMFERENCE_METERS = 40075016.686;
|
||||||
|
var tileSizePixels = _mapConfig.TileSizePixels;
|
||||||
var latRad = latitude * Math.PI / 180.0;
|
var latRad = latitude * Math.PI / 180.0;
|
||||||
var metersPerPixel = (EARTH_CIRCUMFERENCE_METERS * Math.Cos(latRad)) / (Math.Pow(2, zoomLevel) * TILE_SIZE_PIXELS);
|
var metersPerPixel = (EARTH_CIRCUMFERENCE_METERS * Math.Cos(latRad)) / (Math.Pow(2, zoomLevel) * tileSizePixels);
|
||||||
return metersPerPixel * TILE_SIZE_PIXELS;
|
return metersPerPixel * tileSizePixels;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> action, int maxRetries = 5, CancellationToken cancellationToken = default)
|
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> action, int maxRetries = 5, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
int attempt = 0;
|
int attempt = 0;
|
||||||
int delay = BASE_RETRY_DELAY_SECONDS;
|
int delay = _mapConfig.RetryBaseDelaySeconds;
|
||||||
Exception? lastException = null;
|
Exception? lastException = null;
|
||||||
|
|
||||||
while (attempt < maxRetries)
|
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.");
|
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);
|
_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);
|
||||||
}
|
}
|
||||||
@@ -203,7 +202,7 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
|
|||||||
throw;
|
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);
|
_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);
|
||||||
}
|
}
|
||||||
@@ -229,9 +228,9 @@ public class GoogleMapsDownloaderV2 : ISatelliteDownloader
|
|||||||
IEnumerable<ExistingTileInfo> existingTiles,
|
IEnumerable<ExistingTileInfo> existingTiles,
|
||||||
CancellationToken token = default)
|
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);
|
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 tileCenter = GeoUtils.TileToWorldPos(x, y, zoomLevel);
|
||||||
|
|
||||||
|
var tolerance = _processingConfig.LatLonTolerance;
|
||||||
var existingTile = existingTiles.FirstOrDefault(t =>
|
var existingTile = existingTiles.FirstOrDefault(t =>
|
||||||
Math.Abs(t.Latitude - tileCenter.Lat) < 0.0001 &&
|
Math.Abs(t.Latitude - tileCenter.Lat) < tolerance &&
|
||||||
Math.Abs(t.Longitude - tileCenter.Lon) < 0.0001 &&
|
Math.Abs(t.Longitude - tileCenter.Lon) < tolerance &&
|
||||||
t.TileZoom == zoomLevel);
|
t.TileZoom == zoomLevel);
|
||||||
|
|
||||||
if (existingTile != null)
|
if (existingTile != null)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SatelliteProvider.Common.Configs;
|
||||||
using SatelliteProvider.Common.DTO;
|
using SatelliteProvider.Common.DTO;
|
||||||
using SatelliteProvider.Common.Interfaces;
|
using SatelliteProvider.Common.Interfaces;
|
||||||
using SatelliteProvider.Common.Utils;
|
using SatelliteProvider.Common.Utils;
|
||||||
@@ -18,17 +20,20 @@ public class TileService : ITileService
|
|||||||
private readonly ISatelliteDownloader _downloader;
|
private readonly ISatelliteDownloader _downloader;
|
||||||
private readonly ITileRepository _tileRepository;
|
private readonly ITileRepository _tileRepository;
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly MapConfig _mapConfig;
|
||||||
private readonly ILogger<TileService> _logger;
|
private readonly ILogger<TileService> _logger;
|
||||||
|
|
||||||
public TileService(
|
public TileService(
|
||||||
ISatelliteDownloader downloader,
|
ISatelliteDownloader downloader,
|
||||||
ITileRepository tileRepository,
|
ITileRepository tileRepository,
|
||||||
IMemoryCache cache,
|
IMemoryCache cache,
|
||||||
|
IOptions<MapConfig> mapConfig,
|
||||||
ILogger<TileService> logger)
|
ILogger<TileService> logger)
|
||||||
{
|
{
|
||||||
_downloader = downloader;
|
_downloader = downloader;
|
||||||
_tileRepository = tileRepository;
|
_tileRepository = tileRepository;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
|
_mapConfig = mapConfig.Value;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +140,7 @@ public class TileService : ITileService
|
|||||||
return MapToMetadata(entity);
|
return MapToMetadata(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TileEntity BuildTileEntity(DownloadedTileInfoV2 downloaded)
|
private TileEntity BuildTileEntity(DownloadedTileInfoV2 downloaded)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
return new TileEntity
|
return new TileEntity
|
||||||
@@ -147,7 +152,7 @@ public class TileService : ITileService
|
|||||||
Latitude = downloaded.CenterLatitude,
|
Latitude = downloaded.CenterLatitude,
|
||||||
Longitude = downloaded.CenterLongitude,
|
Longitude = downloaded.CenterLongitude,
|
||||||
TileSizeMeters = downloaded.TileSizeMeters,
|
TileSizeMeters = downloaded.TileSizeMeters,
|
||||||
TileSizePixels = 256,
|
TileSizePixels = _mapConfig.TileSizePixels,
|
||||||
ImageType = "jpg",
|
ImageType = "jpg",
|
||||||
MapsVersion = $"downloaded_{now:yyyy-MM-dd}",
|
MapsVersion = $"downloaded_{now:yyyy-MM-dd}",
|
||||||
Version = null,
|
Version = null,
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using SatelliteProvider.Common.Configs;
|
||||||
|
|
||||||
|
namespace SatelliteProvider.Tests;
|
||||||
|
|
||||||
|
// AZ-371 / C18 — verifies the config defaults preserve the original literal values
|
||||||
|
// that lived in source code prior to the magic-numbers-to-config refactor.
|
||||||
|
public class ConfigDefaultsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ProcessingConfig_RegionProcessingTimeout_PreservesOriginal_AZ371_AC2()
|
||||||
|
{
|
||||||
|
// Assert
|
||||||
|
new ProcessingConfig().RegionProcessingTimeoutSeconds.Should().Be(300, "RegionService used TimeSpan.FromMinutes(5) before AZ-371");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProcessingConfig_RouteProcessingPollInterval_PreservesOriginal_AZ371_AC2()
|
||||||
|
{
|
||||||
|
// Assert
|
||||||
|
new ProcessingConfig().RouteProcessingPollIntervalSeconds.Should().Be(5, "RouteProcessingService polled every 5s before AZ-371");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProcessingConfig_MaxRoutePointSpacingMeters_PreservesOriginal_AZ371_AC2()
|
||||||
|
{
|
||||||
|
// Assert
|
||||||
|
new ProcessingConfig().MaxRoutePointSpacingMeters.Should().Be(200.0, "RoutePointGraphBuilder used 200m spacing before AZ-371");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProcessingConfig_LatLonTolerance_PreservesOriginal_AZ371_AC2()
|
||||||
|
{
|
||||||
|
// Assert
|
||||||
|
new ProcessingConfig().LatLonTolerance.Should().Be(0.0001, "RouteValidator and GoogleMapsDownloaderV2 used 0.0001 before AZ-371");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MapConfig_TileSizePixels_PreservesOriginal_AZ371_AC2()
|
||||||
|
{
|
||||||
|
// Assert
|
||||||
|
new MapConfig().TileSizePixels.Should().Be(256, "TileService and GoogleMapsDownloaderV2 used 256 px before AZ-371");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MapConfig_AllowedZoomLevels_PreservesOriginal_AZ371_AC2()
|
||||||
|
{
|
||||||
|
// Assert
|
||||||
|
new MapConfig().AllowedZoomLevels.Should().Equal(15, 16, 17, 18, 19);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MapConfig_RetryDelays_PreserveOriginal_AZ371_AC2()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cfg = new MapConfig();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
cfg.RetryBaseDelaySeconds.Should().Be(1);
|
||||||
|
cfg.RetryMaxDelaySeconds.Should().Be(30);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
using SatelliteProvider.Common.Configs;
|
||||||
using SatelliteProvider.Common.DTO;
|
using SatelliteProvider.Common.DTO;
|
||||||
using SatelliteProvider.Common.Interfaces;
|
using SatelliteProvider.Common.Interfaces;
|
||||||
using SatelliteProvider.DataAccess.Models;
|
using SatelliteProvider.DataAccess.Models;
|
||||||
@@ -65,7 +67,8 @@ public class InfrastructureTests
|
|||||||
var logger = NullLogger<TileService>.Instance;
|
var logger = NullLogger<TileService>.Instance;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var service = new TileService(downloader, tileRepo, cache, logger);
|
var mapConfig = Options.Create(new MapConfig { Service = "GoogleMaps", ApiKey = "" });
|
||||||
|
var service = new TileService(downloader, tileRepo, cache, mapConfig, logger);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
service.Should().NotBeNull();
|
service.Should().NotBeNull();
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ public class RegionServiceTests : IDisposable
|
|||||||
Mock<ITileService> tileService)
|
Mock<ITileService> tileService)
|
||||||
{
|
{
|
||||||
var storage = Options.Create(new StorageConfig { ReadyDirectory = _readyDir, TilesDirectory = "/tiles" });
|
var storage = Options.Create(new StorageConfig { ReadyDirectory = _readyDir, TilesDirectory = "/tiles" });
|
||||||
return new RegionService(regionRepo.Object, queue.Object, tileService.Object, storage, NullLogger<RegionService>.Instance);
|
var processing = Options.Create(new ProcessingConfig());
|
||||||
|
return new RegionService(regionRepo.Object, queue.Object, tileService.Object, storage, processing, NullLogger<RegionService>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ public class RouteImageRendererTests
|
|||||||
{
|
{
|
||||||
loggerMock = new Mock<ILogger<RouteImageRenderer>>();
|
loggerMock = new Mock<ILogger<RouteImageRenderer>>();
|
||||||
var storageOptions = Options.Create(new StorageConfig());
|
var storageOptions = Options.Create(new StorageConfig());
|
||||||
return new RouteImageRenderer(storageOptions, loggerMock.Object);
|
var mapOptions = Options.Create(new MapConfig { Service = "GoogleMaps", ApiKey = "" });
|
||||||
|
return new RouteImageRenderer(storageOptions, mapOptions, loggerMock.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void VerifyWarningLogged(Mock<ILogger<RouteImageRenderer>> loggerMock, string substringInState)
|
private static void VerifyWarningLogged(Mock<ILogger<RouteImageRenderer>> loggerMock, string substringInState)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SatelliteProvider.Common.Configs;
|
||||||
using SatelliteProvider.Common.DTO;
|
using SatelliteProvider.Common.DTO;
|
||||||
using SatelliteProvider.Common.Utils;
|
using SatelliteProvider.Common.Utils;
|
||||||
using SatelliteProvider.Services.RouteManagement;
|
using SatelliteProvider.Services.RouteManagement;
|
||||||
@@ -8,13 +10,18 @@ namespace SatelliteProvider.Tests;
|
|||||||
|
|
||||||
public class RoutePointGraphBuilderTests
|
public class RoutePointGraphBuilderTests
|
||||||
{
|
{
|
||||||
|
private static readonly ProcessingConfig DefaultProcessingConfig = new();
|
||||||
|
|
||||||
|
private static RoutePointGraphBuilder MakeBuilder() =>
|
||||||
|
new(Options.Create(new ProcessingConfig()));
|
||||||
|
|
||||||
private static List<RoutePoint> ToRoutePoints(IEnumerable<(double Lat, double Lon)> points) =>
|
private static List<RoutePoint> ToRoutePoints(IEnumerable<(double Lat, double Lon)> points) =>
|
||||||
points.Select(p => new RoutePoint { Latitude = p.Lat, Longitude = p.Lon }).ToList();
|
points.Select(p => new RoutePoint { Latitude = p.Lat, Longitude = p.Lon }).ToList();
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Build_TwoUserPoints_FirstIsStart_LastIsEnd_BetweenAreIntermediate()
|
public void Build_TwoUserPoints_FirstIsStart_LastIsEnd_BetweenAreIntermediate()
|
||||||
{
|
{
|
||||||
var sut = new RoutePointGraphBuilder();
|
var sut = MakeBuilder();
|
||||||
var input = ToRoutePoints(TestCoordinates.Route.Route01Points);
|
var input = ToRoutePoints(TestCoordinates.Route.Route01Points);
|
||||||
|
|
||||||
var graph = sut.Build(input);
|
var graph = sut.Build(input);
|
||||||
@@ -28,7 +35,7 @@ public class RoutePointGraphBuilderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Build_ConsecutivePointsRespectMaxSpacing()
|
public void Build_ConsecutivePointsRespectMaxSpacing()
|
||||||
{
|
{
|
||||||
var sut = new RoutePointGraphBuilder();
|
var sut = MakeBuilder();
|
||||||
var input = ToRoutePoints(TestCoordinates.Route.Route01Points);
|
var input = ToRoutePoints(TestCoordinates.Route.Route01Points);
|
||||||
|
|
||||||
var graph = sut.Build(input);
|
var graph = sut.Build(input);
|
||||||
@@ -40,15 +47,15 @@ public class RoutePointGraphBuilderTests
|
|||||||
var distance = GeoUtils.CalculateDistance(
|
var distance = GeoUtils.CalculateDistance(
|
||||||
new GeoPoint(prev.Latitude, prev.Longitude),
|
new GeoPoint(prev.Latitude, prev.Longitude),
|
||||||
new GeoPoint(cur.Latitude, cur.Longitude));
|
new GeoPoint(cur.Latitude, cur.Longitude));
|
||||||
distance.Should().BeLessThanOrEqualTo(RoutePointGraphBuilder.MaxPointSpacingMeters + 0.5,
|
distance.Should().BeLessThanOrEqualTo(DefaultProcessingConfig.MaxRoutePointSpacingMeters + 0.5,
|
||||||
$"point {i - 1}→{i} must be ≤{RoutePointGraphBuilder.MaxPointSpacingMeters}m");
|
$"point {i - 1}→{i} must be ≤{DefaultProcessingConfig.MaxRoutePointSpacingMeters}m");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Build_TenPointRoute_HasOneStartOneEndAndEightAction()
|
public void Build_TenPointRoute_HasOneStartOneEndAndEightAction()
|
||||||
{
|
{
|
||||||
var sut = new RoutePointGraphBuilder();
|
var sut = MakeBuilder();
|
||||||
var input = ToRoutePoints(TestCoordinates.Route.Route04Points);
|
var input = ToRoutePoints(TestCoordinates.Route.Route04Points);
|
||||||
|
|
||||||
var graph = sut.Build(input);
|
var graph = sut.Build(input);
|
||||||
@@ -62,7 +69,7 @@ public class RoutePointGraphBuilderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Build_TotalDistanceEqualsSumOfHaversineSegments()
|
public void Build_TotalDistanceEqualsSumOfHaversineSegments()
|
||||||
{
|
{
|
||||||
var sut = new RoutePointGraphBuilder();
|
var sut = MakeBuilder();
|
||||||
var input = ToRoutePoints(TestCoordinates.Route.Route01Points);
|
var input = ToRoutePoints(TestCoordinates.Route.Route01Points);
|
||||||
|
|
||||||
var graph = sut.Build(input);
|
var graph = sut.Build(input);
|
||||||
@@ -83,7 +90,7 @@ public class RoutePointGraphBuilderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Build_SequenceNumbersAreContiguousAndStartAtZero()
|
public void Build_SequenceNumbersAreContiguousAndStartAtZero()
|
||||||
{
|
{
|
||||||
var sut = new RoutePointGraphBuilder();
|
var sut = MakeBuilder();
|
||||||
var input = ToRoutePoints(TestCoordinates.Route.Route04Points);
|
var input = ToRoutePoints(TestCoordinates.Route.Route04Points);
|
||||||
|
|
||||||
var graph = sut.Build(input);
|
var graph = sut.Build(input);
|
||||||
@@ -95,7 +102,7 @@ public class RoutePointGraphBuilderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Build_FirstPointHasNullDistanceFromPrevious()
|
public void Build_FirstPointHasNullDistanceFromPrevious()
|
||||||
{
|
{
|
||||||
var sut = new RoutePointGraphBuilder();
|
var sut = MakeBuilder();
|
||||||
var input = ToRoutePoints(TestCoordinates.Route.Route01Points);
|
var input = ToRoutePoints(TestCoordinates.Route.Route01Points);
|
||||||
|
|
||||||
var graph = sut.Build(input);
|
var graph = sut.Build(input);
|
||||||
@@ -107,7 +114,7 @@ public class RoutePointGraphBuilderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Build_FewerThanTwoPoints_Throws()
|
public void Build_FewerThanTwoPoints_Throws()
|
||||||
{
|
{
|
||||||
var sut = new RoutePointGraphBuilder();
|
var sut = MakeBuilder();
|
||||||
var input = new List<RoutePoint> { new() { Latitude = 47.46, Longitude = 37.64 } };
|
var input = new List<RoutePoint> { new() { Latitude = 47.46, Longitude = 37.64 } };
|
||||||
|
|
||||||
Action act = () => sut.Build(input);
|
Action act = () => sut.Build(input);
|
||||||
@@ -118,7 +125,7 @@ public class RoutePointGraphBuilderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Build_NullInput_Throws()
|
public void Build_NullInput_Throws()
|
||||||
{
|
{
|
||||||
var sut = new RoutePointGraphBuilder();
|
var sut = MakeBuilder();
|
||||||
|
|
||||||
Action act = () => sut.Build(null!);
|
Action act = () => sut.Build(null!);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
using SatelliteProvider.Common.Configs;
|
||||||
using SatelliteProvider.Common.DTO;
|
using SatelliteProvider.Common.DTO;
|
||||||
using SatelliteProvider.Common.Interfaces;
|
using SatelliteProvider.Common.Interfaces;
|
||||||
using SatelliteProvider.Common.Utils;
|
using SatelliteProvider.Common.Utils;
|
||||||
@@ -17,7 +19,7 @@ public class RouteServiceTests
|
|||||||
Mock<IRouteRepository> routeRepo,
|
Mock<IRouteRepository> routeRepo,
|
||||||
Mock<IRegionService> regionService)
|
Mock<IRegionService> regionService)
|
||||||
{
|
{
|
||||||
return new RouteService(routeRepo.Object, regionService.Object, NullLogger<RouteService>.Instance);
|
return new RouteService(routeRepo.Object, regionService.Object, Options.Create(new ProcessingConfig()), NullLogger<RouteService>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CreateRouteRequest BuildRequest(IEnumerable<(double Lat, double Lon)> points, double regionSize = 500, int zoom = 18, bool requestMaps = false, Geofences? geofences = null)
|
private static CreateRouteRequest BuildRequest(IEnumerable<(double Lat, double Lon)> points, double regionSize = 500, int zoom = 18, bool requestMaps = false, Geofences? geofences = null)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SatelliteProvider.Common.Configs;
|
||||||
using SatelliteProvider.Common.DTO;
|
using SatelliteProvider.Common.DTO;
|
||||||
using SatelliteProvider.Services.RouteManagement;
|
using SatelliteProvider.Services.RouteManagement;
|
||||||
using SatelliteProvider.Tests.Fixtures;
|
using SatelliteProvider.Tests.Fixtures;
|
||||||
@@ -7,6 +9,9 @@ namespace SatelliteProvider.Tests;
|
|||||||
|
|
||||||
public class RouteValidatorTests
|
public class RouteValidatorTests
|
||||||
{
|
{
|
||||||
|
private static RouteValidator MakeValidator() =>
|
||||||
|
new(Options.Create(new ProcessingConfig()));
|
||||||
|
|
||||||
private static CreateRouteRequest BuildValidRequest()
|
private static CreateRouteRequest BuildValidRequest()
|
||||||
{
|
{
|
||||||
return new CreateRouteRequest
|
return new CreateRouteRequest
|
||||||
@@ -25,7 +30,7 @@ public class RouteValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_ValidRequest_DoesNotThrow_AZ365_AC2()
|
public void Validate_ValidRequest_DoesNotThrow_AZ365_AC2()
|
||||||
{
|
{
|
||||||
var sut = new RouteValidator();
|
var sut = MakeValidator();
|
||||||
var request = BuildValidRequest();
|
var request = BuildValidRequest();
|
||||||
|
|
||||||
Action act = () => sut.Validate(request);
|
Action act = () => sut.Validate(request);
|
||||||
@@ -36,7 +41,7 @@ public class RouteValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_FewerThanTwoPoints_Throws()
|
public void Validate_FewerThanTwoPoints_Throws()
|
||||||
{
|
{
|
||||||
var sut = new RouteValidator();
|
var sut = MakeValidator();
|
||||||
var request = BuildValidRequest();
|
var request = BuildValidRequest();
|
||||||
request.Points = new List<RoutePoint> { new() { Latitude = 47.46, Longitude = 37.64 } };
|
request.Points = new List<RoutePoint> { new() { Latitude = 47.46, Longitude = 37.64 } };
|
||||||
|
|
||||||
@@ -48,7 +53,7 @@ public class RouteValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_RegionSizeOutOfRange_Throws()
|
public void Validate_RegionSizeOutOfRange_Throws()
|
||||||
{
|
{
|
||||||
var sut = new RouteValidator();
|
var sut = MakeValidator();
|
||||||
var request = BuildValidRequest();
|
var request = BuildValidRequest();
|
||||||
request.RegionSizeMeters = 50;
|
request.RegionSizeMeters = 50;
|
||||||
|
|
||||||
@@ -61,7 +66,7 @@ public class RouteValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_BlankName_Throws()
|
public void Validate_BlankName_Throws()
|
||||||
{
|
{
|
||||||
var sut = new RouteValidator();
|
var sut = MakeValidator();
|
||||||
var request = BuildValidRequest();
|
var request = BuildValidRequest();
|
||||||
request.Name = " ";
|
request.Name = " ";
|
||||||
|
|
||||||
@@ -73,7 +78,7 @@ public class RouteValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_GeofencePolygonZeroZero_Throws()
|
public void Validate_GeofencePolygonZeroZero_Throws()
|
||||||
{
|
{
|
||||||
var sut = new RouteValidator();
|
var sut = MakeValidator();
|
||||||
var request = BuildValidRequest();
|
var request = BuildValidRequest();
|
||||||
request.Geofences = new Geofences
|
request.Geofences = new Geofences
|
||||||
{
|
{
|
||||||
@@ -92,7 +97,7 @@ public class RouteValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_GeofenceInvertedLatitudes_Throws()
|
public void Validate_GeofenceInvertedLatitudes_Throws()
|
||||||
{
|
{
|
||||||
var sut = new RouteValidator();
|
var sut = MakeValidator();
|
||||||
var request = BuildValidRequest();
|
var request = BuildValidRequest();
|
||||||
request.Geofences = new Geofences
|
request.Geofences = new Geofences
|
||||||
{
|
{
|
||||||
@@ -114,7 +119,7 @@ public class RouteValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_NullPolygonCorner_Throws()
|
public void Validate_NullPolygonCorner_Throws()
|
||||||
{
|
{
|
||||||
var sut = new RouteValidator();
|
var sut = MakeValidator();
|
||||||
var request = BuildValidRequest();
|
var request = BuildValidRequest();
|
||||||
request.Geofences = new Geofences
|
request.Geofences = new Geofences
|
||||||
{
|
{
|
||||||
@@ -133,7 +138,7 @@ public class RouteValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_OutOfRangeLatitude_Throws()
|
public void Validate_OutOfRangeLatitude_Throws()
|
||||||
{
|
{
|
||||||
var sut = new RouteValidator();
|
var sut = MakeValidator();
|
||||||
var request = BuildValidRequest();
|
var request = BuildValidRequest();
|
||||||
request.Geofences = new Geofences
|
request.Geofences = new Geofences
|
||||||
{
|
{
|
||||||
@@ -156,7 +161,7 @@ public class RouteValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_MultipleErrors_AggregatesIntoSingleException_AZ365_AC2()
|
public void Validate_MultipleErrors_AggregatesIntoSingleException_AZ365_AC2()
|
||||||
{
|
{
|
||||||
var sut = new RouteValidator();
|
var sut = MakeValidator();
|
||||||
var request = BuildValidRequest();
|
var request = BuildValidRequest();
|
||||||
request.Name = "";
|
request.Name = "";
|
||||||
request.RegionSizeMeters = 50;
|
request.RegionSizeMeters = 50;
|
||||||
@@ -175,7 +180,7 @@ public class RouteValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_NullRequest_Throws()
|
public void Validate_NullRequest_Throws()
|
||||||
{
|
{
|
||||||
var sut = new RouteValidator();
|
var sut = MakeValidator();
|
||||||
|
|
||||||
Action act = () => sut.Validate(null!);
|
Action act = () => sut.Validate(null!);
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class TileServiceTests
|
|||||||
downloader.Object,
|
downloader.Object,
|
||||||
tileRepo.Object,
|
tileRepo.Object,
|
||||||
cache ?? new MemoryCache(new MemoryCacheOptions()),
|
cache ?? new MemoryCache(new MemoryCacheOptions()),
|
||||||
|
Options.Create(new MapConfig { Service = "GoogleMaps", ApiKey = "" }),
|
||||||
NullLogger<TileService>.Instance);
|
NullLogger<TileService>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +373,28 @@ public class TileServiceTests
|
|||||||
tileRepo.Verify(r => r.InsertAsync(It.IsAny<TileEntity>()), Times.Once);
|
tileRepo.Verify(r => r.InsertAsync(It.IsAny<TileEntity>()), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DownloadAndStoreSingleTileAsync_ForwardsCancellationTokenToDownloader_AZ371_AC3()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const int zoom = 18;
|
||||||
|
var downloader = new Mock<ISatelliteDownloader>();
|
||||||
|
var tileRepo = new Mock<ITileRepository>();
|
||||||
|
CancellationToken capturedToken = default;
|
||||||
|
downloader
|
||||||
|
.Setup(d => d.DownloadSingleTileAsync(It.IsAny<double>(), It.IsAny<double>(), zoom, It.IsAny<CancellationToken>()))
|
||||||
|
.Callback<double, double, int, CancellationToken>((_, _, _, ct) => capturedToken = ct)
|
||||||
|
.ReturnsAsync(new DownloadedTileInfoV2(1, 2, zoom, 0, 0, "p.jpg", 100.0));
|
||||||
|
var service = BuildService(downloader, tileRepo);
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await service.DownloadAndStoreSingleTileAsync(0, 0, zoom, cts.Token);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
capturedToken.Should().Be(cts.Token, "AZ-371 AC-3: caller-supplied CT must reach the downloader");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DownloadAndStoreSingleTileAsync_DownloaderThrows_DoesNotInsert_AZ311_AC2b()
|
public async Task DownloadAndStoreSingleTileAsync_DownloaderThrows_DoesNotInsert_AZ311_AC2b()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
# Batch 18 Report — Refactor 03 Phase 4 (kickoff)
|
||||||
|
|
||||||
|
Date: 2026-05-11
|
||||||
|
Epic: AZ-350 (03-code-quality-refactoring)
|
||||||
|
Status: ✅ Complete
|
||||||
|
|
||||||
|
## Scope (1 task / 3 SP)
|
||||||
|
|
||||||
|
| ID | C-ID | Title | Points | Component |
|
||||||
|
|----|------|-------|--------|-----------|
|
||||||
|
| AZ-371 | C18 | Move hardcoded magic numbers to ProcessingConfig / MapConfig | 3 | Common + Services.* (all) |
|
||||||
|
|
||||||
|
Solo batch. First task of Phase 4 (typing/config/tooling). Promotes operational
|
||||||
|
literals scattered across 5 service files into explicit `ProcessingConfig` /
|
||||||
|
`MapConfig` keys. Defaults preserve every prior runtime value byte-for-byte.
|
||||||
|
Also closes the LF-2 finding by forwarding `HttpContext.RequestAborted` from
|
||||||
|
`GetTileByLatLon` into the downloader.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
- **MODIFIED** `SatelliteProvider.Common/Configs/ProcessingConfig.cs`
|
||||||
|
- Added 4 keys with defaults that match prior source literals:
|
||||||
|
- `RegionProcessingTimeoutSeconds = 300` (was `TimeSpan.FromMinutes(5)`)
|
||||||
|
- `RouteProcessingPollIntervalSeconds = 5` (was `TimeSpan.FromSeconds(5)`)
|
||||||
|
- `MaxRoutePointSpacingMeters = 200.0` (was the `public const`)
|
||||||
|
- `LatLonTolerance = 0.0001` (was duplicated in 2 sites)
|
||||||
|
|
||||||
|
- **MODIFIED** `SatelliteProvider.Common/Configs/MapConfig.cs`
|
||||||
|
- Added 4 keys with defaults that match prior source literals:
|
||||||
|
- `TileSizePixels = 256`
|
||||||
|
- `AllowedZoomLevels = [15, 16, 17, 18, 19]`
|
||||||
|
- `RetryBaseDelaySeconds = 1`
|
||||||
|
- `RetryMaxDelaySeconds = 30`
|
||||||
|
|
||||||
|
- **MODIFIED** `SatelliteProvider.Api/appsettings.json`
|
||||||
|
- Added the 8 new keys under `MapConfig` and `ProcessingConfig`. Values match
|
||||||
|
`Common.Configs` defaults. `appsettings.Development.json` left untouched —
|
||||||
|
no environment-specific overrides for these levers yet.
|
||||||
|
|
||||||
|
- **MODIFIED** `SatelliteProvider.Services.RegionProcessing/RegionService.cs`
|
||||||
|
- Constructor now takes `IOptions<ProcessingConfig> processingConfig`.
|
||||||
|
- The 5-minute region-processing timeout in `ProcessRegionAsync` is now
|
||||||
|
`TimeSpan.FromSeconds(_processingConfig.RegionProcessingTimeoutSeconds)`.
|
||||||
|
|
||||||
|
- **MODIFIED** `SatelliteProvider.Services.RouteManagement/RouteProcessingService.cs`
|
||||||
|
- Constructor now takes `IOptions<ProcessingConfig> processingConfig`.
|
||||||
|
- `_checkInterval` is no longer a `readonly` initializer literal; it's
|
||||||
|
set in the constructor from the config.
|
||||||
|
|
||||||
|
- **MODIFIED** `SatelliteProvider.Services.RouteManagement/RoutePointGraphBuilder.cs`
|
||||||
|
- Single ctor `(IOptions<ProcessingConfig>)`; the previous
|
||||||
|
parameterless ctor + `public const MaxPointSpacingMeters` are gone.
|
||||||
|
- The const lived briefly to replace the inline literal in `RouteService`
|
||||||
|
(batch 15); promoting it the rest of the way to config is the natural
|
||||||
|
completion of that move.
|
||||||
|
|
||||||
|
- **MODIFIED** `SatelliteProvider.Services.RouteManagement/RouteValidator.cs`
|
||||||
|
- Constructor now requires `IOptions<ProcessingConfig>`; reads
|
||||||
|
`LatLonTolerance` once and reuses it for the polygon (0,0) check.
|
||||||
|
- `ValidatePolygon` is no longer `static` because it reads instance state.
|
||||||
|
|
||||||
|
- **MODIFIED** `SatelliteProvider.Services.RouteManagement/RouteService.cs`
|
||||||
|
- The 4-arg DI ctor (`IRouteRepository, IRegionService, IOptions<ProcessingConfig>, ILogger`)
|
||||||
|
is the production-callable ctor; the chained 8-arg ctor used by the
|
||||||
|
decomposition still accepts the helpers directly. The 4-arg ctor
|
||||||
|
constructs `RouteValidator` and `RoutePointGraphBuilder` with the same
|
||||||
|
`IOptions<ProcessingConfig>` reference.
|
||||||
|
|
||||||
|
- **MODIFIED** `SatelliteProvider.Services.RouteManagement/RouteImageRenderer.cs`
|
||||||
|
- **Adjacent hygiene**: the `private const int TileSizePixels = 256` that
|
||||||
|
was extracted from `RouteProcessingService` in batch 17 is now read from
|
||||||
|
`_mapConfig.TileSizePixels`. Same operational lever as the `TileService`
|
||||||
|
site — leaving one configurable and the other hardcoded would be an
|
||||||
|
immediate cumulative-review finding (cross-task duplicate).
|
||||||
|
|
||||||
|
- **MODIFIED** `SatelliteProvider.Services.TileDownloader/GoogleMapsDownloaderV2.cs`
|
||||||
|
- The 4 `private const`s (`TILE_SIZE_PIXELS`, `MAX_RETRY_DELAY_SECONDS`,
|
||||||
|
`BASE_RETRY_DELAY_SECONDS`, `ALLOWED_ZOOM_LEVELS`) are gone.
|
||||||
|
- `MapConfig` was already injected; now `_mapConfig` field stores the
|
||||||
|
resolved `MapConfig.Value` and the 4 sites consume `_mapConfig.*`.
|
||||||
|
- `CalculateTileSizeInMeters` is no longer `static` (reads `_mapConfig`).
|
||||||
|
- The `0.0001` lat/lon tolerance in `GetTilesWithMetadataAsync` now reads
|
||||||
|
`_processingConfig.LatLonTolerance` once per call and reuses it in the
|
||||||
|
closure.
|
||||||
|
|
||||||
|
- **MODIFIED** `SatelliteProvider.Services.TileDownloader/TileService.cs`
|
||||||
|
- Constructor now takes `IOptions<MapConfig> mapConfig`.
|
||||||
|
- `BuildTileEntity` becomes an instance method (reads `_mapConfig`); the
|
||||||
|
`TileSizePixels = 256` literal becomes `_mapConfig.TileSizePixels`.
|
||||||
|
|
||||||
|
- **MODIFIED** `SatelliteProvider.Api/Program.cs`
|
||||||
|
- `GetTileByLatLon` now accepts `HttpContext httpContext` and forwards
|
||||||
|
`httpContext.RequestAborted` into `DownloadAndStoreSingleTileAsync`.
|
||||||
|
- This closes LF-2 (logical-flow finding from the discovery phase): client
|
||||||
|
cancellations now propagate through the `/api/satellite/tiles/latlon`
|
||||||
|
endpoint into the downloader.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- **NEW** `SatelliteProvider.Tests/ConfigDefaultsTests.cs` — 7 tests that
|
||||||
|
pin the new config defaults to the original source literals (AC-2 evidence
|
||||||
|
— defaults preserve behavior is the load-bearing claim of this batch).
|
||||||
|
- **MODIFIED** `SatelliteProvider.Tests/TileServiceTests.cs`
|
||||||
|
- Added 1 new test `DownloadAndStoreSingleTileAsync_ForwardsCancellationTokenToDownloader_AZ371_AC3`
|
||||||
|
(AC-3 evidence — caller-supplied CT reaches the downloader).
|
||||||
|
- Updated `BuildService` to construct `TileService` with the new
|
||||||
|
`IOptions<MapConfig>` argument.
|
||||||
|
- **MODIFIED** `SatelliteProvider.Tests/InfrastructureTests.cs`
|
||||||
|
- Updated the `TileService_ConstructsWithMockedDependencies` smoke test to
|
||||||
|
provide the new `IOptions<MapConfig>`.
|
||||||
|
- **MODIFIED** `SatelliteProvider.Tests/RegionServiceTests.cs`
|
||||||
|
- `BuildService` now also threads `IOptions<ProcessingConfig>`.
|
||||||
|
- **MODIFIED** `SatelliteProvider.Tests/RouteServiceTests.cs`
|
||||||
|
- `BuildService` now also threads `IOptions<ProcessingConfig>`.
|
||||||
|
- **MODIFIED** `SatelliteProvider.Tests/RoutePointGraphBuilderTests.cs`
|
||||||
|
- All 8 `new RoutePointGraphBuilder()` calls go through a `MakeBuilder()`
|
||||||
|
helper that wraps the new options ctor.
|
||||||
|
- The two assertions that referenced `RoutePointGraphBuilder.MaxPointSpacingMeters`
|
||||||
|
now read `DefaultProcessingConfig.MaxRoutePointSpacingMeters`.
|
||||||
|
- **MODIFIED** `SatelliteProvider.Tests/RouteValidatorTests.cs`
|
||||||
|
- All 9 `new RouteValidator()` calls go through `MakeValidator()`.
|
||||||
|
- **MODIFIED** `SatelliteProvider.Tests/RouteImageRendererTests.cs`
|
||||||
|
- `BuildSut` now also threads `IOptions<MapConfig>`.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|-------|--------|
|
||||||
|
| Unit tests | ✅ 141 / 141 pass (was 133; +7 ConfigDefaultsTests, +1 AZ-371 AC-3 CT test) |
|
||||||
|
| Integration smoke | ✅ All scenarios pass; exit 0 |
|
||||||
|
| Behavior preservation | ✅ Defaults match prior literals byte-for-byte |
|
||||||
|
| Linter | ✅ No findings on any modified file |
|
||||||
|
|
||||||
|
## AC Coverage
|
||||||
|
|
||||||
|
| AC | Evidence |
|
||||||
|
|----|----------|
|
||||||
|
| AC-1: All listed magic numbers replaced by config-bound values | Grep for `FromMinutes(5)`, `TILE_SIZE_PIXELS`, `MAX_RETRY`, `BASE_RETRY`, `ALLOWED_ZOOM`, `200.0`, `0.0001`, `TileSizePixels = 256` across `SatelliteProvider.Services.*/**/*.cs` returns no matches. The remaining `_tileSizePixels` references are `MapConfig`-bound. |
|
||||||
|
| AC-2: Defaults preserve all existing behavior | `ConfigDefaultsTests` (7 tests) pins each new default to the original literal value. Smoke suite passes — region pipeline, route pipeline, tile downloads all behave identically. |
|
||||||
|
| AC-3: `GetTileByLatLon` request cancellation flows into the downloader | `Program.cs:165` now passes `httpContext.RequestAborted` into `DownloadAndStoreSingleTileAsync`. `TileServiceTests.DownloadAndStoreSingleTileAsync_ForwardsCancellationTokenToDownloader_AZ371_AC3` captures the CT through the mock and asserts identity. |
|
||||||
|
|
||||||
|
## Inline Code Review
|
||||||
|
|
||||||
|
This batch is a config-binding refactor. The risk surface:
|
||||||
|
|
||||||
|
1. **Default-value drift** — every default in `ProcessingConfig` and `MapConfig`
|
||||||
|
was set side-by-side with the original literal in source and verified by
|
||||||
|
`ConfigDefaultsTests`. No drift possible.
|
||||||
|
2. **Constructor fan-out** — 5 production classes gained ctor params. Only the
|
||||||
|
already-DI-resolved consumers (`RouteService` 4-arg, `RegionService`,
|
||||||
|
`TileService`, `RouteProcessingService`, `GoogleMapsDownloaderV2`) needed
|
||||||
|
`IServiceCollection` calls — and those are unchanged because the existing
|
||||||
|
`services.AddSingleton(...)` lines bind via reflection. The two helper
|
||||||
|
classes that tests `new` directly (`RouteValidator`, `RoutePointGraphBuilder`)
|
||||||
|
now require `IOptions<ProcessingConfig>` — every call site is updated.
|
||||||
|
3. **Static → instance demotions** — three methods became instance methods
|
||||||
|
(`TileService.BuildTileEntity`, `GoogleMapsDownloaderV2.CalculateTileSizeInMeters`,
|
||||||
|
`RouteValidator.ValidatePolygon`). Each one now reads instance state, so
|
||||||
|
the demotion is correct per `coderule.mdc` ("Only use static methods for
|
||||||
|
pure, self-contained computations").
|
||||||
|
4. **CT plumbing** (LF-2) — endpoint local function gained `HttpContext`
|
||||||
|
parameter; minimal API binding picks it up automatically. Smoke covers
|
||||||
|
the happy path; the new unit test pins the contract on `TileService`.
|
||||||
|
|
||||||
|
**Verdict: PASS.** No findings. Behavior preserved end-to-end.
|
||||||
|
|
||||||
|
## State
|
||||||
|
|
||||||
|
`auto_push: true`. After this commit lands, transitions on AZ-371 → In Testing
|
||||||
|
in Jira and the task file moves from `todo/` to `done/`. Batch 18 closes the
|
||||||
|
window for the cumulative K=3 review covering batches 16–18 — that runs next.
|
||||||
|
|
||||||
|
## Next Batch (preview)
|
||||||
|
|
||||||
|
Phase 4 ordering: AZ-370 (status / point-type enums + AC RT2 update, 3 SP).
|
||||||
|
After the cumulative K=3 review for 16–18.
|
||||||
Reference in New Issue
Block a user