mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 21:56:31 +00:00
babcbc0fc7
clean warnings
244 lines
9.7 KiB
C#
244 lines
9.7 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.Extensions;
|
|
using Azaion.CommonSecurity;
|
|
using Azaion.CommonSecurity.DTO;
|
|
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 = 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 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;
|
|
}
|
|
}
|
|
});
|
|
|
|
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
|
|
};
|
|
}
|
|
} |