Files
annotations/Azaion.Common/Services/SatelliteDownloader.cs
T
Alex Bezdieniezhnykh c0f8dd792d fixed console Log
fix same files problem in python different libs
correct command logging in command handler
2025-06-14 21:01:32 +03:00

250 lines
10 KiB
C#

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.Events;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity;
using MediatR;
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,
IMediator mediator)
: 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)
{
await mediator.Publish(new SetStatusTextEvent($"Завантажується супутникові зображення по координатах: центр: lat: {centerLat:F3} lon: {centerLon:F3} квадрат {radiusM}м * {radiusM}м, zoom: {zoomLevel}..."), token);
//empty Satellite directory
if (Directory.Exists(_satDirectory))
Directory.Delete(_satDirectory, true);
Directory.CreateDirectory(_satDirectory);
var downloadTilesResult = await DownloadTiles(centerLat, centerLon, radiusM, zoomLevel, token);
await mediator.Publish(new SetStatusTextEvent("Завершено! Склеюється в 1 зображення..."), token);
var image = ComposeTiles(downloadTilesResult.Tiles, token);
if (image == null)
return;
await mediator.Publish(new SetStatusTextEvent("Розбиття на малі зображення для опрацювання..."), token);
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 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)
{
logger.LogError($"Error while loading tile: {tileData}");
}
if (token.IsCancellationRequested)
return;
}
}
});
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
};
}
}