using System.Collections.Concurrent; using System.Net.Http.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; using SatelliteProvider.Common.Configs; using SatelliteProvider.Common.DTO; using SatelliteProvider.Common.Interfaces; using SatelliteProvider.Common.Utils; using SixLabors.ImageSharp; namespace SatelliteProvider.Services; public record DownloadedTileInfo(int X, int Y, int ZoomLevel, double Latitude, double Longitude, string FilePath, double TileSizeMeters); public class GoogleMapsDownloader(ILogger logger, IOptions mapConfig, IOptions storageConfig, IHttpClientFactory httpClientFactory) : ISatelliteDownloader { private const string TILE_URL_TEMPLATE = "https://mt{0}.google.com/vt/lyrs=s&x={1}&y={2}&z={3}&token={4}"; private const string USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36"; private const int NUM_SERVERS = 4; private const int TILE_SIZE_PIXELS = 256; private readonly string _apiKey = mapConfig.Value.ApiKey; private readonly string _tilesDirectory = storageConfig.Value.TilesDirectory; private record SessionResponse(string Session); private async Task GetSessionToken() { var url = $"https://tile.googleapis.com/v1/createSession?key={_apiKey}"; using var httpClient = httpClientFactory.CreateClient(); try { var str = JsonConvert.SerializeObject(new { mapType = "satellite" }); var response = await httpClient.PostAsync(url, new StringContent(str)); response.EnsureSuccessStatusCode(); var sessionResponse = await response.Content.ReadFromJsonAsync(); return sessionResponse?.Session; } catch (Exception e) { logger.LogError(e, "Failed to get session token"); throw; } } public async Task GetTiles(GeoPoint centerGeoPoint, double radiusM, int zoomLevel, CancellationToken token = default) { await GetTilesWithMetadataAsync(centerGeoPoint, radiusM, zoomLevel, token); } public async Task> GetTilesWithMetadataAsync(GeoPoint centerGeoPoint, double radiusM, int zoomLevel, CancellationToken token = default) { var (latMin, latMax, lonMin, lonMax) = GeoUtils.GetBoundingBox(centerGeoPoint, radiusM); var (xMin, yMin) = GeoUtils.WorldToTilePos(new GeoPoint(latMax, lonMin), zoomLevel); var (xMax, yMax) = GeoUtils.WorldToTilePos(new GeoPoint(latMin, lonMax), zoomLevel); var tilesToDownload = new ConcurrentQueue(); var downloadedTiles = new ConcurrentBag(); var server = 0; var sessionToken = await GetSessionToken(); for (var y = yMin; y <= yMax + 1; y++) for (var x = xMin; x <= xMax + 1; x++) { token.ThrowIfCancellationRequested(); var url = string.Format(TILE_URL_TEMPLATE, server, x, y, zoomLevel, sessionToken); tilesToDownload.Enqueue(new SatTile(x, y, zoomLevel, url)); server = (server + 1) % NUM_SERVERS; } var downloadTasks = new List(); for (int i = 0; i < NUM_SERVERS; i++) { downloadTasks.Add(Task.Run(() => DownloadTilesWorker(tilesToDownload, downloadedTiles, zoomLevel, token), token)); } await Task.WhenAll(downloadTasks); return downloadedTiles.ToList(); } private async Task DownloadTilesWorker(ConcurrentQueue tilesToDownload, ConcurrentBag downloadedTiles, int zoomLevel, CancellationToken token) { using var httpClient = httpClientFactory.CreateClient(); while (tilesToDownload.TryDequeue(out var tileInfo)) { if (token.IsCancellationRequested) break; try { Directory.CreateDirectory(_tilesDirectory); var timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss"); var fileName = $"tile_{tileInfo.Zoom}_{tileInfo.X}_{tileInfo.Y}_{timestamp}.jpg"; var filePath = Path.Combine(_tilesDirectory, fileName); httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT); var response = await httpClient.GetAsync(tileInfo.Url, token); response.EnsureSuccessStatusCode(); var tileData = await response.Content.ReadAsByteArrayAsync(token); using var tileImage = Image.Load(tileData); await tileImage.SaveAsync(filePath, token); var tileCenter = GeoUtils.TileToWorldPos(tileInfo.X, tileInfo.Y, tileInfo.Zoom); var tileSizeMeters = CalculateTileSizeInMeters(tileInfo.Zoom, tileCenter.Lat); var downloadedTile = new DownloadedTileInfo( tileInfo.X, tileInfo.Y, tileInfo.Zoom, tileCenter.Lat, tileCenter.Lon, filePath, tileSizeMeters ); downloadedTiles.Add(downloadedTile); } catch (HttpRequestException requestException) { logger.LogError(requestException, "Failed to download tile. Url: {Url}", tileInfo.Url); } catch (Exception e) { logger.LogError(e, "Failed to download tile"); } } } private static double CalculateTileSizeInMeters(int zoomLevel, double latitude) { const double EARTH_CIRCUMFERENCE_METERS = 40075016.686; var latRad = latitude * Math.PI / 180.0; var metersPerPixel = (EARTH_CIRCUMFERENCE_METERS * Math.Cos(latRad)) / (Math.Pow(2, zoomLevel) * TILE_SIZE_PIXELS); return metersPerPixel * TILE_SIZE_PIXELS; } }