mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-04-23 03:06:38 +00:00
add SatelliteDownloader
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
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!;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace SatelliteProvider.Configs;
|
||||||
|
|
||||||
|
public class MapConfig
|
||||||
|
{
|
||||||
|
public string Service { get; set; } = null!;
|
||||||
|
public string ApiKey { get; set; } = null!;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace SatelliteProvider.DTO;
|
||||||
|
|
||||||
|
public class Direction
|
||||||
|
{
|
||||||
|
public double Distance { get; set; }
|
||||||
|
public double Azimuth { get; set; }
|
||||||
|
|
||||||
|
public Direction() { }
|
||||||
|
|
||||||
|
public Direction(double distance, double azimuth)
|
||||||
|
{
|
||||||
|
Distance = distance;
|
||||||
|
Azimuth = azimuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"{Distance:F2}, {Azimuth:F1} deg";
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace SatelliteProvider;
|
||||||
|
|
||||||
|
public class GeoPoint
|
||||||
|
{
|
||||||
|
const double PRECISION_TOLERANCE = 0.00005;
|
||||||
|
public double Lat { get; }
|
||||||
|
public double Lon { get; }
|
||||||
|
|
||||||
|
public GeoPoint() { }
|
||||||
|
|
||||||
|
public GeoPoint(double lat, double lon)
|
||||||
|
{
|
||||||
|
Lat = lat;
|
||||||
|
Lon = lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"{Lat:F4}, {Lon:F4}";
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
if (obj is not GeoPoint point) return false;
|
||||||
|
return ReferenceEquals(this, obj) || Equals(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Equals(GeoPoint point) =>
|
||||||
|
Math.Abs(Lat - point.Lat) < PRECISION_TOLERANCE && Math.Abs(Lon - point.Lon) < PRECISION_TOLERANCE;
|
||||||
|
|
||||||
|
public override int GetHashCode() => HashCode.Combine(Lat, Lon);
|
||||||
|
|
||||||
|
public static bool operator ==(GeoPoint left, GeoPoint right) => Equals(left, right);
|
||||||
|
public static bool operator !=(GeoPoint left, GeoPoint right) => !Equals(left, right);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace SatelliteProvider.DTO;
|
||||||
|
|
||||||
|
public class SatTile
|
||||||
|
{
|
||||||
|
public int X { get; }
|
||||||
|
public int Y { get; }
|
||||||
|
public GeoPoint LeftTop { get; }
|
||||||
|
public GeoPoint BottomRight { get; }
|
||||||
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public SatTile(int x, int y, int zoom, string url)
|
||||||
|
{
|
||||||
|
X = x;
|
||||||
|
Y = y;
|
||||||
|
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 override string ToString()
|
||||||
|
{
|
||||||
|
return $"Tile[X={X}, Y={Y}, TL=({LeftTop.Lat:F6}, {LeftTop.Lon:F6}), BR=({BottomRight.Lat:F6}, {BottomRight.Lon:F6})]";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using SatelliteProvider.DTO;
|
||||||
|
|
||||||
|
namespace SatelliteProvider;
|
||||||
|
|
||||||
|
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 ToRadians(double degrees) => degrees * Math.PI / 180.0;
|
||||||
|
public static double ToDegrees(double radians) => radians * 180.0 / Math.PI;
|
||||||
|
|
||||||
|
public static Direction DirectionTo(this GeoPoint p1, GeoPoint p2)
|
||||||
|
{
|
||||||
|
var lat1Rad = ToRadians(p1.Lat);
|
||||||
|
var lat2Rad = ToRadians(p2.Lat);
|
||||||
|
var dLon = ToRadians(p2.Lon - p1.Lon);
|
||||||
|
var dLat = ToRadians(p2.Lat - p1.Lat);
|
||||||
|
|
||||||
|
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
|
||||||
|
Math.Cos(lat1Rad) * Math.Cos(lat2Rad) *
|
||||||
|
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
|
||||||
|
var c = 2 * Math.Asin(Math.Sqrt(a));
|
||||||
|
var distance = EARTH_RADIUS * c;
|
||||||
|
|
||||||
|
var y = Math.Sin(dLon) * Math.Cos(lat2Rad);
|
||||||
|
var x = Math.Cos(lat1Rad) * Math.Sin(lat2Rad) -
|
||||||
|
Math.Sin(lat1Rad) * Math.Cos(lat2Rad) * Math.Cos(dLon);
|
||||||
|
var azimuthRadians = Math.Atan2(y, x);
|
||||||
|
var azimuth = (ToDegrees(azimuthRadians) + 360) % 360;
|
||||||
|
|
||||||
|
return new Direction
|
||||||
|
{
|
||||||
|
Distance = distance,
|
||||||
|
Azimuth = azimuth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GeoPoint GoDirection(this GeoPoint startPoint, Direction direction)
|
||||||
|
{
|
||||||
|
var angularDistance = direction.Distance / EARTH_RADIUS;
|
||||||
|
var azimuthRadians = ToRadians(direction.Azimuth);
|
||||||
|
var startLatRad = ToRadians(startPoint.Lat);
|
||||||
|
var startLonRad = ToRadians(startPoint.Lon);
|
||||||
|
|
||||||
|
var destLatRad = Math.Asin(Math.Sin(startLatRad) * Math.Cos(angularDistance) +
|
||||||
|
Math.Cos(startLatRad) * Math.Sin(angularDistance) * Math.Cos(azimuthRadians));
|
||||||
|
|
||||||
|
var destLonRad = startLonRad + Math.Atan2(Math.Sin(azimuthRadians) * Math.Sin(angularDistance) * Math.Cos(startLatRad),
|
||||||
|
Math.Cos(angularDistance) - Math.Sin(startLatRad) * Math.Sin(destLatRad));
|
||||||
|
|
||||||
|
return new GeoPoint(ToDegrees(destLatRad), ToDegrees(destLonRad));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GeoPoint 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 new GeoPoint(latDeg, lonDeg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (double minLat, double maxLat, double minLon, double maxLon) GetBoundingBox(GeoPoint centerGeoPoint, double radiusM)
|
||||||
|
{
|
||||||
|
var latRad = centerGeoPoint.Lat * Math.PI / 180.0;
|
||||||
|
|
||||||
|
var latDiff = (radiusM / EARTH_RADIUS) * (180.0 / Math.PI);
|
||||||
|
var minLat = Math.Max(centerGeoPoint.Lat - latDiff, -90.0);
|
||||||
|
var maxLat = Math.Min(centerGeoPoint.Lat + latDiff, 90.0);
|
||||||
|
|
||||||
|
var lonDiff = (radiusM / (EARTH_RADIUS * Math.Cos(latRad))) * (180.0 / Math.PI);
|
||||||
|
var minLon = Math.Max(centerGeoPoint.Lon - lonDiff, -180.0);
|
||||||
|
var maxLon = Math.Min(centerGeoPoint.Lon + lonDiff, 180.0);
|
||||||
|
|
||||||
|
return (minLat, maxLat, minLon, maxLon);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
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<SatelliteDownloader> logger, IOptions<MapConfig> 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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<SatTile>();
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.21"/>
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.21"/>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user