add gps matcher service

This commit is contained in:
Alex Bezdieniezhnykh
2025-04-14 09:50:34 +03:00
parent 36b3bf1712
commit ca1682a86e
26 changed files with 759 additions and 119 deletions
+3
View File
@@ -12,10 +12,13 @@
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="MessagePack" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Polly" Version="8.5.2" />
<PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="System.Drawing.Common" Version="5.0.3" />
</ItemGroup>
+2
View File
@@ -14,6 +14,8 @@ public class Constants
public const string DEFAULT_IMAGES_DIR = "images";
public const string DEFAULT_RESULTS_DIR = "results";
public const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
public const string DEFAULT_GPS_SAT_DIRECTORY = "satellitesDir";
public const string DEFAULT_GPS_ROUTE_DIRECTORY = "routeDir";
#endregion
+3 -1
View File
@@ -67,7 +67,9 @@ public class ConfigUpdater : IConfigUpdater
ImagesDirectory = Constants.DEFAULT_IMAGES_DIR,
LabelsDirectory = Constants.DEFAULT_LABELS_DIR,
ResultsDirectory = Constants.DEFAULT_RESULTS_DIR,
ThumbnailsDirectory = Constants.DEFAULT_THUMBNAILS_DIR
ThumbnailsDirectory = Constants.DEFAULT_THUMBNAILS_DIR,
GpsSatDirectory = Constants.DEFAULT_GPS_SAT_DIRECTORY,
GpsRouteDirectory = Constants.DEFAULT_GPS_ROUTE_DIRECTORY
},
ThumbnailConfig = new ThumbnailConfig
@@ -7,4 +7,7 @@ public class DirectoriesConfig
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!;
}
+12
View File
@@ -0,0 +1,12 @@
using System.Collections.Concurrent;
namespace Azaion.Common.DTO;
public class DownloadTilesResult
{
public ConcurrentDictionary<(int x, int y), byte[]> Tiles { get; set; } = null!;
public double LatMin { get; set; }
public double LatMax { get; set; }
public double LonMin { get; set; }
public double LonMax { get; set; }
}
@@ -2,24 +2,25 @@
using System.Collections.Generic;
using System.IO;
public class GpsCsvResult
public class GpsMatchResult
{
public string Image { get; set; }
public double Latitude { get; set; }
public int Index { get; set; }
public string Image { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
public int Keypoints { get; set; }
public int Rotation { get; set; }
public string MatchType { get; set; }
public static List<GpsCsvResult> ReadFromCsv(string csvFilePath)
public static List<GpsMatchResult> ReadFromCsv(string csvFilePath)
{
var imageDatas = new List<GpsCsvResult>();
var imageDatas = new List<GpsMatchResult>();
using var reader = new StreamReader(csvFilePath);
//read header
reader.ReadLine();
if (reader.EndOfStream)
return new List<GpsCsvResult>();
return new List<GpsMatchResult>();
while (!reader.EndOfStream)
{
@@ -29,7 +30,7 @@ public class GpsCsvResult
var values = line.Split(',');
if (values.Length == 6)
{
imageDatas.Add(new GpsCsvResult
imageDatas.Add(new GpsMatchResult
{
Image = GetFilename(values[0]),
Latitude = double.Parse(values[1]),
+31
View File
@@ -0,0 +1,31 @@
using Azaion.Common.Extensions;
namespace Azaion.Common.DTO;
public class SatTile
{
public int X { get; }
public int Y { get; }
public double LeftTopLat { get; }
public double LeftTopLon { get; }
public double BottomRightLat { get; }
public double BottomRightLon { get; }
public string Url { get; set; }
public SatTile(int x, int y, int zoom, string url)
{
X = x;
Y = y;
Url = url;
(LeftTopLat, LeftTopLon) = GeoUtils.TileToWorldPos(x, y, zoom);
(BottomRightLat, BottomRightLon) = GeoUtils.TileToWorldPos(x + 1, y + 1, zoom);
}
public override string ToString()
{
return $"Tile[X={X}, Y={Y}, TL=({LeftTopLat:F6}, {LeftTopLon:F6}), BR=({BottomRightLat:F6}, {BottomRightLon:F6})]";
}
}
+39
View File
@@ -0,0 +1,39 @@
namespace Azaion.Common.Extensions;
public static class GeoUtils
{
private const double EARTH_RADIUS = 6378137;
public static (int x, int y) WorldToTilePos(double lat, double lon, int zoom)
{
var latRad = lat * Math.PI / 180.0;
var n = Math.Pow(2.0, zoom);
var xTile = (int)Math.Floor((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);
}
public static (double lat, double lon) TileToWorldPos(int x, int y, int zoom)
{
var n = Math.Pow(2.0, zoom);
var lonDeg = x / n * 360.0 - 180.0;
var latRad = Math.Atan(Math.Sinh(Math.PI * (1.0 - 2.0 * y / n)));
var latDeg = latRad * 180.0 / Math.PI;
return (latDeg, lonDeg);
}
public static (double minLat, double maxLat, double minLon, double maxLon) GetBoundingBox(double centerLat, double centerLon, double radiusM)
{
var latRad = centerLat * Math.PI / 180.0;
var latDiff = (radiusM / EARTH_RADIUS) * (180.0 / Math.PI);
var minLat = Math.Max(centerLat - latDiff, -90.0);
var maxLat = Math.Min(centerLat + latDiff, 90.0);
var lonDiff = (radiusM / (EARTH_RADIUS * Math.Cos(latRad))) * (180.0 / Math.PI);
var minLon = Math.Max(centerLon - lonDiff, -180.0);
var maxLon = Math.Min(centerLon + lonDiff, 180.0);
return (minLat, maxLat, minLon, maxLon);
}
}
@@ -0,0 +1,73 @@
using System.Diagnostics;
using System.IO;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.CommonSecurity;
using Microsoft.Extensions.Options;
namespace Azaion.Common.Services;
public interface IGpsMatcherService
{
Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default);
void StopGpsMatching();
}
public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions<DirectoriesConfig> dirConfig) : IGpsMatcherService
{
private const int ZOOM_LEVEL = 18;
private const int POINTS_COUNT = 10;
private const int DISTANCE_BETWEEN_POINTS_M = 100;
private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1);
public async Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default)
{
var currentLat = initialLatitude;
var currentLon = initialLongitude;
var routeDir = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, dirConfig.Value.GpsRouteDirectory);
if (Directory.Exists(routeDir))
Directory.Delete(routeDir, true);
Directory.CreateDirectory(routeDir);
var routeFiles = new List<string>();
foreach (var file in Directory.GetFiles(userRouteDir))
{
routeFiles.Add(file);
File.Copy(file, Path.Combine(routeDir, Path.GetFileName(file)));
}
var indexOffset = 0;
while (routeFiles.Any())
{
await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken);
gpsMatcherClient.StartMatching(new StartMatchingEvent
{
ImagesCount = POINTS_COUNT,
Latitude = initialLatitude,
Longitude = initialLongitude,
SatelliteImagesDir = dirConfig.Value.GpsSatDirectory,
RouteDir = dirConfig.Value.GpsRouteDirectory
});
while (true)
{
var result = gpsMatcherClient.GetResult();
if (result == null)
break;
result.Index += indexOffset;
await processResult(result);
currentLat = result.Latitude;
currentLon = result.Longitude;
routeFiles.RemoveAt(0);
}
indexOffset += POINTS_COUNT;
}
}
public void StopGpsMatching()
{
gpsMatcherClient.Stop();
}
}
+118
View File
@@ -0,0 +1,118 @@
using System.Diagnostics;
using Azaion.Common.DTO;
using Azaion.CommonSecurity;
using Azaion.CommonSecurity.DTO;
using Microsoft.Extensions.Options;
using NetMQ;
using NetMQ.Sockets;
namespace Azaion.Common.Services;
public interface IGpsMatcherClient
{
void StartMatching(StartMatchingEvent startEvent);
GpsMatchResult? GetResult(int retries = 2, int tryTimeoutSeconds = 5, CancellationToken ct = default);
void Stop();
}
public class StartMatchingEvent
{
public string RouteDir { get; set; } = null!;
public string SatelliteImagesDir { get; set; } = null!;
public int ImagesCount { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
public string ProcessingType { get; set; } = "cuda";
public int Altitude { get; set; } = 400;
public double CameraSensorWidth { get; set; } = 23.5;
public double CameraFocalLength { get; set; } = 24;
public override string ToString() =>
$"{RouteDir},{SatelliteImagesDir},{ImagesCount},{Latitude},{Longitude},{ProcessingType},{Altitude},{CameraSensorWidth},{CameraFocalLength}";
}
public class GpsMatcherClient : IGpsMatcherClient
{
private readonly GpsDeniedClientConfig _gpsDeniedClientConfig;
private readonly RequestSocket _requestSocket = new();
private readonly SubscriberSocket _subscriberSocket = new();
public GpsMatcherClient(IOptions<GpsDeniedClientConfig> gpsDeniedClientConfig)
{
_gpsDeniedClientConfig = gpsDeniedClientConfig.Value;
Start();
}
private void Start()
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = SecurityConstants.ExternalGpsDeniedPath,
WorkingDirectory = SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER
//Arguments = $"-e {credentials.Email} -p {credentials.Password} -f {apiConfig.ResourcesFolder}",
//RedirectStandardOutput = true,
//RedirectStandardError = true,
//CreateNoWindow = true
};
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
//process.Start();
}
catch (Exception e)
{
Console.WriteLine(e);
//throw;
}
_requestSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqPort}");
_subscriberSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqSubscriberPort}");
_subscriberSocket.Subscribe("");
}
public void StartMatching(StartMatchingEvent e)
{
_requestSocket.SendFrame(e.ToString());
var response = _requestSocket.ReceiveFrameString();
if (response != "OK")
throw new Exception("Start Matching Failed");
}
public GpsMatchResult? GetResult(int retries = 15, int tryTimeoutSeconds = 5, CancellationToken ct = default)
{
var tryNum = 0;
while (!ct.IsCancellationRequested && tryNum++ < retries)
{
if (!_subscriberSocket.TryReceiveFrameString(TimeSpan.FromSeconds(tryTimeoutSeconds), out var update))
continue;
if (update == "FINISHED")
return null;
var parts = update.Split(',');
if (parts.Length != 5)
throw new Exception("Matching Result Failed");
return new GpsMatchResult
{
Index = int.Parse(parts[0]),
Image = parts[1],
Latitude = double.Parse(parts[2]),
Longitude = double.Parse(parts[3]),
MatchType = parts[4]
};
}
if (!ct.IsCancellationRequested)
throw new Exception($"Unable to get bytes after {tryNum} retries, {tryTimeoutSeconds} seconds each");
return null;
}
public void Stop()
{
_requestSocket.SendFrame("STOP");
}
}
+4 -4
View File
@@ -17,20 +17,20 @@ public interface IInferenceService
void StopInference();
}
public class InferenceService(ILogger<InferenceService> logger, [FromKeyedServices(SecurityConstants.EXTERNAL_INFERENCE_PATH)] IExternalClient externalClient, IOptions<AIRecognitionConfig> aiConfigOptions) : IInferenceService
public class InferenceService(ILogger<InferenceService> logger, IInferenceClient client, IOptions<AIRecognitionConfig> aiConfigOptions) : IInferenceService
{
public async Task RunInference(List<string> mediaPaths, Func<AnnotationImage, Task> processAnnotation, CancellationToken detectToken = default)
{
var aiConfig = aiConfigOptions.Value;
aiConfig.Paths = mediaPaths;
externalClient.Send(RemoteCommand.Create(CommandType.Inference, aiConfig));
client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig));
while (!detectToken.IsCancellationRequested)
{
try
{
var bytes = externalClient.GetBytes(ct: detectToken);
var bytes = client.GetBytes(ct: detectToken);
if (bytes == null)
throw new Exception("Can't get bytes from inference client");
@@ -51,6 +51,6 @@ public class InferenceService(ILogger<InferenceService> logger, [FromKeyedServic
public void StopInference()
{
externalClient.Send(RemoteCommand.Create(CommandType.StopInference));
client.Send(RemoteCommand.Create(CommandType.StopInference));
}
}
@@ -0,0 +1,290 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Net.Http.Json;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace Azaion.Common.Services;
public interface ISatelliteDownloader
{
Task GetTiles(double latitude, double longitude, double radiusM, int zoomLevel, CancellationToken token = default);
}
public class SatelliteDownloader(
ILogger<SatelliteDownloader> logger,
IOptions<MapConfig> mapConfig,
IOptions<DirectoriesConfig> directoriesConfig,
IHttpClientFactory httpClientFactory)
: ISatelliteDownloader
{
private const int INPUT_TILE_SIZE = 256;
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 const int CROP_WIDTH = 1024;
private const int CROP_HEIGHT = 1024;
private const int STEP_X = 300;
private const int STEP_Y = 300;
private const int OUTPUT_TILE_SIZE = 512;
private readonly string _apiKey = mapConfig.Value.ApiKey;
private readonly string _satDirectory = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, directoriesConfig.Value.GpsSatDirectory);
public async Task GetTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default)
{
//empty Satellite directory
if (Directory.Exists(_satDirectory))
Directory.Delete(_satDirectory, true);
Directory.CreateDirectory(_satDirectory);
var downloadTilesResult = await DownloadTiles(centerLat, centerLon, radiusM, zoomLevel, token);
var image = await ComposeTiles(downloadTilesResult.Tiles, token);
if (image != null)
await SplitToTiles(image, downloadTilesResult, token);
}
private async Task SplitToTiles(Image<Rgba32> image, DownloadTilesResult bounds, CancellationToken token = default)
{
// Calculate all crop parameters beforehand
var cropTasks = new List<Action>();
var latRange = bounds.LatMax - bounds.LatMin; // [cite: 13]
var lonRange = bounds.LonMax - bounds.LonMin; // [cite: 13]
var degreesPerPixelLat = latRange / image.Height; // [cite: 13]
var degreesPerPixelLon = lonRange / image.Width; // [cite: 14]
int tempRowIndex = 0;
for (int top = 0; top <= image.Height - CROP_HEIGHT; top += STEP_Y) // [cite: 15]
{
int tempColIndex = 0;
for (int left = 0; left <= image.Width - CROP_WIDTH; left += STEP_X) // [cite: 16]
{
// Capture loop variables for the closure
int currentTop = top;
int currentLeft = left;
int rowIndex = tempRowIndex;
int colIndex = tempColIndex;
cropTasks.Add(() =>
{
token.ThrowIfCancellationRequested();
var cropBox = new Rectangle(currentLeft, currentTop, CROP_WIDTH, CROP_HEIGHT);
using var croppedImage = image.Clone(ctx => ctx.Crop(cropBox));
var cropTlLat = bounds.LatMax - (currentTop * degreesPerPixelLat);
var cropTlLon = bounds.LonMin + (currentLeft * degreesPerPixelLon);
var cropBrLat = cropTlLat - (CROP_HEIGHT * degreesPerPixelLat);
var cropBrLon = cropTlLon + (CROP_WIDTH * degreesPerPixelLon);
var outputFilename = Path.Combine(_satDirectory,
$"map_{rowIndex:D4}_{colIndex:D4}_tl_{cropTlLat:F6}_{cropTlLon:F6}_br_{cropBrLat:F6}_{cropBrLon:F6}.tif"
);
using var resizedImage = croppedImage.Clone(ctx => ctx.Resize(OUTPUT_TILE_SIZE, OUTPUT_TILE_SIZE, KnownResamplers.Lanczos3));
resizedImage.SaveAsTiffAsync(outputFilename, token).GetAwaiter().GetResult(); // Use synchronous saving or manage async Tasks properly in parallel context
});
tempColIndex++;
}
tempRowIndex++;
}
// Execute tasks in parallel
await Task.Run(() => Parallel.ForEach(cropTasks, action => action()), token);
}
private async Task SplitToTiles_OLD(Image<Rgba32> image, DownloadTilesResult bounds, CancellationToken token = default)
{
ArgumentNullException.ThrowIfNull(image);
ArgumentNullException.ThrowIfNull(bounds);
if (bounds.LatMax <= bounds.LatMin || bounds.LonMax <= bounds.LonMin || image.Width <= 0 || image.Height <= 0)
throw new ArgumentException("Invalid coordinate bounds (LatMax <= LatMin or LonMax <= LonMin) or image dimensions (Width/Height <= 0).");
var latRange = bounds.LatMax - bounds.LatMin;
var lonRange = bounds.LonMax - bounds.LonMin;
var degreesPerPixelLat = latRange / image.Height;
var degreesPerPixelLon = lonRange / image.Width;
var rowIndex = 0;
for (int top = 0; top <= image.Height - CROP_HEIGHT; top += STEP_Y)
{
token.ThrowIfCancellationRequested();
int colIndex = 0;
for (int left = 0; left <= image.Width - CROP_WIDTH; left += STEP_X)
{
token.ThrowIfCancellationRequested();
var cropBox = new Rectangle(left, top, CROP_WIDTH, CROP_HEIGHT);
using (var croppedImage = image.Clone(ctx => ctx.Crop(cropBox)))
{
var cropTlLat = bounds.LatMax - (top * degreesPerPixelLat);
var cropTlLon = bounds.LonMin + (left * degreesPerPixelLon);
var cropBrLat = cropTlLat - (CROP_HEIGHT * degreesPerPixelLat);
var cropBrLon = cropTlLon + (CROP_WIDTH * degreesPerPixelLon);
var outputFilename = Path.Combine(_satDirectory,
$"map_{rowIndex:D4}_{colIndex:D4}_tl_{cropTlLat:F6}_{cropTlLon:F6}_br_{cropBrLat:F6}_{cropBrLon:F6}.tif"
);
using (var resizedImage = croppedImage.Clone(ctx => ctx.Resize(OUTPUT_TILE_SIZE, OUTPUT_TILE_SIZE, KnownResamplers.Lanczos3)))
await resizedImage.SaveAsTiffAsync(outputFilename, token);
}
colIndex++;
}
rowIndex++;
}
}
private async Task<Image<Rgba32>?> ComposeTiles(ConcurrentDictionary<(int x, int y), byte[]> downloadedTiles, CancellationToken token = default)
{
if (downloadedTiles.IsEmpty)
return null;
var xMin = downloadedTiles.Min(t => t.Key.x);
var xMax = downloadedTiles.Max(t => t.Key.x);
var yMin = downloadedTiles.Min(t => t.Key.y);
var yMax = downloadedTiles.Max(t => t.Key.y);
var totalWidth = (xMax - xMin + 1) * INPUT_TILE_SIZE;
var totalHeight = (yMax - yMin + 1) * INPUT_TILE_SIZE;
if (totalWidth <= 0 || totalHeight <= 0)
return null;
var largeImage = new Image<Rgba32>(totalWidth, totalHeight);
largeImage.Mutate(ctx =>
{
for (var y = yMin; y <= yMax; y++)
{
for (var x = xMin; x <= xMax; x++)
{
if (!downloadedTiles.TryGetValue((x, y), out var tileData))
continue;
try
{
using var tileImage = Image.Load(tileData);
var offsetX = (x - xMin) * INPUT_TILE_SIZE;
var offsetY = (y - yMin) * INPUT_TILE_SIZE;
ctx.DrawImage(tileImage, new Point(offsetX, offsetY), 1f);
}
catch (Exception)
{
Console.WriteLine($"Error while loading tile: {tileData}");
}
if (token.IsCancellationRequested)
return;
}
}
});
// await largeImage.SaveAsync(Path.Combine(_satDirectory, "full_map.tif"),
// new TiffEncoder { Compression = TiffCompression.Deflate }, token);
return largeImage;
}
private record SessionResponse(string Session);
private async Task<string?> 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<SessionResponse>();
return sessionResponse?.Session;
}
catch (Exception e)
{
logger.LogError(e, e.Message);
throw;
}
}
private async Task<DownloadTilesResult> DownloadTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default)
{
var (latMin, latMax, lonMin, lonMax) = GeoUtils.GetBoundingBox(centerLat, centerLon, 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<SatTile>();
var downloadedTiles = new ConcurrentDictionary<(int x, int y), byte[]>();
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<Task>();
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);
if (tileData?.Length > 0)
{
downloadedTiles.TryAdd((tileInfo.X, tileInfo.Y), tileData);
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);
return new DownloadTilesResult
{
Tiles = downloadedTiles,
LatMin = latMin,
LatMax = latMax,
LonMin = lonMin,
LonMax = lonMax
};
}
}