From 12f3bf890a8a645fc55d72bfd73f2f4632cc5646 Mon Sep 17 00:00:00 2001 From: Anton Martynenko Date: Tue, 28 Oct 2025 15:10:50 +0100 Subject: [PATCH] downloaderV2, download single tile --- SatelliteProvider.Api/Program.cs | 51 +++--- .../GoogleMapsDownloaderV2.cs | 148 ++++++++++++++++++ 2 files changed, 179 insertions(+), 20 deletions(-) create mode 100644 SatelliteProvider.Services/GoogleMapsDownloaderV2.cs diff --git a/SatelliteProvider.Api/Program.cs b/SatelliteProvider.Api/Program.cs index 1e7c86e..765c021 100644 --- a/SatelliteProvider.Api/Program.cs +++ b/SatelliteProvider.Api/Program.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using SatelliteProvider.DataAccess; +using SatelliteProvider.DataAccess.Models; using SatelliteProvider.DataAccess.Repositories; using SatelliteProvider.Common.Configs; using SatelliteProvider.Common.Interfaces; @@ -22,6 +23,7 @@ builder.Services.AddSingleton(sp => new RegionRepository(conn builder.Services.AddHttpClient(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddEndpointsApiExplorer(); @@ -97,41 +99,50 @@ IResult UploadImage([FromForm] UploadImageRequest request) return Results.Ok(new SaveResult { Success = false }); } -async Task DownloadSingleTile([FromBody] DownloadTileRequest request, ITileService tileService, ILogger logger) +async Task DownloadSingleTile([FromBody] DownloadTileRequest request, GoogleMapsDownloaderV2 downloader, ITileRepository tileRepository, ILogger logger) { try { logger.LogInformation("Downloading single tile at ({Lat}, {Lon}) with zoom level {Zoom}", request.Latitude, request.Longitude, request.ZoomLevel); - var tiles = await tileService.DownloadAndStoreTilesAsync( + var downloadedTile = await downloader.DownloadSingleTileAsync( request.Latitude, request.Longitude, - 1.0, request.ZoomLevel); - if (tiles.Count == 0) + var now = DateTime.UtcNow; + var tileEntity = new TileEntity { - logger.LogWarning("No tiles were downloaded"); - return Results.NotFound(new { message = "No tiles were downloaded" }); - } + Id = Guid.NewGuid(), + ZoomLevel = downloadedTile.ZoomLevel, + Latitude = downloadedTile.CenterLatitude, + Longitude = downloadedTile.CenterLongitude, + TileSizeMeters = downloadedTile.TileSizeMeters, + TileSizePixels = 256, + ImageType = "jpg", + MapsVersion = $"downloaded_{now:yyyy-MM-dd}", + FilePath = downloadedTile.FilePath, + CreatedAt = now, + UpdatedAt = now + }; - var tile = tiles[0]; - logger.LogInformation("Tile downloaded successfully: {Id}", tile.Id); + await tileRepository.InsertAsync(tileEntity); + logger.LogInformation("Tile saved to database with ID: {Id}", tileEntity.Id); var response = new DownloadTileResponse { - Id = tile.Id, - ZoomLevel = tile.ZoomLevel, - Latitude = tile.Latitude, - Longitude = tile.Longitude, - TileSizeMeters = tile.TileSizeMeters, - TileSizePixels = tile.TileSizePixels, - ImageType = tile.ImageType, - MapsVersion = tile.MapsVersion, - FilePath = tile.FilePath, - CreatedAt = tile.CreatedAt, - UpdatedAt = tile.UpdatedAt + Id = tileEntity.Id, + ZoomLevel = tileEntity.ZoomLevel, + Latitude = tileEntity.Latitude, + Longitude = tileEntity.Longitude, + TileSizeMeters = tileEntity.TileSizeMeters, + TileSizePixels = tileEntity.TileSizePixels, + ImageType = tileEntity.ImageType, + MapsVersion = tileEntity.MapsVersion, + FilePath = tileEntity.FilePath, + CreatedAt = tileEntity.CreatedAt, + UpdatedAt = tileEntity.UpdatedAt }; return Results.Ok(response); diff --git a/SatelliteProvider.Services/GoogleMapsDownloaderV2.cs b/SatelliteProvider.Services/GoogleMapsDownloaderV2.cs new file mode 100644 index 0000000..b5a4190 --- /dev/null +++ b/SatelliteProvider.Services/GoogleMapsDownloaderV2.cs @@ -0,0 +1,148 @@ +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.Utils; + +namespace SatelliteProvider.Services; + +public record DownloadedTileInfoV2(int X, int Y, int ZoomLevel, double CenterLatitude, double CenterLongitude, string FilePath, double TileSizeMeters); + +public class GoogleMapsDownloaderV2 +{ + 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 TILE_SIZE_PIXELS = 256; + private static readonly int[] ALLOWED_ZOOM_LEVELS = { 15, 16, 17, 18, 19 }; + + private readonly ILogger _logger; + private readonly string _apiKey; + private readonly string _tilesDirectory; + private readonly IHttpClientFactory _httpClientFactory; + + public GoogleMapsDownloaderV2( + ILogger logger, + IOptions mapConfig, + IOptions storageConfig, + IHttpClientFactory httpClientFactory) + { + _logger = logger; + _apiKey = mapConfig.Value.ApiKey; + _tilesDirectory = storageConfig.Value.TilesDirectory; + _httpClientFactory = httpClientFactory; + } + + 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 DownloadSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken token = default) + { + if (!ALLOWED_ZOOM_LEVELS.Contains(zoomLevel)) + { + throw new ArgumentException($"Zoom level {zoomLevel} is not allowed. Allowed zoom levels are: {string.Join(", ", ALLOWED_ZOOM_LEVELS)}", nameof(zoomLevel)); + } + + var geoPoint = new GeoPoint(latitude, longitude); + var (tileX, tileY) = GeoUtils.WorldToTilePos(geoPoint, zoomLevel); + + _logger.LogInformation("Downloading single tile at ({Lat}, {Lon}), zoom {Zoom}, tile coordinates ({X}, {Y})", + latitude, longitude, zoomLevel, tileX, tileY); + + var sessionToken = await GetSessionToken(); + var server = 0; + var url = string.Format(TILE_URL_TEMPLATE, server, tileX, tileY, zoomLevel, sessionToken); + + Directory.CreateDirectory(_tilesDirectory); + + var timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss"); + var fileName = $"raw_tile_{zoomLevel}_{tileX}_{tileY}_{timestamp}.jpg"; + var filePath = Path.Combine(_tilesDirectory, fileName); + + using var httpClient = _httpClientFactory.CreateClient(); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT); + + var response = await httpClient.GetAsync(url, token); + response.EnsureSuccessStatusCode(); + + _logger.LogInformation("=== HTTP Response Headers from Google Maps ==="); + _logger.LogInformation("Status Code: {StatusCode}", response.StatusCode); + + foreach (var header in response.Headers) + { + _logger.LogInformation("Header: {Key} = {Value}", header.Key, string.Join(", ", header.Value)); + } + + foreach (var header in response.Content.Headers) + { + _logger.LogInformation("Content Header: {Key} = {Value}", header.Key, string.Join(", ", header.Value)); + } + + if (response.Headers.ETag != null) + { + _logger.LogInformation("*** ETag Found: {ETag}", response.Headers.ETag.Tag); + } + + if (response.Content.Headers.LastModified.HasValue) + { + _logger.LogInformation("*** Last-Modified Found: {LastModified}", response.Content.Headers.LastModified.Value); + } + + if (response.Headers.CacheControl != null) + { + _logger.LogInformation("*** Cache-Control: MaxAge={MaxAge}, Public={Public}, Private={Private}, MustRevalidate={MustRevalidate}", + response.Headers.CacheControl.MaxAge, + response.Headers.CacheControl.Public, + response.Headers.CacheControl.Private, + response.Headers.CacheControl.MustRevalidate); + } + + _logger.LogInformation("=== End of Headers ==="); + + var imageBytes = await response.Content.ReadAsByteArrayAsync(token); + await File.WriteAllBytesAsync(filePath, imageBytes, token); + + var tileCenter = GeoUtils.TileToWorldPos(tileX, tileY, zoomLevel); + var tileSizeMeters = CalculateTileSizeInMeters(zoomLevel, tileCenter.Lat); + + _logger.LogInformation("Downloaded tile to {FilePath}, size: {Size:F2} meters", filePath, tileSizeMeters); + + return new DownloadedTileInfoV2( + tileX, + tileY, + zoomLevel, + tileCenter.Lat, + tileCenter.Lon, + filePath, + tileSizeMeters + ); + } + + 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; + } +} +