mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-23 15:01:13 +00:00
Add TileProvision configuration and gRPC service for tile delivery
- Introduced new TileProvision settings in appsettings.json, including MaxTilesPerBatch and ProgressEmitIntervalSeconds. - Configured TileProvisionConfig in Program.cs to bind the new settings. - Added gRPC service for RouteTileDelivery in Program.cs to handle tile delivery requests. - Updated SatelliteProvider.Api.csproj to include Grpc.AspNetCore package and added protobuf file for tile provision. - Enhanced AuthenticationServiceCollectionExtensions to handle JWT token extraction from the Authorization header. - Registered additional services in RouteManagementServiceCollectionExtensions for tile processing. These changes enhance the API's capability to manage tile provisioning and delivery efficiently.
This commit is contained in:
@@ -41,6 +41,20 @@ public static class AuthenticationServiceCollectionExtensions
|
||||
RequireSignedTokens = true,
|
||||
RequireExpirationTime = true
|
||||
};
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(authHeader)
|
||||
&& authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
context.Token = authHeader["Bearer ".Length..].Trim();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return services;
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Satellite.V1;
|
||||
using SatelliteProvider.Services.RouteManagement.TileProvision;
|
||||
|
||||
namespace SatelliteProvider.Api.Grpc;
|
||||
|
||||
[Authorize]
|
||||
public sealed class RouteTileDeliveryGrpcService : RouteTileDelivery.RouteTileDeliveryBase
|
||||
{
|
||||
private readonly RouteTileDeliveryOrchestrator _orchestrator;
|
||||
private readonly ILogger<RouteTileDeliveryGrpcService> _logger;
|
||||
|
||||
public RouteTileDeliveryGrpcService(
|
||||
RouteTileDeliveryOrchestrator orchestrator,
|
||||
ILogger<RouteTileDeliveryGrpcService> logger)
|
||||
{
|
||||
_orchestrator = orchestrator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override async Task DeliverRouteTiles(
|
||||
DeliverRouteTilesRequest request,
|
||||
IServerStreamWriter<RouteTileEvent> responseStream,
|
||||
ServerCallContext context)
|
||||
{
|
||||
if (request.Route is null)
|
||||
{
|
||||
await WriteErrorAsync(responseStream, "INVALID_REQUEST", "route is required", retryable: false, context.CancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(request.Route.RouteId, out var routeId))
|
||||
{
|
||||
await WriteErrorAsync(responseStream, "INVALID_REQUEST", "route_id must be a UUID", retryable: false, context.CancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var job = MapJob(request, routeId);
|
||||
var sink = new GrpcRouteTileDeliverySink(responseStream, context.CancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
await _orchestrator.DeliverAsync(job, sink, context.CancellationToken);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Invalid route tile delivery request for route {RouteId}", routeId);
|
||||
await WriteErrorAsync(responseStream, "INVALID_REQUEST", ex.Message, retryable: false, context.CancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Route tile delivery failed for route {RouteId}", routeId);
|
||||
await WriteErrorAsync(responseStream, "INTERNAL_ERROR", ex.Message, retryable: true, context.CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private static RouteTileDeliveryJob MapJob(DeliverRouteTilesRequest request, Guid routeId)
|
||||
{
|
||||
var route = request.Route!;
|
||||
var waypoints = route.Waypoints
|
||||
.Select(w => (w.Lat, w.Lon))
|
||||
.ToList();
|
||||
|
||||
var geofences = route.Geofences
|
||||
.Select(polygon => (IReadOnlyList<(double Lat, double Lon)>)polygon.Vertices
|
||||
.Select(v => (v.Lat, v.Lon))
|
||||
.ToList())
|
||||
.ToList();
|
||||
|
||||
var clientTiles = request.ClientTiles
|
||||
.Select(MapClientTile)
|
||||
.ToList();
|
||||
|
||||
return new RouteTileDeliveryJob(
|
||||
routeId,
|
||||
waypoints,
|
||||
route.RegionSizeMeters,
|
||||
route.Zoom,
|
||||
geofences,
|
||||
route.IncludeGeofenceTiles,
|
||||
clientTiles);
|
||||
}
|
||||
|
||||
private static ClientTileSnapshot MapClientTile(ClientTileRecord record)
|
||||
{
|
||||
var capturedAt = record.CapturedAt?.ToDateTime().ToUniversalTime() ?? DateTime.MinValue;
|
||||
byte[]? hash = record.ContentSha256 is { Length: > 0 } sha ? sha.ToByteArray() : null;
|
||||
return new ClientTileSnapshot(
|
||||
record.Z,
|
||||
record.X,
|
||||
record.Y,
|
||||
record.ResolutionMPerPx,
|
||||
capturedAt,
|
||||
hash);
|
||||
}
|
||||
|
||||
private static async Task WriteErrorAsync(
|
||||
IServerStreamWriter<RouteTileEvent> responseStream,
|
||||
string code,
|
||||
string message,
|
||||
bool retryable,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await responseStream.WriteAsync(new RouteTileEvent
|
||||
{
|
||||
Error = new DeliveryError
|
||||
{
|
||||
Code = code,
|
||||
Message = message,
|
||||
Retryable = retryable,
|
||||
},
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private sealed class GrpcRouteTileDeliverySink : IRouteTileDeliverySink
|
||||
{
|
||||
private readonly IServerStreamWriter<RouteTileEvent> _stream;
|
||||
|
||||
public GrpcRouteTileDeliverySink(IServerStreamWriter<RouteTileEvent> stream, CancellationToken cancellationToken)
|
||||
{
|
||||
_stream = stream;
|
||||
_ = cancellationToken;
|
||||
}
|
||||
|
||||
public async ValueTask WriteManifestAsync(uint totalCandidates, uint skippedByClient, uint toDeliver, CancellationToken cancellationToken)
|
||||
{
|
||||
await _stream.WriteAsync(new RouteTileEvent
|
||||
{
|
||||
Manifest = new RouteManifest
|
||||
{
|
||||
TotalCandidates = totalCandidates,
|
||||
SkippedByClient = skippedByClient,
|
||||
ToDeliver = toDeliver,
|
||||
},
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async ValueTask WriteBatchAsync(uint batchSeq, IReadOnlyList<PreparedTileDelivery> tiles, CancellationToken cancellationToken)
|
||||
{
|
||||
var batch = new TileBatch { BatchSeq = batchSeq };
|
||||
foreach (var tile in tiles)
|
||||
{
|
||||
batch.Tiles.Add(new TilePayload
|
||||
{
|
||||
Z = tile.Candidate.Z,
|
||||
X = tile.Candidate.X,
|
||||
Y = tile.Candidate.Y,
|
||||
ResolutionMPerPx = tile.ResolutionMetersPerPx,
|
||||
CapturedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(tile.CapturedAtUtc, DateTimeKind.Utc)),
|
||||
Source = tile.Source,
|
||||
Jpeg = Google.Protobuf.ByteString.CopyFrom(tile.Jpeg),
|
||||
ContentSha256 = Google.Protobuf.ByteString.CopyFrom(tile.ContentSha256),
|
||||
RoutePriority = tile.Candidate.RoutePriority,
|
||||
});
|
||||
}
|
||||
|
||||
await _stream.WriteAsync(new RouteTileEvent { Batch = batch }, cancellationToken);
|
||||
}
|
||||
|
||||
public async ValueTask WriteProgressAsync(uint delivered, uint total, uint downloading, CancellationToken cancellationToken)
|
||||
{
|
||||
await _stream.WriteAsync(new RouteTileEvent
|
||||
{
|
||||
Progress = new ProgressUpdate
|
||||
{
|
||||
Delivered = delivered,
|
||||
Total = total,
|
||||
Downloading = downloading,
|
||||
},
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async ValueTask WriteCompleteAsync(uint delivered, uint skippedClient, uint skippedServerFilter, CancellationToken cancellationToken)
|
||||
{
|
||||
await _stream.WriteAsync(new RouteTileEvent
|
||||
{
|
||||
Complete = new DeliveryComplete
|
||||
{
|
||||
Delivered = delivered,
|
||||
SkippedClient = skippedClient,
|
||||
SkippedServerFilter = skippedServerFilter,
|
||||
},
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async ValueTask WriteErrorAsync(string code, string message, bool retryable, CancellationToken cancellationToken)
|
||||
{
|
||||
await _stream.WriteAsync(new RouteTileEvent
|
||||
{
|
||||
Error = new DeliveryError
|
||||
{
|
||||
Code = code,
|
||||
Message = message,
|
||||
Retryable = retryable,
|
||||
},
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using SatelliteProvider.Api;
|
||||
using SatelliteProvider.Api.Authentication;
|
||||
using SatelliteProvider.Api.DTOs;
|
||||
using SatelliteProvider.Api.Grpc;
|
||||
using SatelliteProvider.Api.Swagger;
|
||||
using SatelliteProvider.Api.Validators;
|
||||
using SatelliteProvider.DataAccess;
|
||||
@@ -35,6 +36,7 @@ builder.Services.Configure<MapConfig>(builder.Configuration.GetSection("MapConfi
|
||||
builder.Services.Configure<StorageConfig>(builder.Configuration.GetSection("StorageConfig"));
|
||||
builder.Services.Configure<ProcessingConfig>(builder.Configuration.GetSection("ProcessingConfig"));
|
||||
builder.Services.Configure<UavQualityConfig>(builder.Configuration.GetSection("UavQuality"));
|
||||
builder.Services.Configure<TileProvisionConfig>(builder.Configuration.GetSection("TileProvision"));
|
||||
|
||||
var uavQuality = builder.Configuration.GetSection("UavQuality").Get<UavQualityConfig>() ?? new UavQualityConfig();
|
||||
var uavBatchBodyLimit = checked((long)uavQuality.MaxBatchSize * uavQuality.MaxBytes);
|
||||
@@ -127,6 +129,12 @@ GlobalValidatorConfig.ApplyOnce();
|
||||
// options constructor deps. Transient so each request gets a fresh instance.
|
||||
builder.Services.AddTransient<UavUploadValidationFilter>();
|
||||
|
||||
builder.Services.AddGrpc(options =>
|
||||
{
|
||||
options.MaxReceiveMessageSize = 16 * 1024 * 1024;
|
||||
options.MaxSendMessageSize = 64 * 1024 * 1024;
|
||||
});
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
@@ -205,6 +213,8 @@ app.UseCors("TilesCors");
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapGrpcService<RouteTileDeliveryGrpcService>();
|
||||
|
||||
app.MapGet("/tiles/{z:int}/{x:int}/{y:int}", ServeTile)
|
||||
.RequireAuthorization()
|
||||
.WithOpenApi(op => new(op) { Summary = "Get satellite tile image by z/x/y coordinates (Slippy Map tile server)" });
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package satellite.v1;
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
option csharp_namespace = "Satellite.V1";
|
||||
|
||||
service RouteTileDelivery {
|
||||
rpc DeliverRouteTiles(DeliverRouteTilesRequest) returns (stream RouteTileEvent);
|
||||
}
|
||||
|
||||
message DeliverRouteTilesRequest {
|
||||
RouteSpec route = 1;
|
||||
repeated ClientTileRecord client_tiles = 2;
|
||||
}
|
||||
|
||||
message RouteSpec {
|
||||
string route_id = 1;
|
||||
repeated Waypoint waypoints = 2;
|
||||
double region_size_meters = 3;
|
||||
int32 zoom = 4;
|
||||
repeated GeofencePolygon geofences = 5;
|
||||
bool include_geofence_tiles = 6;
|
||||
}
|
||||
|
||||
message Waypoint {
|
||||
double lat = 1;
|
||||
double lon = 2;
|
||||
}
|
||||
|
||||
message GeofencePolygon {
|
||||
repeated Waypoint vertices = 1;
|
||||
}
|
||||
|
||||
message ClientTileRecord {
|
||||
int32 z = 1;
|
||||
int32 x = 2;
|
||||
int32 y = 3;
|
||||
double resolution_m_per_px = 4;
|
||||
google.protobuf.Timestamp captured_at = 5;
|
||||
optional string source = 6;
|
||||
bytes content_sha256 = 7;
|
||||
}
|
||||
|
||||
message RouteTileEvent {
|
||||
oneof payload {
|
||||
RouteManifest manifest = 1;
|
||||
TileBatch batch = 2;
|
||||
ProgressUpdate progress = 3;
|
||||
DeliveryComplete complete = 4;
|
||||
DeliveryError error = 5;
|
||||
}
|
||||
}
|
||||
|
||||
message RouteManifest {
|
||||
uint32 total_candidates = 1;
|
||||
uint32 skipped_by_client = 2;
|
||||
uint32 to_deliver = 3;
|
||||
}
|
||||
|
||||
message TileBatch {
|
||||
uint32 batch_seq = 1;
|
||||
repeated TilePayload tiles = 2;
|
||||
}
|
||||
|
||||
message TilePayload {
|
||||
int32 z = 1;
|
||||
int32 x = 2;
|
||||
int32 y = 3;
|
||||
double resolution_m_per_px = 4;
|
||||
google.protobuf.Timestamp captured_at = 5;
|
||||
string source = 6;
|
||||
bytes jpeg = 7;
|
||||
bytes content_sha256 = 8;
|
||||
uint32 route_priority = 9;
|
||||
}
|
||||
|
||||
message ProgressUpdate {
|
||||
uint32 delivered = 1;
|
||||
uint32 total = 2;
|
||||
uint32 downloading = 3;
|
||||
}
|
||||
|
||||
message DeliveryComplete {
|
||||
uint32 delivered = 1;
|
||||
uint32 skipped_client = 2;
|
||||
uint32 skipped_server_filter = 3;
|
||||
}
|
||||
|
||||
message DeliveryError {
|
||||
string code = 1;
|
||||
string message = 2;
|
||||
bool retryable = 3;
|
||||
}
|
||||
@@ -7,9 +7,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" Version="12.0.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.7" />
|
||||
<PackageReference Include="FluentValidation" Version="12.0.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7"/>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
@@ -26,4 +27,8 @@
|
||||
<ProjectReference Include="..\SatelliteProvider.Services.RouteManagement\SatelliteProvider.Services.RouteManagement.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Protos\tile_provision.proto" GrpcServices="Server" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -61,6 +61,10 @@
|
||||
"MaxRoutePointSpacingMeters": 200.0,
|
||||
"LatLonTolerance": 0.0001
|
||||
},
|
||||
"TileProvision": {
|
||||
"MaxTilesPerBatch": 200,
|
||||
"ProgressEmitIntervalSeconds": 2
|
||||
},
|
||||
"CorsConfig": {
|
||||
"AllowedOrigins": []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user