From a7a645c7abcd07451e641413ee9c21001ee9d3b5 Mon Sep 17 00:00:00 2001 From: Oleksandr Bezdieniezhnykh Date: Sun, 26 Oct 2025 09:15:06 +0200 Subject: [PATCH] make structure add tests --- .cursor/rules/coderule.mdc | 21 ++++ .cursor/rules/techstackrule.mdc | 9 ++ .vscode/settings.json | 12 ++ .../Program.cs | 0 .../Properties/launchSettings.json | 0 .../SatelliteProvider.Api.csproj | 4 + .../SatelliteProvider.http | 0 .../appsettings.Development.json | 0 .../appsettings.json | 0 .../Configs/MapConfig.cs | 2 +- .../DTO/Direction.cs | 2 +- .../DTO/GeoPoint.cs | 2 +- .../DTO/SatTile.cs | 8 +- .../Interfaces/ISatelliteDownloader.cs | 8 ++ .../SatelliteProvider.Common.csproj | 9 ++ .../Utils}/GeoUtils.cs | 10 +- .../GoogleMapsDownloader.cs | 104 +++++++++++++++++ .../SatelliteProvider.Services.csproj | 21 ++++ .../GoogleMapsDownloaderTests.cs | 57 ++++++++++ .../SatelliteProvider.Tests.csproj | 42 +++++++ SatelliteProvider.Tests/appsettings.json | 5 + SatelliteProvider.sln | 21 +++- .../Configs/DirectoriesConfig.cs | 15 --- SatelliteProvider/SatelliteDownloader.cs | 107 ------------------ 24 files changed, 326 insertions(+), 133 deletions(-) create mode 100644 .cursor/rules/coderule.mdc create mode 100644 .cursor/rules/techstackrule.mdc create mode 100644 .vscode/settings.json rename {SatelliteProvider => SatelliteProvider.Api}/Program.cs (100%) rename {SatelliteProvider => SatelliteProvider.Api}/Properties/launchSettings.json (100%) rename SatelliteProvider/SatelliteProvider.csproj => SatelliteProvider.Api/SatelliteProvider.Api.csproj (81%) rename {SatelliteProvider => SatelliteProvider.Api}/SatelliteProvider.http (100%) rename {SatelliteProvider => SatelliteProvider.Api}/appsettings.Development.json (100%) rename {SatelliteProvider => SatelliteProvider.Api}/appsettings.json (100%) rename {SatelliteProvider => SatelliteProvider.Common}/Configs/MapConfig.cs (72%) rename {SatelliteProvider => SatelliteProvider.Common}/DTO/Direction.cs (89%) rename {SatelliteProvider => SatelliteProvider.Common}/DTO/GeoPoint.cs (95%) rename {SatelliteProvider => SatelliteProvider.Common}/DTO/SatTile.cs (76%) create mode 100644 SatelliteProvider.Common/Interfaces/ISatelliteDownloader.cs create mode 100644 SatelliteProvider.Common/SatelliteProvider.Common.csproj rename {SatelliteProvider => SatelliteProvider.Common/Utils}/GeoUtils.cs (92%) create mode 100644 SatelliteProvider.Services/GoogleMapsDownloader.cs create mode 100644 SatelliteProvider.Services/SatelliteProvider.Services.csproj create mode 100644 SatelliteProvider.Tests/GoogleMapsDownloaderTests.cs create mode 100644 SatelliteProvider.Tests/SatelliteProvider.Tests.csproj create mode 100644 SatelliteProvider.Tests/appsettings.json delete mode 100644 SatelliteProvider/Configs/DirectoriesConfig.cs delete mode 100644 SatelliteProvider/SatelliteDownloader.cs diff --git a/.cursor/rules/coderule.mdc b/.cursor/rules/coderule.mdc new file mode 100644 index 0000000..31c023a --- /dev/null +++ b/.cursor/rules/coderule.mdc @@ -0,0 +1,21 @@ +--- +description: Coding rules +alwaysApply: true +--- +# Coding preferences +- Always prefer simple solution +- Generate concise code +- Do not put comments in the code +- Do not put logs unless it is an exception, or was asked specifically +- Do not put code annotations unless it was asked specifically +- Your changes should be well correlated with what was requested. In case of any uncertainties ask questions. +- Mocking data is needed only for tests +- When you add new libraries or dependencies make sure you are using the same version of it as other parts of the code + +- Focus on the areas of code relevant to the task +- Do not touch code that is unrelated to the task +- Always think about what other methods and areas of code might be affected by the code changes +- When you think you are done with changes, run tests and make sure they are not broken +- Do not rename any databases or tables or table columns without confirmation. Avoid such renaming if possible. +- Do not create diagrams unless I ask explicitly +- Do not commit binaries, create and keep .gitignore up to date and delete binaries after you are done with the task diff --git a/.cursor/rules/techstackrule.mdc b/.cursor/rules/techstackrule.mdc new file mode 100644 index 0000000..46c916c --- /dev/null +++ b/.cursor/rules/techstackrule.mdc @@ -0,0 +1,9 @@ +--- +description: Techstack +alwaysApply: true +--- +# Tech Stack +- Use poetry +- Cython for backend +- Using Postgres database +- document api with OpenAPI \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a72dd7d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "workbench.colorCustomizations": { + "editor.background": "#191A1C", + "sideBar.background": "#191A1C", + "activityBar.background": "#191A1C", + "panel.background": "#191A1C", + "terminal.background": "#191A1C", + "editorGroupHeader.tabsBackground": "#191A1C", + "tab.inactiveBackground": "#191A1C" + } +} + diff --git a/SatelliteProvider/Program.cs b/SatelliteProvider.Api/Program.cs similarity index 100% rename from SatelliteProvider/Program.cs rename to SatelliteProvider.Api/Program.cs diff --git a/SatelliteProvider/Properties/launchSettings.json b/SatelliteProvider.Api/Properties/launchSettings.json similarity index 100% rename from SatelliteProvider/Properties/launchSettings.json rename to SatelliteProvider.Api/Properties/launchSettings.json diff --git a/SatelliteProvider/SatelliteProvider.csproj b/SatelliteProvider.Api/SatelliteProvider.Api.csproj similarity index 81% rename from SatelliteProvider/SatelliteProvider.csproj rename to SatelliteProvider.Api/SatelliteProvider.Api.csproj index 899a851..d329bc4 100644 --- a/SatelliteProvider/SatelliteProvider.csproj +++ b/SatelliteProvider.Api/SatelliteProvider.Api.csproj @@ -13,4 +13,8 @@ + + + + diff --git a/SatelliteProvider/SatelliteProvider.http b/SatelliteProvider.Api/SatelliteProvider.http similarity index 100% rename from SatelliteProvider/SatelliteProvider.http rename to SatelliteProvider.Api/SatelliteProvider.http diff --git a/SatelliteProvider/appsettings.Development.json b/SatelliteProvider.Api/appsettings.Development.json similarity index 100% rename from SatelliteProvider/appsettings.Development.json rename to SatelliteProvider.Api/appsettings.Development.json diff --git a/SatelliteProvider/appsettings.json b/SatelliteProvider.Api/appsettings.json similarity index 100% rename from SatelliteProvider/appsettings.json rename to SatelliteProvider.Api/appsettings.json diff --git a/SatelliteProvider/Configs/MapConfig.cs b/SatelliteProvider.Common/Configs/MapConfig.cs similarity index 72% rename from SatelliteProvider/Configs/MapConfig.cs rename to SatelliteProvider.Common/Configs/MapConfig.cs index 0d86d37..e53a0f5 100644 --- a/SatelliteProvider/Configs/MapConfig.cs +++ b/SatelliteProvider.Common/Configs/MapConfig.cs @@ -1,4 +1,4 @@ -namespace SatelliteProvider.Configs; +namespace SatelliteProvider.Common.Configs; public class MapConfig { diff --git a/SatelliteProvider/DTO/Direction.cs b/SatelliteProvider.Common/DTO/Direction.cs similarity index 89% rename from SatelliteProvider/DTO/Direction.cs rename to SatelliteProvider.Common/DTO/Direction.cs index 333d54d..f81aca9 100644 --- a/SatelliteProvider/DTO/Direction.cs +++ b/SatelliteProvider.Common/DTO/Direction.cs @@ -1,4 +1,4 @@ -namespace SatelliteProvider.DTO; +namespace SatelliteProvider.Common.DTO; public class Direction { diff --git a/SatelliteProvider/DTO/GeoPoint.cs b/SatelliteProvider.Common/DTO/GeoPoint.cs similarity index 95% rename from SatelliteProvider/DTO/GeoPoint.cs rename to SatelliteProvider.Common/DTO/GeoPoint.cs index 0f5d16e..cc03696 100644 --- a/SatelliteProvider/DTO/GeoPoint.cs +++ b/SatelliteProvider.Common/DTO/GeoPoint.cs @@ -1,4 +1,4 @@ -namespace SatelliteProvider; +namespace SatelliteProvider.Common.DTO; public class GeoPoint { diff --git a/SatelliteProvider/DTO/SatTile.cs b/SatelliteProvider.Common/DTO/SatTile.cs similarity index 76% rename from SatelliteProvider/DTO/SatTile.cs rename to SatelliteProvider.Common/DTO/SatTile.cs index 31f7aa2..dab2ebc 100644 --- a/SatelliteProvider/DTO/SatTile.cs +++ b/SatelliteProvider.Common/DTO/SatTile.cs @@ -1,9 +1,12 @@ -namespace SatelliteProvider.DTO; +using SatelliteProvider.Common.Utils; + +namespace SatelliteProvider.Common.DTO; public class SatTile { public int X { get; } public int Y { get; } + public int Zoom { get; } public GeoPoint LeftTop { get; } public GeoPoint BottomRight { get; } public string Url { get; set; } @@ -13,13 +16,14 @@ public class SatTile { X = x; Y = y; + Zoom = zoom; Url = url; LeftTop = GeoUtils.TileToWorldPos(x, y, zoom); BottomRight = GeoUtils.TileToWorldPos(x + 1, y + 1, zoom); } - public string FileName => $"tile_lt_{LeftTop.Lat:F6}_{LeftTop.Lon:F6}_br_{BottomRight.Lat:F6}_{BottomRight.Lon:F6}.jpg"; + public string FileName => $"{X}.{Y}.{Zoom}.jpg"; public override string ToString() { diff --git a/SatelliteProvider.Common/Interfaces/ISatelliteDownloader.cs b/SatelliteProvider.Common/Interfaces/ISatelliteDownloader.cs new file mode 100644 index 0000000..20651c1 --- /dev/null +++ b/SatelliteProvider.Common/Interfaces/ISatelliteDownloader.cs @@ -0,0 +1,8 @@ +using SatelliteProvider.Common.DTO; + +namespace SatelliteProvider.Common.Interfaces; + +public interface ISatelliteDownloader +{ + Task GetTiles(GeoPoint geoPoint, double radiusM, int zoomLevel, CancellationToken token = default); +} \ No newline at end of file diff --git a/SatelliteProvider.Common/SatelliteProvider.Common.csproj b/SatelliteProvider.Common/SatelliteProvider.Common.csproj new file mode 100644 index 0000000..3a63532 --- /dev/null +++ b/SatelliteProvider.Common/SatelliteProvider.Common.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/SatelliteProvider/GeoUtils.cs b/SatelliteProvider.Common/Utils/GeoUtils.cs similarity index 92% rename from SatelliteProvider/GeoUtils.cs rename to SatelliteProvider.Common/Utils/GeoUtils.cs index 4c927dc..e85c6b1 100644 --- a/SatelliteProvider/GeoUtils.cs +++ b/SatelliteProvider.Common/Utils/GeoUtils.cs @@ -1,16 +1,16 @@ -using SatelliteProvider.DTO; +using SatelliteProvider.Common.DTO; -namespace SatelliteProvider; +namespace SatelliteProvider.Common.Utils; public static class GeoUtils { private const double EARTH_RADIUS = 6378137; - public static (int x, int y) WorldToTilePos(double lat, double lon, int zoom) + public static (int x, int y) WorldToTilePos(GeoPoint point, int zoom) { - var latRad = lat * Math.PI / 180.0; + var latRad = point.Lat * Math.PI / 180.0; var n = Math.Pow(2.0, zoom); - var xTile = (int)Math.Floor((lon + 180.0) / 360.0 * n); + var xTile = (int)Math.Floor((point.Lon + 180.0) / 360.0 * n); var yTile = (int)Math.Floor((1.0 - Math.Log(Math.Tan(latRad) + 1.0 / Math.Cos(latRad)) / Math.PI) / 2.0 * n); return (xTile, yTile); } diff --git a/SatelliteProvider.Services/GoogleMapsDownloader.cs b/SatelliteProvider.Services/GoogleMapsDownloader.cs new file mode 100644 index 0000000..b811da9 --- /dev/null +++ b/SatelliteProvider.Services/GoogleMapsDownloader.cs @@ -0,0 +1,104 @@ +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 class GoogleMapsDownloader(ILogger logger, IOptions mapConfig, 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 readonly string _apiKey = mapConfig.Value.ApiKey; + + private readonly string _satDirectory = Path.Combine(Directory.GetCurrentDirectory(), "maps"); + + + 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) + { + 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 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, token), token)); + } + + await Task.WhenAll(downloadTasks); + } + + private async Task DownloadTilesWorker(ConcurrentQueue tilesToDownload, CancellationToken token) + { + using var httpClient = httpClientFactory.CreateClient(); + + while (tilesToDownload.TryDequeue(out var tileInfo)) + { + if (token.IsCancellationRequested) break; + try + { + 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(Path.Combine(_satDirectory, tileInfo.FileName), token); + } + catch (HttpRequestException requestException) + { + logger.LogError(requestException, "Failed to download tile. Url: {Url}", tileInfo.Url); + } + catch (Exception e) + { + logger.LogError(e, "Failed to download tile"); + } + } + } +} \ No newline at end of file diff --git a/SatelliteProvider.Services/SatelliteProvider.Services.csproj b/SatelliteProvider.Services/SatelliteProvider.Services.csproj new file mode 100644 index 0000000..1155581 --- /dev/null +++ b/SatelliteProvider.Services/SatelliteProvider.Services.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/SatelliteProvider.Tests/GoogleMapsDownloaderTests.cs b/SatelliteProvider.Tests/GoogleMapsDownloaderTests.cs new file mode 100644 index 0000000..b4f69aa --- /dev/null +++ b/SatelliteProvider.Tests/GoogleMapsDownloaderTests.cs @@ -0,0 +1,57 @@ +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using SatelliteProvider.Common.Configs; +using SatelliteProvider.Common.DTO; +using SatelliteProvider.Services; +using Xunit; + +namespace SatelliteProvider.Tests; + +public class GoogleMapsDownloaderTests +{ + [Fact] + public async Task IntegrationTest_DownloadRealTiles_ShouldDownloadBytes() + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .Build(); + + var mapConfig = new MapConfig(); + configuration.GetSection("MapConfig").Bind(mapConfig); + + var services = new ServiceCollection(); + services.AddHttpClient(); + services.AddLogging(builder => builder.AddConsole()); + var serviceProvider = services.BuildServiceProvider(); + + var logger = serviceProvider.GetRequiredService>(); + var options = Options.Create(mapConfig); + var httpClientFactory = serviceProvider.GetRequiredService(); + + var downloader = new GoogleMapsDownloader(logger, options, httpClientFactory); + + var centerPoint = new GeoPoint(37.7749, -122.4194); + var radius = 200.0; + var zoomLevel = 15; + + await downloader.GetTiles(centerPoint, radius, zoomLevel); + + var mapsDirectory = Path.Combine(Directory.GetCurrentDirectory(), "maps"); + Directory.Exists(mapsDirectory).Should().BeTrue(); + + var files = Directory.GetFiles(mapsDirectory, "*.jpg"); + files.Should().NotBeEmpty(); + + var totalBytes = files.Sum(file => new FileInfo(file).Length); + totalBytes.Should().BeGreaterThan(0); + + foreach (var file in files) + { + File.Delete(file); + } + } +} diff --git a/SatelliteProvider.Tests/SatelliteProvider.Tests.csproj b/SatelliteProvider.Tests/SatelliteProvider.Tests.csproj new file mode 100644 index 0000000..986019d --- /dev/null +++ b/SatelliteProvider.Tests/SatelliteProvider.Tests.csproj @@ -0,0 +1,42 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + + + diff --git a/SatelliteProvider.Tests/appsettings.json b/SatelliteProvider.Tests/appsettings.json new file mode 100644 index 0000000..d9be549 --- /dev/null +++ b/SatelliteProvider.Tests/appsettings.json @@ -0,0 +1,5 @@ +{ + "MapConfig": { + "ApiKey": "AIzaSyAXRBDBOskC5QOHG6VJWzmVJwYKcu6WH8k" + } +} diff --git a/SatelliteProvider.sln b/SatelliteProvider.sln index e25f723..7428785 100644 --- a/SatelliteProvider.sln +++ b/SatelliteProvider.sln @@ -1,6 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteProvider", "SatelliteProvider\SatelliteProvider.csproj", "{35C6FC8B-92D8-4D8D-BE36-D6B181715019}" +# +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteProvider.Api", "SatelliteProvider.Api\SatelliteProvider.Api.csproj", "{35C6FC8B-92D8-4D8D-BE36-D6B181715019}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteProvider.Common", "SatelliteProvider.Common\SatelliteProvider.Common.csproj", "{5499248E-F025-4091-9103-6AA02C6CB613}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteProvider.Services", "SatelliteProvider.Services\SatelliteProvider.Services.csproj", "{452166A0-28C3-429F-B2BD-39041FB7B5A5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SatelliteProvider.Tests", "SatelliteProvider.Tests\SatelliteProvider.Tests.csproj", "{A44A2E49-9270-4938-9D34-A31CE63E636C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,5 +19,17 @@ Global {35C6FC8B-92D8-4D8D-BE36-D6B181715019}.Debug|Any CPU.Build.0 = Debug|Any CPU {35C6FC8B-92D8-4D8D-BE36-D6B181715019}.Release|Any CPU.ActiveCfg = Release|Any CPU {35C6FC8B-92D8-4D8D-BE36-D6B181715019}.Release|Any CPU.Build.0 = Release|Any CPU + {5499248E-F025-4091-9103-6AA02C6CB613}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5499248E-F025-4091-9103-6AA02C6CB613}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5499248E-F025-4091-9103-6AA02C6CB613}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5499248E-F025-4091-9103-6AA02C6CB613}.Release|Any CPU.Build.0 = Release|Any CPU + {452166A0-28C3-429F-B2BD-39041FB7B5A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {452166A0-28C3-429F-B2BD-39041FB7B5A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {452166A0-28C3-429F-B2BD-39041FB7B5A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {452166A0-28C3-429F-B2BD-39041FB7B5A5}.Release|Any CPU.Build.0 = Release|Any CPU + {A44A2E49-9270-4938-9D34-A31CE63E636C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A44A2E49-9270-4938-9D34-A31CE63E636C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A44A2E49-9270-4938-9D34-A31CE63E636C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A44A2E49-9270-4938-9D34-A31CE63E636C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/SatelliteProvider/Configs/DirectoriesConfig.cs b/SatelliteProvider/Configs/DirectoriesConfig.cs deleted file mode 100644 index 57cd824..0000000 --- a/SatelliteProvider/Configs/DirectoriesConfig.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace SatelliteProvider.Configs; - -public class DirectoriesConfig -{ - public string? ApiResourcesDirectory { get; set; } = null!; - - public string VideosDirectory { get; set; } = null!; - public string LabelsDirectory { get; set; } = null!; - public string ImagesDirectory { get; set; } = null!; - public string ResultsDirectory { get; set; } = null!; - public string ThumbnailsDirectory { get; set; } = null!; - - public string GpsSatDirectory { get; set; } = null!; - public string GpsRouteDirectory { get; set; } = null!; -} \ No newline at end of file diff --git a/SatelliteProvider/SatelliteDownloader.cs b/SatelliteProvider/SatelliteDownloader.cs deleted file mode 100644 index c98000d..0000000 --- a/SatelliteProvider/SatelliteDownloader.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Collections.Concurrent; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using SatelliteProvider.Configs; -using SatelliteProvider.DTO; -using SixLabors.ImageSharp; - -namespace SatelliteProvider; - -public interface ISatelliteDownloader -{ - Task GetTiles(GeoPoint geoPoint, double radiusM, int zoomLevel, CancellationToken token = default); -} - -public class SatelliteDownloader(ILogger logger, IOptions mapConfig, 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 int NUM_SERVERS = 4; - private readonly string _apiKey = mapConfig.Value.ApiKey; - - private readonly string _satDirectory = Path.Combine(Directory.GetCurrentDirectory(), "maps"); - - - 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, e.Message); - throw; - } - } - - public async Task GetTiles(GeoPoint centerGeoPoint, double radiusM, int zoomLevel, CancellationToken token = default) - { - var (latMin, latMax, lonMin, lonMax) = GeoUtils.GetBoundingBox(centerGeoPoint, radiusM); - - var (xMin, yMin) = GeoUtils.WorldToTilePos(latMax, lonMin, zoomLevel); // Top-left corner - var (xMax, yMax) = GeoUtils.WorldToTilePos(latMin, lonMax, zoomLevel); // Bottom-right corner - - var tilesToDownload = new ConcurrentQueue(); - 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(); - int downloadedCount = 0; - - - for (int i = 0; i < NUM_SERVERS; i++) - { - downloadTasks.Add(Task.Run(async () => - { - using var httpClient = httpClientFactory.CreateClient(); - - while (tilesToDownload.TryDequeue(out var tileInfo)) - { - if (token.IsCancellationRequested) break; - try - { - httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36"); - 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(Path.Combine(_satDirectory, tileInfo.FileName), token); - if (tileData.Length > 0) - { - Interlocked.Increment(ref downloadedCount); - } - } - catch (HttpRequestException requestException) - { - logger.LogError(requestException, $"Fail to download tile! Url: {tileInfo.Url}. {requestException.Message}"); - } - catch (Exception e) - { - logger.LogError(e, $"Fail to download tile! {e.Message}"); - } - } - }, token)); - } - - await Task.WhenAll(downloadTasks); - } -} \ No newline at end of file