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"); } _logger.LogInformation("Creating route {RouteId} with {PointCount} original points and {GeofenceCount} geofences", request.Id, request.Points.Count, request.Geofences?.Polygons?.Count ?? 0); _logger.LogInformation("Route {RouteId} - Input coordinates: First point ({Lat}, {Lon}), Last point ({LastLat}, {LastLon})", request.Id, request.Points[0].Latitude, request.Points[0].Longitude, request.Points[^1].Latitude, request.Points[^1].Longitude); 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 }; if (segmentIndex == 0 || segmentIndex == request.Points.Count - 1) { _logger.LogInformation("Route {RouteId} - Creating {PointType} point: Lat={Lat:F12}, Lon={Lon:F12}", request.Id, pointType, routePointDto.Latitude, routePointDto.Longitude); } 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); _logger.LogInformation("Segment {SegmentIndex}: Adding {Count} intermediate points", segmentIndex, intermediatePoints.Count); 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 }); } } } _logger.LogInformation("Route {RouteId}: Total {TotalPoints} points (original + intermediate), distance {Distance:F2}m", request.Id, allPoints.Count, totalDistance); 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, 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(); _logger.LogInformation("Route {RouteId} - Saving {Count} route points to DB. First: Lat={Lat:F12}, Lon={Lon:F12}, Last: Lat={LastLat:F12}, Lon={LastLon:F12}", request.Id, pointEntities.Count, pointEntities[0].Latitude, pointEntities[0].Longitude, pointEntities[^1].Latitude, pointEntities[^1].Longitude); await _routeRepository.InsertRoutePointsAsync(pointEntities); if (request.Geofences?.Polygons != null && request.Geofences.Polygons.Count > 0) { _logger.LogInformation("Route {RouteId}: Processing {GeofenceCount} geofence polygons", request.Id, request.Geofences.Polygons.Count); foreach (var polygon in request.Geofences.Polygons) { 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 center = GeoUtils.CalculateCenter(polygon.NorthWest, polygon.SouthEast); var diagonalDistance = GeoUtils.CalculatePolygonDiagonalDistance(polygon.NorthWest, polygon.SouthEast); var geofenceRegionSize = Math.Max(diagonalDistance * 0.6, request.RegionSizeMeters); var geofenceRegionId = Guid.NewGuid(); _logger.LogInformation("Route {RouteId}: Requesting geofence region {RegionId} at center ({Lat}, {Lon}) with size {Size}m", request.Id, geofenceRegionId, center.Lat, center.Lon, geofenceRegionSize); await _regionService.RequestRegionAsync( geofenceRegionId, center.Lat, center.Lon, geofenceRegionSize, request.ZoomLevel, stitchTiles: false); await _routeRepository.LinkRouteToRegionAsync(request.Id, geofenceRegionId, isGeofence: true); } } if (request.RequestMaps) { _logger.LogInformation("Route {RouteId}: Maps requested. Regions will be processed sequentially by background service.", request.Id); } _logger.LogInformation("Route {RouteId} created successfully", request.Id); 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, 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, CreatedAt = route.CreatedAt, UpdatedAt = route.UpdatedAt }; } }