using Microsoft.Extensions.Logging; using SatelliteProvider.Common.DTO; using SatelliteProvider.Common.Interfaces; using SatelliteProvider.Common.Utils; using SatelliteProvider.DataAccess.Models; using SatelliteProvider.DataAccess.Repositories; namespace SatelliteProvider.Services; public class RouteService : IRouteService { private readonly IRouteRepository _routeRepository; private readonly IRegionService _regionService; private readonly ILogger _logger; private const double MAX_POINT_SPACING_METERS = 200.0; public RouteService( IRouteRepository routeRepository, IRegionService regionService, ILogger logger) { _routeRepository = routeRepository; _regionService = regionService; _logger = logger; } public async Task CreateRouteAsync(CreateRouteRequest request) { if (request.Points.Count < 2) { throw new ArgumentException("Route must have at least 2 points"); } 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(); 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 now = DateTime.UtcNow; var routeEntity = new RouteEntity { Id = request.Id, Name = request.Name, Description = request.Description, RegionSizeMeters = request.RegionSizeMeters, ZoomLevel = request.ZoomLevel, TotalDistanceMeters = totalDistance, TotalPoints = allPoints.Count, RequestMaps = request.RequestMaps, CreateTilesZip = request.CreateTilesZip, MapsReady = false, CreatedAt = now, UpdatedAt = now }; await _routeRepository.InsertRouteAsync(routeEntity); var pointEntities = allPoints.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); 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"); } 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 }; } public async Task GetRouteAsync(Guid id) { var route = await _routeRepository.GetByIdAsync(id); if (route == null) { return null; } 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 }; } private List CreateGeofenceRegionGrid(GeoPoint northWest, GeoPoint southEast, double regionSizeMeters) { var regions = new List(); 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++) { 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; } }