mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 08:21:14 +00:00
[AZ-365] Refactor C12: decompose RouteService.CreateRouteAsync
Extract RouteValidator (aggregating validator), RoutePointGraphBuilder (point interpolation + sequence numbering), GeofenceGridCalculator (NW/SE region centers), and RouteResponseMapper (entity -> DTO; also used by GetRouteAsync, eliminating duplicate DTO assembly). CreateRouteAsync shrinks 184 -> 52 LOC of orchestration. RouteService.cs shrinks 295 -> 138 LOC overall. Validation aggregates all failures into a single ArgumentException (AC-2); single-violation messages preserved verbatim so existing RouteServiceTests pass unchanged. 28 new unit tests for the four helpers (112/112 unit tests, smoke green). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
using SatelliteProvider.Common.DTO;
|
||||
using SatelliteProvider.Common.Utils;
|
||||
|
||||
namespace SatelliteProvider.Services.RouteManagement;
|
||||
|
||||
public class GeofenceGridCalculator
|
||||
{
|
||||
public IReadOnlyList<GeoPoint> GenerateRegions(GeoPoint northWest, GeoPoint southEast, double regionSizeMeters)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(northWest);
|
||||
ArgumentNullException.ThrowIfNull(southEast);
|
||||
if (regionSizeMeters <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(regionSizeMeters), "Region size must be positive");
|
||||
}
|
||||
|
||||
var midLon = (northWest.Lon + southEast.Lon) / 2;
|
||||
var midLat = (northWest.Lat + southEast.Lat) / 2;
|
||||
|
||||
var heightMeters = GeoUtils.CalculateDistance(
|
||||
new GeoPoint(northWest.Lat, midLon),
|
||||
new GeoPoint(southEast.Lat, midLon));
|
||||
var widthMeters = GeoUtils.CalculateDistance(
|
||||
new GeoPoint(midLat, northWest.Lon),
|
||||
new GeoPoint(midLat, southEast.Lon));
|
||||
|
||||
var numLatSteps = Math.Max(1, (int)Math.Ceiling(heightMeters / regionSizeMeters));
|
||||
var numLonSteps = Math.Max(1, (int)Math.Ceiling(widthMeters / regionSizeMeters));
|
||||
|
||||
var latStep = (northWest.Lat - southEast.Lat) / numLatSteps;
|
||||
var lonStep = (southEast.Lon - northWest.Lon) / numLonSteps;
|
||||
|
||||
var regions = new List<GeoPoint>(numLatSteps * numLonSteps);
|
||||
for (int latIdx = 0; latIdx < numLatSteps; latIdx++)
|
||||
{
|
||||
for (int lonIdx = 0; lonIdx < numLonSteps; lonIdx++)
|
||||
{
|
||||
var lat = northWest.Lat - (latIdx + 0.5) * latStep;
|
||||
var lon = northWest.Lon + (lonIdx + 0.5) * lonStep;
|
||||
regions.Add(new GeoPoint(lat, lon));
|
||||
}
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using SatelliteProvider.Common.DTO;
|
||||
using SatelliteProvider.Common.Utils;
|
||||
|
||||
namespace SatelliteProvider.Services.RouteManagement;
|
||||
|
||||
public class RoutePointGraphBuilder
|
||||
{
|
||||
public const double MaxPointSpacingMeters = 200.0;
|
||||
|
||||
public RoutePointGraph Build(IReadOnlyList<RoutePoint> userPoints)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userPoints);
|
||||
if (userPoints.Count < 2)
|
||||
{
|
||||
throw new ArgumentException("Route must have at least 2 points", nameof(userPoints));
|
||||
}
|
||||
|
||||
var allPoints = new List<RoutePointDto>();
|
||||
var totalDistance = 0.0;
|
||||
var sequenceNumber = 0;
|
||||
|
||||
for (int segmentIndex = 0; segmentIndex < userPoints.Count; segmentIndex++)
|
||||
{
|
||||
var current = userPoints[segmentIndex];
|
||||
var isStart = segmentIndex == 0;
|
||||
var isEnd = segmentIndex == userPoints.Count - 1;
|
||||
|
||||
var currentGeo = new GeoPoint(current.Latitude, current.Longitude);
|
||||
|
||||
double? distanceFromPrevious = null;
|
||||
if (allPoints.Count > 0)
|
||||
{
|
||||
var prev = allPoints[^1];
|
||||
var prevGeo = new GeoPoint(prev.Latitude, prev.Longitude);
|
||||
distanceFromPrevious = GeoUtils.CalculateDistance(prevGeo, currentGeo);
|
||||
totalDistance += distanceFromPrevious.Value;
|
||||
}
|
||||
|
||||
allPoints.Add(new RoutePointDto
|
||||
{
|
||||
Latitude = current.Latitude,
|
||||
Longitude = current.Longitude,
|
||||
PointType = isStart ? "start" : (isEnd ? "end" : "action"),
|
||||
SequenceNumber = sequenceNumber++,
|
||||
SegmentIndex = segmentIndex,
|
||||
DistanceFromPrevious = distanceFromPrevious,
|
||||
});
|
||||
|
||||
if (isEnd)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var next = userPoints[segmentIndex + 1];
|
||||
var nextGeo = new GeoPoint(next.Latitude, next.Longitude);
|
||||
var intermediates = GeoUtils.CalculateIntermediatePoints(currentGeo, nextGeo, MaxPointSpacingMeters);
|
||||
|
||||
foreach (var intermediateGeo in intermediates)
|
||||
{
|
||||
var prev = allPoints[^1];
|
||||
var prevGeo = new GeoPoint(prev.Latitude, prev.Longitude);
|
||||
var distFromPrev = GeoUtils.CalculateDistance(prevGeo, intermediateGeo);
|
||||
totalDistance += distFromPrev;
|
||||
|
||||
allPoints.Add(new RoutePointDto
|
||||
{
|
||||
Latitude = intermediateGeo.Lat,
|
||||
Longitude = intermediateGeo.Lon,
|
||||
PointType = "intermediate",
|
||||
SequenceNumber = sequenceNumber++,
|
||||
SegmentIndex = segmentIndex,
|
||||
DistanceFromPrevious = distFromPrev,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new RoutePointGraph(allPoints, totalDistance);
|
||||
}
|
||||
}
|
||||
|
||||
public record RoutePointGraph(IReadOnlyList<RoutePointDto> Points, double TotalDistanceMeters);
|
||||
@@ -0,0 +1,50 @@
|
||||
using SatelliteProvider.Common.DTO;
|
||||
using SatelliteProvider.DataAccess.Models;
|
||||
|
||||
namespace SatelliteProvider.Services.RouteManagement;
|
||||
|
||||
public class RouteResponseMapper
|
||||
{
|
||||
public RouteResponse Map(RouteEntity route, IEnumerable<RoutePointDto> points)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(route);
|
||||
ArgumentNullException.ThrowIfNull(points);
|
||||
|
||||
var pointList = points as List<RoutePointDto> ?? points.ToList();
|
||||
|
||||
return new RouteResponse
|
||||
{
|
||||
Id = route.Id,
|
||||
Name = route.Name,
|
||||
Description = route.Description,
|
||||
RegionSizeMeters = route.RegionSizeMeters,
|
||||
ZoomLevel = route.ZoomLevel,
|
||||
TotalDistanceMeters = route.TotalDistanceMeters,
|
||||
TotalPoints = route.TotalPoints,
|
||||
Points = pointList,
|
||||
RequestMaps = route.RequestMaps,
|
||||
MapsReady = route.MapsReady,
|
||||
CsvFilePath = route.CsvFilePath,
|
||||
SummaryFilePath = route.SummaryFilePath,
|
||||
StitchedImagePath = route.StitchedImagePath,
|
||||
TilesZipPath = route.TilesZipPath,
|
||||
CreatedAt = route.CreatedAt,
|
||||
UpdatedAt = route.UpdatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
public RouteResponse Map(RouteEntity route, IEnumerable<RoutePointEntity> entities)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entities);
|
||||
var pointDtos = entities.Select(p => new RoutePointDto
|
||||
{
|
||||
Latitude = p.Latitude,
|
||||
Longitude = p.Longitude,
|
||||
PointType = p.PointType,
|
||||
SequenceNumber = p.SequenceNumber,
|
||||
SegmentIndex = p.SegmentIndex,
|
||||
DistanceFromPrevious = p.DistanceFromPrevious,
|
||||
}).ToList();
|
||||
return Map(route, pointDtos);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SatelliteProvider.Common.DTO;
|
||||
using SatelliteProvider.Common.Interfaces;
|
||||
using SatelliteProvider.Common.Utils;
|
||||
using SatelliteProvider.DataAccess.Models;
|
||||
using SatelliteProvider.DataAccess.Repositories;
|
||||
|
||||
@@ -12,20 +11,45 @@ public class RouteService : IRouteService
|
||||
private readonly IRouteRepository _routeRepository;
|
||||
private readonly IRegionService _regionService;
|
||||
private readonly ILogger<RouteService> _logger;
|
||||
private const double MAX_POINT_SPACING_METERS = 200.0;
|
||||
private readonly RouteValidator _validator;
|
||||
private readonly RoutePointGraphBuilder _pointGraphBuilder;
|
||||
private readonly GeofenceGridCalculator _geofenceGridCalculator;
|
||||
private readonly RouteResponseMapper _responseMapper;
|
||||
|
||||
public RouteService(
|
||||
IRouteRepository routeRepository,
|
||||
IRegionService regionService,
|
||||
ILogger<RouteService> logger)
|
||||
: this(routeRepository, regionService, logger,
|
||||
new RouteValidator(),
|
||||
new RoutePointGraphBuilder(),
|
||||
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.
|
||||
@@ -38,85 +62,9 @@ public class RouteService : IRouteService
|
||||
return existing;
|
||||
}
|
||||
|
||||
if (request.Points.Count < 2)
|
||||
{
|
||||
throw new ArgumentException("Route must have at least 2 points");
|
||||
}
|
||||
_validator.Validate(request);
|
||||
|
||||
if (request.RegionSizeMeters < 100 || request.RegionSizeMeters > 10000)
|
||||
{
|
||||
throw new ArgumentException("Region size must be between 100 and 10000 meters");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new ArgumentException("Route name is required");
|
||||
}
|
||||
|
||||
|
||||
var allPoints = new List<RoutePointDto>();
|
||||
var totalDistance = 0.0;
|
||||
var sequenceNumber = 0;
|
||||
|
||||
for (int segmentIndex = 0; segmentIndex < request.Points.Count; segmentIndex++)
|
||||
{
|
||||
var currentPoint = request.Points[segmentIndex];
|
||||
var isStart = segmentIndex == 0;
|
||||
var isEnd = segmentIndex == request.Points.Count - 1;
|
||||
|
||||
var geoPoint = new GeoPoint(currentPoint.Latitude, currentPoint.Longitude);
|
||||
|
||||
double? distanceFromPrevious = null;
|
||||
if (allPoints.Count > 0)
|
||||
{
|
||||
var lastAddedPoint = allPoints[^1];
|
||||
var prevGeoPoint = new GeoPoint(lastAddedPoint.Latitude, lastAddedPoint.Longitude);
|
||||
distanceFromPrevious = GeoUtils.CalculateDistance(prevGeoPoint, geoPoint);
|
||||
totalDistance += distanceFromPrevious.Value;
|
||||
}
|
||||
|
||||
var pointType = isStart ? "start" : (isEnd ? "end" : "action");
|
||||
|
||||
var routePointDto = new RoutePointDto
|
||||
{
|
||||
Latitude = currentPoint.Latitude,
|
||||
Longitude = currentPoint.Longitude,
|
||||
PointType = pointType,
|
||||
SequenceNumber = sequenceNumber++,
|
||||
SegmentIndex = segmentIndex,
|
||||
DistanceFromPrevious = distanceFromPrevious
|
||||
};
|
||||
|
||||
allPoints.Add(routePointDto);
|
||||
|
||||
if (!isEnd)
|
||||
{
|
||||
var nextPoint = request.Points[segmentIndex + 1];
|
||||
var startGeo = new GeoPoint(currentPoint.Latitude, currentPoint.Longitude);
|
||||
var endGeo = new GeoPoint(nextPoint.Latitude, nextPoint.Longitude);
|
||||
|
||||
var intermediatePoints = GeoUtils.CalculateIntermediatePoints(startGeo, endGeo, MAX_POINT_SPACING_METERS);
|
||||
|
||||
foreach (var intermediateGeo in intermediatePoints)
|
||||
{
|
||||
var lastAddedPoint = allPoints[^1];
|
||||
var prevGeo = new GeoPoint(lastAddedPoint.Latitude, lastAddedPoint.Longitude);
|
||||
|
||||
var distFromPrev = GeoUtils.CalculateDistance(prevGeo, intermediateGeo);
|
||||
totalDistance += distFromPrev;
|
||||
|
||||
allPoints.Add(new RoutePointDto
|
||||
{
|
||||
Latitude = intermediateGeo.Lat,
|
||||
Longitude = intermediateGeo.Lon,
|
||||
PointType = "intermediate",
|
||||
SequenceNumber = sequenceNumber++,
|
||||
SegmentIndex = segmentIndex,
|
||||
DistanceFromPrevious = distFromPrev
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
var graph = _pointGraphBuilder.Build(request.Points);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var routeEntity = new RouteEntity
|
||||
@@ -126,18 +74,18 @@ public class RouteService : IRouteService
|
||||
Description = request.Description,
|
||||
RegionSizeMeters = request.RegionSizeMeters,
|
||||
ZoomLevel = request.ZoomLevel,
|
||||
TotalDistanceMeters = totalDistance,
|
||||
TotalPoints = allPoints.Count,
|
||||
TotalDistanceMeters = graph.TotalDistanceMeters,
|
||||
TotalPoints = graph.Points.Count,
|
||||
RequestMaps = request.RequestMaps,
|
||||
CreateTilesZip = request.CreateTilesZip,
|
||||
MapsReady = false,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
UpdatedAt = now,
|
||||
};
|
||||
|
||||
await _routeRepository.InsertRouteAsync(routeEntity);
|
||||
|
||||
var pointEntities = allPoints.Select(p => new RoutePointEntity
|
||||
var pointEntities = graph.Points.Select(p => new RoutePointEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
RouteId = request.Id,
|
||||
@@ -147,79 +95,14 @@ public class RouteService : IRouteService
|
||||
PointType = p.PointType,
|
||||
SegmentIndex = p.SegmentIndex,
|
||||
DistanceFromPrevious = p.DistanceFromPrevious,
|
||||
CreatedAt = now
|
||||
CreatedAt = now,
|
||||
}).ToList();
|
||||
|
||||
|
||||
await _routeRepository.InsertRoutePointsAsync(pointEntities);
|
||||
|
||||
if (request.Geofences?.Polygons != null && request.Geofences.Polygons.Count > 0)
|
||||
{
|
||||
for (int polygonIndex = 0; polygonIndex < request.Geofences.Polygons.Count; polygonIndex++)
|
||||
{
|
||||
var polygon = request.Geofences.Polygons[polygonIndex];
|
||||
|
||||
if (polygon.NorthWest is null || polygon.SouthEast is null)
|
||||
{
|
||||
throw new ArgumentException("Geofence polygon coordinates are required");
|
||||
}
|
||||
await ProcessGeofencePolygonsAsync(request);
|
||||
|
||||
if ((Math.Abs(polygon.NorthWest.Lat) < 0.0001 && Math.Abs(polygon.NorthWest.Lon) < 0.0001) ||
|
||||
(Math.Abs(polygon.SouthEast.Lat) < 0.0001 && Math.Abs(polygon.SouthEast.Lon) < 0.0001))
|
||||
{
|
||||
throw new ArgumentException("Geofence polygon coordinates cannot be (0,0)");
|
||||
}
|
||||
|
||||
if (polygon.NorthWest.Lat < -90 || polygon.NorthWest.Lat > 90 ||
|
||||
polygon.SouthEast.Lat < -90 || polygon.SouthEast.Lat > 90 ||
|
||||
polygon.NorthWest.Lon < -180 || polygon.NorthWest.Lon > 180 ||
|
||||
polygon.SouthEast.Lon < -180 || polygon.SouthEast.Lon > 180)
|
||||
{
|
||||
throw new ArgumentException("Geofence polygon coordinates must be valid (lat: -90 to 90, lon: -180 to 180)");
|
||||
}
|
||||
|
||||
if (polygon.NorthWest.Lat <= polygon.SouthEast.Lat)
|
||||
{
|
||||
throw new ArgumentException("Geofence northWest latitude must be greater than southEast latitude");
|
||||
}
|
||||
|
||||
var geofenceRegions = CreateGeofenceRegionGrid(polygon.NorthWest, polygon.SouthEast, request.RegionSizeMeters);
|
||||
|
||||
foreach (var geofencePoint in geofenceRegions)
|
||||
{
|
||||
var geofenceRegionId = Guid.NewGuid();
|
||||
|
||||
await _regionService.RequestRegionAsync(
|
||||
geofenceRegionId,
|
||||
geofencePoint.Lat,
|
||||
geofencePoint.Lon,
|
||||
request.RegionSizeMeters,
|
||||
request.ZoomLevel,
|
||||
stitchTiles: false);
|
||||
|
||||
await _routeRepository.LinkRouteToRegionAsync(request.Id, geofenceRegionId, isGeofence: true, geofencePolygonIndex: polygonIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new RouteResponse
|
||||
{
|
||||
Id = routeEntity.Id,
|
||||
Name = routeEntity.Name,
|
||||
Description = routeEntity.Description,
|
||||
RegionSizeMeters = routeEntity.RegionSizeMeters,
|
||||
ZoomLevel = routeEntity.ZoomLevel,
|
||||
TotalDistanceMeters = routeEntity.TotalDistanceMeters,
|
||||
TotalPoints = routeEntity.TotalPoints,
|
||||
Points = allPoints,
|
||||
RequestMaps = routeEntity.RequestMaps,
|
||||
MapsReady = routeEntity.MapsReady,
|
||||
CsvFilePath = routeEntity.CsvFilePath,
|
||||
SummaryFilePath = routeEntity.SummaryFilePath,
|
||||
StitchedImagePath = routeEntity.StitchedImagePath,
|
||||
TilesZipPath = routeEntity.TilesZipPath,
|
||||
CreatedAt = routeEntity.CreatedAt,
|
||||
UpdatedAt = routeEntity.UpdatedAt
|
||||
};
|
||||
return _responseMapper.Map(routeEntity, graph.Points);
|
||||
}
|
||||
|
||||
public async Task<RouteResponse?> GetRouteAsync(Guid id)
|
||||
@@ -231,65 +114,44 @@ public class RouteService : IRouteService
|
||||
}
|
||||
|
||||
var points = await _routeRepository.GetRoutePointsAsync(id);
|
||||
|
||||
return new RouteResponse
|
||||
{
|
||||
Id = route.Id,
|
||||
Name = route.Name,
|
||||
Description = route.Description,
|
||||
RegionSizeMeters = route.RegionSizeMeters,
|
||||
ZoomLevel = route.ZoomLevel,
|
||||
TotalDistanceMeters = route.TotalDistanceMeters,
|
||||
TotalPoints = route.TotalPoints,
|
||||
Points = points.Select(p => new RoutePointDto
|
||||
{
|
||||
Latitude = p.Latitude,
|
||||
Longitude = p.Longitude,
|
||||
PointType = p.PointType,
|
||||
SequenceNumber = p.SequenceNumber,
|
||||
SegmentIndex = p.SegmentIndex,
|
||||
DistanceFromPrevious = p.DistanceFromPrevious
|
||||
}).ToList(),
|
||||
RequestMaps = route.RequestMaps,
|
||||
MapsReady = route.MapsReady,
|
||||
CsvFilePath = route.CsvFilePath,
|
||||
SummaryFilePath = route.SummaryFilePath,
|
||||
StitchedImagePath = route.StitchedImagePath,
|
||||
TilesZipPath = route.TilesZipPath,
|
||||
CreatedAt = route.CreatedAt,
|
||||
UpdatedAt = route.UpdatedAt
|
||||
};
|
||||
return _responseMapper.Map(route, points);
|
||||
}
|
||||
|
||||
private List<GeoPoint> CreateGeofenceRegionGrid(GeoPoint northWest, GeoPoint southEast, double regionSizeMeters)
|
||||
private async Task ProcessGeofencePolygonsAsync(CreateRouteRequest request)
|
||||
{
|
||||
var regions = new List<GeoPoint>();
|
||||
|
||||
var northPoint = new GeoPoint(northWest.Lat, (northWest.Lon + southEast.Lon) / 2);
|
||||
var southPoint = new GeoPoint(southEast.Lat, (northWest.Lon + southEast.Lon) / 2);
|
||||
var heightMeters = GeoUtils.CalculateDistance(northPoint, southPoint);
|
||||
|
||||
var westPoint = new GeoPoint((northWest.Lat + southEast.Lat) / 2, northWest.Lon);
|
||||
var eastPoint = new GeoPoint((northWest.Lat + southEast.Lat) / 2, southEast.Lon);
|
||||
var widthMeters = GeoUtils.CalculateDistance(westPoint, eastPoint);
|
||||
|
||||
var numLatSteps = Math.Max(1, (int)Math.Ceiling(heightMeters / regionSizeMeters));
|
||||
var numLonSteps = Math.Max(1, (int)Math.Ceiling(widthMeters / regionSizeMeters));
|
||||
|
||||
var latStep = (northWest.Lat - southEast.Lat) / numLatSteps;
|
||||
var lonStep = (southEast.Lon - northWest.Lon) / numLonSteps;
|
||||
|
||||
for (int latIdx = 0; latIdx < numLatSteps; latIdx++)
|
||||
var polygons = request.Geofences?.Polygons;
|
||||
if (polygons is null || polygons.Count == 0)
|
||||
{
|
||||
for (int lonIdx = 0; lonIdx < numLonSteps; lonIdx++)
|
||||
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 lat = northWest.Lat - (latIdx + 0.5) * latStep;
|
||||
var lon = northWest.Lon + (lonIdx + 0.5) * lonStep;
|
||||
regions.Add(new GeoPoint(lat, lon));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using SatelliteProvider.Common.DTO;
|
||||
|
||||
namespace SatelliteProvider.Services.RouteManagement;
|
||||
|
||||
public class RouteValidator
|
||||
{
|
||||
public void Validate(CreateRouteRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var errors = new List<string>();
|
||||
|
||||
if (request.Points is null || request.Points.Count < 2)
|
||||
{
|
||||
errors.Add("Route must have at least 2 points");
|
||||
}
|
||||
|
||||
if (request.RegionSizeMeters < 100 || request.RegionSizeMeters > 10000)
|
||||
{
|
||||
errors.Add("Region size must be between 100 and 10000 meters");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
errors.Add("Route name is required");
|
||||
}
|
||||
|
||||
if (request.Geofences?.Polygons is { Count: > 0 } polygons)
|
||||
{
|
||||
for (int i = 0; i < polygons.Count; i++)
|
||||
{
|
||||
ValidatePolygon(polygons[i], errors);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
throw new ArgumentException(string.Join("; ", errors));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidatePolygon(GeofencePolygon polygon, List<string> errors)
|
||||
{
|
||||
if (polygon.NorthWest is null || polygon.SouthEast is null)
|
||||
{
|
||||
errors.Add("Geofence polygon coordinates are required");
|
||||
return;
|
||||
}
|
||||
|
||||
var nw = polygon.NorthWest;
|
||||
var se = polygon.SouthEast;
|
||||
|
||||
if ((Math.Abs(nw.Lat) < 0.0001 && Math.Abs(nw.Lon) < 0.0001) ||
|
||||
(Math.Abs(se.Lat) < 0.0001 && Math.Abs(se.Lon) < 0.0001))
|
||||
{
|
||||
errors.Add("Geofence polygon coordinates cannot be (0,0)");
|
||||
}
|
||||
|
||||
if (nw.Lat < -90 || nw.Lat > 90 ||
|
||||
se.Lat < -90 || se.Lat > 90 ||
|
||||
nw.Lon < -180 || nw.Lon > 180 ||
|
||||
se.Lon < -180 || se.Lon > 180)
|
||||
{
|
||||
errors.Add("Geofence polygon coordinates must be valid (lat: -90 to 90, lon: -180 to 180)");
|
||||
}
|
||||
|
||||
if (nw.Lat <= se.Lat)
|
||||
{
|
||||
errors.Add("Geofence northWest latitude must be greater than southEast latitude");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user