Files
Oleksandr Bezdieniezhnykh 1dcd089d39 [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>
2026-05-11 03:30:07 +03:00

161 lines
5.5 KiB
C#

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Common.Interfaces;
using SatelliteProvider.DataAccess.Models;
using SatelliteProvider.DataAccess.Repositories;
namespace SatelliteProvider.Services.RouteManagement;
public class RouteService : IRouteService
{
private readonly IRouteRepository _routeRepository;
private readonly IRegionService _regionService;
private readonly ILogger<RouteService> _logger;
private readonly RouteValidator _validator;
private readonly RoutePointGraphBuilder _pointGraphBuilder;
private readonly GeofenceGridCalculator _geofenceGridCalculator;
private readonly RouteResponseMapper _responseMapper;
public RouteService(
IRouteRepository routeRepository,
IRegionService regionService,
IOptions<ProcessingConfig> processingConfig,
ILogger<RouteService> logger)
: this(routeRepository, regionService, logger,
new RouteValidator(processingConfig),
new RoutePointGraphBuilder(processingConfig),
new GeofenceGridCalculator(),
new RouteResponseMapper())
{
}
public RouteService(
IRouteRepository routeRepository,
IRegionService regionService,
ILogger<RouteService> logger,
RouteValidator validator,
RoutePointGraphBuilder pointGraphBuilder,
GeofenceGridCalculator geofenceGridCalculator,
RouteResponseMapper responseMapper)
{
_routeRepository = routeRepository;
_regionService = regionService;
_logger = logger;
_validator = validator;
_pointGraphBuilder = pointGraphBuilder;
_geofenceGridCalculator = geofenceGridCalculator;
_responseMapper = responseMapper;
}
public async Task<RouteResponse> CreateRouteAsync(CreateRouteRequest request)
{
ArgumentNullException.ThrowIfNull(request);
// AZ-362: idempotent POST contract. A retried POST with the same caller-supplied
// Id returns the existing route instead of re-running point generation and
// re-queueing geofence regions.
var existing = await GetRouteAsync(request.Id);
if (existing != null)
{
_logger.LogInformation(
"Idempotent route POST: id {RouteId} already exists; returning existing resource",
request.Id);
return existing;
}
_validator.Validate(request);
var graph = _pointGraphBuilder.Build(request.Points);
var now = DateTime.UtcNow;
var routeEntity = new RouteEntity
{
Id = request.Id,
Name = request.Name,
Description = request.Description,
RegionSizeMeters = request.RegionSizeMeters,
ZoomLevel = request.ZoomLevel,
TotalDistanceMeters = graph.TotalDistanceMeters,
TotalPoints = graph.Points.Count,
RequestMaps = request.RequestMaps,
CreateTilesZip = request.CreateTilesZip,
MapsReady = false,
CreatedAt = now,
UpdatedAt = now,
};
await _routeRepository.InsertRouteAsync(routeEntity);
var pointEntities = graph.Points.Select(p => new RoutePointEntity
{
Id = Guid.NewGuid(),
RouteId = request.Id,
SequenceNumber = p.SequenceNumber,
Latitude = p.Latitude,
Longitude = p.Longitude,
PointType = p.PointType,
SegmentIndex = p.SegmentIndex,
DistanceFromPrevious = p.DistanceFromPrevious,
CreatedAt = now,
}).ToList();
await _routeRepository.InsertRoutePointsAsync(pointEntities);
await ProcessGeofencePolygonsAsync(request);
return _responseMapper.Map(routeEntity, graph.Points);
}
public async Task<RouteResponse?> GetRouteAsync(Guid id)
{
var route = await _routeRepository.GetByIdAsync(id);
if (route == null)
{
return null;
}
var points = await _routeRepository.GetRoutePointsAsync(id);
return _responseMapper.Map(route, points);
}
private async Task ProcessGeofencePolygonsAsync(CreateRouteRequest request)
{
var polygons = request.Geofences?.Polygons;
if (polygons is null || polygons.Count == 0)
{
return;
}
for (int polygonIndex = 0; polygonIndex < polygons.Count; polygonIndex++)
{
var polygon = polygons[polygonIndex];
// Validator (above) guarantees NorthWest/SouthEast are non-null and well-formed.
var regions = _geofenceGridCalculator.GenerateRegions(
polygon.NorthWest!,
polygon.SouthEast!,
request.RegionSizeMeters);
foreach (var center in regions)
{
var geofenceRegionId = Guid.NewGuid();
await _regionService.RequestRegionAsync(
geofenceRegionId,
center.Lat,
center.Lon,
request.RegionSizeMeters,
request.ZoomLevel,
stitchTiles: false);
await _routeRepository.LinkRouteToRegionAsync(
request.Id,
geofenceRegionId,
isGeofence: true,
geofencePolygonIndex: polygonIndex);
}
}
}
}