using Microsoft.Extensions.Options; using SatelliteProvider.Common.Configs; using SatelliteProvider.Common.DTO; using SatelliteProvider.Common.Utils; namespace SatelliteProvider.Services.RouteManagement.TileProvision; public sealed class RouteTileExpander { private readonly RoutePointGraphBuilder _pointGraphBuilder; private readonly GeofenceGridCalculator _geofenceGridCalculator; public RouteTileExpander( RoutePointGraphBuilder pointGraphBuilder, GeofenceGridCalculator geofenceGridCalculator) { _pointGraphBuilder = pointGraphBuilder; _geofenceGridCalculator = geofenceGridCalculator; } public IReadOnlyList Expand( IReadOnlyList<(double Lat, double Lon)> waypoints, double regionSizeMeters, int zoom, IReadOnlyList> geofenceVertices, bool includeGeofenceTiles) { if (waypoints.Count < 2) { throw new ArgumentException("Route must have at least 2 waypoints", nameof(waypoints)); } if (regionSizeMeters <= 0) { throw new ArgumentOutOfRangeException(nameof(regionSizeMeters), "Region size must be positive"); } var routePoints = waypoints .Select(w => new RoutePoint { Latitude = w.Lat, Longitude = w.Lon }) .ToList(); var graph = _pointGraphBuilder.Build(routePoints); var tiles = new Dictionary<(int Z, int X, int Y), uint>(); for (var priority = 0; priority < graph.Points.Count; priority++) { var point = graph.Points[priority]; AddCorridorTiles( tiles, new GeoPoint(point.Latitude, point.Longitude), regionSizeMeters, zoom, (uint)priority); } if (includeGeofenceTiles) { var geofencePriority = (uint)graph.Points.Count; foreach (var vertices in geofenceVertices) { if (vertices.Count < 3) { continue; } var minLat = vertices.Min(v => v.Lat); var maxLat = vertices.Max(v => v.Lat); var minLon = vertices.Min(v => v.Lon); var maxLon = vertices.Max(v => v.Lon); var northWest = new GeoPoint(maxLat, minLon); var southEast = new GeoPoint(minLat, maxLon); var centers = _geofenceGridCalculator.GenerateRegions(northWest, southEast, regionSizeMeters); foreach (var center in centers) { AddCorridorTiles(tiles, center, regionSizeMeters, zoom, geofencePriority); } geofencePriority++; } } return tiles .OrderBy(t => t.Value) .ThenBy(t => t.Key.Y) .ThenBy(t => t.Key.X) .Select(t => new RouteTileCandidate(t.Key.Z, t.Key.X, t.Key.Y, t.Value)) .ToList(); } private static void AddCorridorTiles( Dictionary<(int Z, int X, int Y), uint> tiles, GeoPoint center, double regionSizeMeters, int zoom, uint routePriority) { var radiusMeters = regionSizeMeters / 2.0; var (latMin, latMax, lonMin, lonMax) = GeoUtils.GetBoundingBox(center, radiusMeters); var (xMin, yMin) = GeoUtils.WorldToTilePos(new GeoPoint(latMax, lonMin), zoom); var (xMax, yMax) = GeoUtils.WorldToTilePos(new GeoPoint(latMin, lonMax), zoom); for (var y = yMin; y <= yMax; y++) { for (var x = xMin; x <= xMax; x++) { var key = (zoom, x, y); if (!tiles.TryGetValue(key, out var existing) || routePriority < existing) { tiles[key] = routePriority; } } } } }