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