mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-04-22 05:26:39 +00:00
route stitching
This commit is contained in:
+2
-1
@@ -9,4 +9,5 @@ logs/
|
|||||||
Content/
|
Content/
|
||||||
.env
|
.env
|
||||||
tiles/
|
tiles/
|
||||||
ready/
|
ready/
|
||||||
|
.DS_Store
|
||||||
@@ -36,6 +36,7 @@ builder.Services.AddSingleton<IRegionRequestQueue>(sp => new RegionRequestQueue(
|
|||||||
builder.Services.AddSingleton<IRegionService, RegionService>();
|
builder.Services.AddSingleton<IRegionService, RegionService>();
|
||||||
builder.Services.AddHostedService<RegionProcessingService>();
|
builder.Services.AddHostedService<RegionProcessingService>();
|
||||||
builder.Services.AddSingleton<IRouteService, RouteService>();
|
builder.Services.AddSingleton<IRouteService, RouteService>();
|
||||||
|
builder.Services.AddHostedService<RouteProcessingService>();
|
||||||
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen(c =>
|
builder.Services.AddSwaggerGen(c =>
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ public class CreateRouteRequest
|
|||||||
public double RegionSizeMeters { get; set; }
|
public double RegionSizeMeters { get; set; }
|
||||||
public int ZoomLevel { get; set; }
|
public int ZoomLevel { get; set; }
|
||||||
public List<RoutePoint> Points { get; set; } = new();
|
public List<RoutePoint> Points { get; set; } = new();
|
||||||
|
public bool RequestMaps { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ public class RouteResponse
|
|||||||
public double TotalDistanceMeters { get; set; }
|
public double TotalDistanceMeters { get; set; }
|
||||||
public int TotalPoints { get; set; }
|
public int TotalPoints { get; set; }
|
||||||
public List<RoutePointDto> Points { get; set; } = new();
|
public List<RoutePointDto> Points { get; set; } = new();
|
||||||
|
public bool RequestMaps { get; set; }
|
||||||
|
public bool MapsReady { get; set; }
|
||||||
|
public string? CsvFilePath { get; set; }
|
||||||
|
public string? SummaryFilePath { get; set; }
|
||||||
|
public string? StitchedImagePath { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public DateTime UpdatedAt { get; set; }
|
public DateTime UpdatedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE routes
|
||||||
|
ADD COLUMN request_maps BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
ADD COLUMN maps_ready BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
ADD COLUMN csv_file_path VARCHAR(500),
|
||||||
|
ADD COLUMN summary_file_path VARCHAR(500),
|
||||||
|
ADD COLUMN stitched_image_path VARCHAR(500);
|
||||||
|
|
||||||
@@ -9,6 +9,11 @@ public class RouteEntity
|
|||||||
public int ZoomLevel { get; set; }
|
public int ZoomLevel { get; set; }
|
||||||
public double TotalDistanceMeters { get; set; }
|
public double TotalDistanceMeters { get; set; }
|
||||||
public int TotalPoints { get; set; }
|
public int TotalPoints { get; set; }
|
||||||
|
public bool RequestMaps { get; set; }
|
||||||
|
public bool MapsReady { get; set; }
|
||||||
|
public string? CsvFilePath { get; set; }
|
||||||
|
public string? SummaryFilePath { get; set; }
|
||||||
|
public string? StitchedImagePath { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public DateTime UpdatedAt { get; set; }
|
public DateTime UpdatedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ public interface IRouteRepository
|
|||||||
Task<int> DeleteRouteAsync(Guid id);
|
Task<int> DeleteRouteAsync(Guid id);
|
||||||
Task LinkRouteToRegionAsync(Guid routeId, Guid regionId);
|
Task LinkRouteToRegionAsync(Guid routeId, Guid regionId);
|
||||||
Task<IEnumerable<Guid>> GetRegionIdsByRouteAsync(Guid routeId);
|
Task<IEnumerable<Guid>> GetRegionIdsByRouteAsync(Guid routeId);
|
||||||
|
Task<IEnumerable<RouteEntity>> GetRoutesWithPendingMapsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ public class RouteRepository : IRouteRepository
|
|||||||
const string sql = @"
|
const string sql = @"
|
||||||
SELECT id, name, description, region_size_meters as RegionSizeMeters,
|
SELECT id, name, description, region_size_meters as RegionSizeMeters,
|
||||||
zoom_level as ZoomLevel, total_distance_meters as TotalDistanceMeters,
|
zoom_level as ZoomLevel, total_distance_meters as TotalDistanceMeters,
|
||||||
total_points as TotalPoints, created_at as CreatedAt, updated_at as UpdatedAt
|
total_points as TotalPoints, request_maps as RequestMaps,
|
||||||
|
maps_ready as MapsReady, csv_file_path as CsvFilePath,
|
||||||
|
summary_file_path as SummaryFilePath, stitched_image_path as StitchedImagePath,
|
||||||
|
created_at as CreatedAt, updated_at as UpdatedAt
|
||||||
FROM routes
|
FROM routes
|
||||||
WHERE id = @Id";
|
WHERE id = @Id";
|
||||||
|
|
||||||
@@ -45,9 +48,13 @@ public class RouteRepository : IRouteRepository
|
|||||||
using var connection = new NpgsqlConnection(_connectionString);
|
using var connection = new NpgsqlConnection(_connectionString);
|
||||||
const string sql = @"
|
const string sql = @"
|
||||||
INSERT INTO routes (id, name, description, region_size_meters, zoom_level,
|
INSERT INTO routes (id, name, description, region_size_meters, zoom_level,
|
||||||
total_distance_meters, total_points, created_at, updated_at)
|
total_distance_meters, total_points, request_maps, maps_ready,
|
||||||
|
csv_file_path, summary_file_path, stitched_image_path,
|
||||||
|
created_at, updated_at)
|
||||||
VALUES (@Id, @Name, @Description, @RegionSizeMeters, @ZoomLevel,
|
VALUES (@Id, @Name, @Description, @RegionSizeMeters, @ZoomLevel,
|
||||||
@TotalDistanceMeters, @TotalPoints, @CreatedAt, @UpdatedAt)
|
@TotalDistanceMeters, @TotalPoints, @RequestMaps, @MapsReady,
|
||||||
|
@CsvFilePath, @SummaryFilePath, @StitchedImagePath,
|
||||||
|
@CreatedAt, @UpdatedAt)
|
||||||
RETURNING id";
|
RETURNING id";
|
||||||
|
|
||||||
return await connection.ExecuteScalarAsync<Guid>(sql, route);
|
return await connection.ExecuteScalarAsync<Guid>(sql, route);
|
||||||
@@ -76,6 +83,11 @@ public class RouteRepository : IRouteRepository
|
|||||||
zoom_level = @ZoomLevel,
|
zoom_level = @ZoomLevel,
|
||||||
total_distance_meters = @TotalDistanceMeters,
|
total_distance_meters = @TotalDistanceMeters,
|
||||||
total_points = @TotalPoints,
|
total_points = @TotalPoints,
|
||||||
|
request_maps = @RequestMaps,
|
||||||
|
maps_ready = @MapsReady,
|
||||||
|
csv_file_path = @CsvFilePath,
|
||||||
|
summary_file_path = @SummaryFilePath,
|
||||||
|
stitched_image_path = @StitchedImagePath,
|
||||||
updated_at = @UpdatedAt
|
updated_at = @UpdatedAt
|
||||||
WHERE id = @Id";
|
WHERE id = @Id";
|
||||||
|
|
||||||
@@ -110,5 +122,21 @@ public class RouteRepository : IRouteRepository
|
|||||||
|
|
||||||
return await connection.QueryAsync<Guid>(sql, new { RouteId = routeId });
|
return await connection.QueryAsync<Guid>(sql, new { RouteId = routeId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<RouteEntity>> GetRoutesWithPendingMapsAsync()
|
||||||
|
{
|
||||||
|
using var connection = new NpgsqlConnection(_connectionString);
|
||||||
|
const string sql = @"
|
||||||
|
SELECT id, name, description, region_size_meters as RegionSizeMeters,
|
||||||
|
zoom_level as ZoomLevel, total_distance_meters as TotalDistanceMeters,
|
||||||
|
total_points as TotalPoints, request_maps as RequestMaps,
|
||||||
|
maps_ready as MapsReady, csv_file_path as CsvFilePath,
|
||||||
|
summary_file_path as SummaryFilePath, stitched_image_path as StitchedImagePath,
|
||||||
|
created_at as CreatedAt, updated_at as UpdatedAt
|
||||||
|
FROM routes
|
||||||
|
WHERE request_maps = true AND maps_ready = false";
|
||||||
|
|
||||||
|
return await connection.QueryAsync<RouteEntity>(sql);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ public class CreateRouteRequest
|
|||||||
public double RegionSizeMeters { get; set; }
|
public double RegionSizeMeters { get; set; }
|
||||||
public int ZoomLevel { get; set; }
|
public int ZoomLevel { get; set; }
|
||||||
public List<RoutePointInput> Points { get; set; } = new();
|
public List<RoutePointInput> Points { get; set; } = new();
|
||||||
|
public bool RequestMaps { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RoutePointModel
|
public class RoutePointModel
|
||||||
@@ -80,6 +81,11 @@ public class RouteResponseModel
|
|||||||
public double TotalDistanceMeters { get; set; }
|
public double TotalDistanceMeters { get; set; }
|
||||||
public int TotalPoints { get; set; }
|
public int TotalPoints { get; set; }
|
||||||
public List<RoutePointModel> Points { get; set; } = new();
|
public List<RoutePointModel> Points { get; set; } = new();
|
||||||
|
public bool RequestMaps { get; set; }
|
||||||
|
public bool MapsReady { get; set; }
|
||||||
|
public string? CsvFilePath { get; set; }
|
||||||
|
public string? SummaryFilePath { get; set; }
|
||||||
|
public string? StitchedImagePath { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public DateTime UpdatedAt { get; set; }
|
public DateTime UpdatedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class Program
|
|||||||
|
|
||||||
await RouteTests.RunSimpleRouteTest(httpClient);
|
await RouteTests.RunSimpleRouteTest(httpClient);
|
||||||
|
|
||||||
|
await RouteTests.RunRouteWithRegionProcessingAndStitching(httpClient);
|
||||||
|
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine("=========================");
|
Console.WriteLine("=========================");
|
||||||
Console.WriteLine("All tests completed successfully!");
|
Console.WriteLine("All tests completed successfully!");
|
||||||
|
|||||||
@@ -117,5 +117,171 @@ public static class RouteTests
|
|||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine("Simple Route Test: PASSED");
|
Console.WriteLine("Simple Route Test: PASSED");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task RunRouteWithRegionProcessingAndStitching(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Test: Route with Region Processing and Full Map Stitching (Service-Level)");
|
||||||
|
Console.WriteLine("===========================================================================");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
var routeId = Guid.NewGuid();
|
||||||
|
var request = new CreateRouteRequest
|
||||||
|
{
|
||||||
|
Id = routeId,
|
||||||
|
Name = "Route with Region Processing",
|
||||||
|
Description = "Test route that processes regions for all points and stitches a full map",
|
||||||
|
RegionSizeMeters = 300.0,
|
||||||
|
ZoomLevel = 18,
|
||||||
|
RequestMaps = true,
|
||||||
|
Points = new List<RoutePointInput>
|
||||||
|
{
|
||||||
|
new() { Latitude = 48.276067180586544, Longitude = 37.38445758819581 },
|
||||||
|
new() { Latitude = 48.27074009522731, Longitude = 37.374029159545906 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Console.WriteLine("Step 1: Creating route with RequestMaps=true");
|
||||||
|
Console.WriteLine($" Start: ({request.Points[0].Latitude}, {request.Points[0].Longitude})");
|
||||||
|
Console.WriteLine($" End: ({request.Points[1].Latitude}, {request.Points[1].Longitude})");
|
||||||
|
Console.WriteLine($" Region Size: {request.RegionSizeMeters}m");
|
||||||
|
Console.WriteLine($" Zoom Level: {request.ZoomLevel}");
|
||||||
|
Console.WriteLine($" Request Maps: {request.RequestMaps}");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
var routeResponse = await httpClient.PostAsJsonAsync("/api/satellite/route", request);
|
||||||
|
|
||||||
|
if (!routeResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await routeResponse.Content.ReadAsStringAsync();
|
||||||
|
throw new Exception($"Route creation failed: {routeResponse.StatusCode} - {errorContent}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var route = await routeResponse.Content.ReadFromJsonAsync<RouteResponseModel>(JsonOptions);
|
||||||
|
|
||||||
|
if (route == null)
|
||||||
|
{
|
||||||
|
throw new Exception("No route data returned");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"✓ Route created with {route.TotalPoints} points");
|
||||||
|
Console.WriteLine($" Distance: {route.TotalDistanceMeters:F2}m");
|
||||||
|
Console.WriteLine($" Request Maps: {route.RequestMaps}");
|
||||||
|
Console.WriteLine($" Maps Ready: {route.MapsReady}");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
if (!route.RequestMaps)
|
||||||
|
{
|
||||||
|
throw new Exception("Expected RequestMaps to be true");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.MapsReady)
|
||||||
|
{
|
||||||
|
throw new Exception("Expected MapsReady to be false initially");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Step 2: Waiting for route maps to be ready");
|
||||||
|
Console.WriteLine(" (Service is processing regions and stitching automatically)");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
RouteResponseModel? finalRoute = null;
|
||||||
|
int maxAttempts = 180;
|
||||||
|
int pollInterval = 3000;
|
||||||
|
|
||||||
|
for (int attempt = 0; attempt < maxAttempts; attempt++)
|
||||||
|
{
|
||||||
|
await Task.Delay(pollInterval);
|
||||||
|
|
||||||
|
var getResponse = await httpClient.GetAsync($"/api/satellite/route/{routeId}");
|
||||||
|
|
||||||
|
if (!getResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new Exception($"Failed to get route status: {getResponse.StatusCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentRoute = await getResponse.Content.ReadFromJsonAsync<RouteResponseModel>(JsonOptions);
|
||||||
|
|
||||||
|
if (currentRoute == null)
|
||||||
|
{
|
||||||
|
throw new Exception("No route returned");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentRoute.MapsReady)
|
||||||
|
{
|
||||||
|
finalRoute = currentRoute;
|
||||||
|
Console.WriteLine($"✓ Route maps ready in approximately {attempt * pollInterval / 1000}s");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt % 5 == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" Waiting... (attempt {attempt + 1}/{maxAttempts})");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt == maxAttempts - 1)
|
||||||
|
{
|
||||||
|
throw new Exception($"Timeout: Route maps did not become ready in {maxAttempts * pollInterval / 1000}s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalRoute == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Final route is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
Console.WriteLine("Step 3: Verifying generated files");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(finalRoute.CsvFilePath))
|
||||||
|
{
|
||||||
|
throw new Exception("CSV file path is null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(finalRoute.SummaryFilePath))
|
||||||
|
{
|
||||||
|
throw new Exception("Summary file path is null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(finalRoute.StitchedImagePath))
|
||||||
|
{
|
||||||
|
throw new Exception("Stitched image path is null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(finalRoute.CsvFilePath))
|
||||||
|
{
|
||||||
|
throw new Exception($"CSV file not found: {finalRoute.CsvFilePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(finalRoute.SummaryFilePath))
|
||||||
|
{
|
||||||
|
throw new Exception($"Summary file not found: {finalRoute.SummaryFilePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(finalRoute.StitchedImagePath))
|
||||||
|
{
|
||||||
|
throw new Exception($"Stitched image not found: {finalRoute.StitchedImagePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var csvLines = await File.ReadAllLinesAsync(finalRoute.CsvFilePath);
|
||||||
|
var uniqueTileCount = csvLines.Length - 1;
|
||||||
|
|
||||||
|
var stitchedInfo = new FileInfo(finalRoute.StitchedImagePath);
|
||||||
|
|
||||||
|
Console.WriteLine("Files Generated:");
|
||||||
|
Console.WriteLine($" ✓ CSV: {Path.GetFileName(finalRoute.CsvFilePath)} ({uniqueTileCount} tiles)");
|
||||||
|
Console.WriteLine($" ✓ Summary: {Path.GetFileName(finalRoute.SummaryFilePath)}");
|
||||||
|
Console.WriteLine($" ✓ Stitched Map: {Path.GetFileName(finalRoute.StitchedImagePath)} ({stitchedInfo.Length / 1024:F2} KB)");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
Console.WriteLine("Route Summary:");
|
||||||
|
Console.WriteLine($" Route ID: {finalRoute.Id}");
|
||||||
|
Console.WriteLine($" Route Points: {finalRoute.TotalPoints}");
|
||||||
|
Console.WriteLine($" Distance: {finalRoute.TotalDistanceMeters:F2}m");
|
||||||
|
Console.WriteLine($" Unique Tiles: {uniqueTileCount}");
|
||||||
|
Console.WriteLine($" Maps Ready: {finalRoute.MapsReady}");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("✓ Route with Region Processing and Stitching Test: PASSED");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,424 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SatelliteProvider.Common.Configs;
|
||||||
|
using SatelliteProvider.Common.Utils;
|
||||||
|
using SatelliteProvider.DataAccess.Repositories;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
|
namespace SatelliteProvider.Services;
|
||||||
|
|
||||||
|
public class RouteProcessingService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IRouteRepository _routeRepository;
|
||||||
|
private readonly IRegionRepository _regionRepository;
|
||||||
|
private readonly StorageConfig _storageConfig;
|
||||||
|
private readonly ILogger<RouteProcessingService> _logger;
|
||||||
|
private readonly TimeSpan _checkInterval = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
public RouteProcessingService(
|
||||||
|
IRouteRepository routeRepository,
|
||||||
|
IRegionRepository regionRepository,
|
||||||
|
IOptions<StorageConfig> storageConfig,
|
||||||
|
ILogger<RouteProcessingService> logger)
|
||||||
|
{
|
||||||
|
_routeRepository = routeRepository;
|
||||||
|
_regionRepository = regionRepository;
|
||||||
|
_storageConfig = storageConfig.Value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Route Processing Service started");
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ProcessPendingRoutesAsync(stoppingToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error in route processing service");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(_checkInterval, stoppingToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Route Processing Service stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessPendingRoutesAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var pendingRoutes = await GetRoutesWithPendingMapsAsync();
|
||||||
|
|
||||||
|
foreach (var route in pendingRoutes)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ProcessRouteIfReadyAsync(route.Id, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error processing route {RouteId}", route.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<(Guid Id, bool RequestMaps)>> GetRoutesWithPendingMapsAsync()
|
||||||
|
{
|
||||||
|
var routes = await _routeRepository.GetRoutesWithPendingMapsAsync();
|
||||||
|
return routes.Select(r => (r.Id, r.RequestMaps)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessRouteIfReadyAsync(Guid routeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var route = await _routeRepository.GetByIdAsync(routeId);
|
||||||
|
if (route == null || !route.RequestMaps || route.MapsReady)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var regionIds = await _routeRepository.GetRegionIdsByRouteAsync(routeId);
|
||||||
|
if (!regionIds.Any())
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Route {RouteId} has no regions linked", routeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allCompleted = true;
|
||||||
|
var anyFailed = false;
|
||||||
|
|
||||||
|
foreach (var regionId in regionIds)
|
||||||
|
{
|
||||||
|
var region = await _regionRepository.GetByIdAsync(regionId);
|
||||||
|
if (region == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Region {RegionId} not found for route {RouteId}", regionId, routeId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (region.Status == "failed")
|
||||||
|
{
|
||||||
|
anyFailed = true;
|
||||||
|
}
|
||||||
|
else if (region.Status != "completed")
|
||||||
|
{
|
||||||
|
allCompleted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allCompleted)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyFailed)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Route {RouteId} has failed regions, skipping processing", routeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("All regions completed for route {RouteId}, starting final processing", routeId);
|
||||||
|
|
||||||
|
await GenerateRouteMapsAsync(routeId, route, regionIds, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GenerateRouteMapsAsync(
|
||||||
|
Guid routeId,
|
||||||
|
DataAccess.Models.RouteEntity route,
|
||||||
|
IEnumerable<Guid> regionIds,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var readyDir = _storageConfig.ReadyDirectory;
|
||||||
|
Directory.CreateDirectory(readyDir);
|
||||||
|
|
||||||
|
var allTiles = new Dictionary<string, TileInfo>();
|
||||||
|
int totalTilesFromRegions = 0;
|
||||||
|
int duplicateTiles = 0;
|
||||||
|
|
||||||
|
foreach (var regionId in regionIds)
|
||||||
|
{
|
||||||
|
var region = await _regionRepository.GetByIdAsync(regionId);
|
||||||
|
if (region == null || string.IsNullOrEmpty(region.CsvFilePath) || !File.Exists(region.CsvFilePath))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Region {RegionId} CSV not found for route {RouteId}", regionId, routeId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var csvLines = await File.ReadAllLinesAsync(region.CsvFilePath, cancellationToken);
|
||||||
|
|
||||||
|
foreach (var line in csvLines.Skip(1))
|
||||||
|
{
|
||||||
|
var parts = line.Split(',');
|
||||||
|
if (parts.Length < 3) continue;
|
||||||
|
|
||||||
|
if (!double.TryParse(parts[0], out var lat)) continue;
|
||||||
|
if (!double.TryParse(parts[1], out var lon)) continue;
|
||||||
|
var filePath = parts[2];
|
||||||
|
|
||||||
|
totalTilesFromRegions++;
|
||||||
|
var key = $"{lat:F6}_{lon:F6}";
|
||||||
|
|
||||||
|
if (!allTiles.ContainsKey(key))
|
||||||
|
{
|
||||||
|
allTiles[key] = new TileInfo
|
||||||
|
{
|
||||||
|
Latitude = lat,
|
||||||
|
Longitude = lon,
|
||||||
|
FilePath = filePath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
duplicateTiles++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Route {RouteId}: Collected {UniqueCount} unique tiles ({DuplicateCount} duplicates from {TotalCount} total)",
|
||||||
|
routeId, allTiles.Count, duplicateTiles, totalTilesFromRegions);
|
||||||
|
|
||||||
|
var csvPath = Path.Combine(readyDir, $"route_{routeId}_ready.csv");
|
||||||
|
await GenerateRouteCsvAsync(csvPath, allTiles.Values, cancellationToken);
|
||||||
|
|
||||||
|
string? stitchedImagePath = null;
|
||||||
|
if (route.RequestMaps)
|
||||||
|
{
|
||||||
|
stitchedImagePath = Path.Combine(readyDir, $"route_{routeId}_stitched.jpg");
|
||||||
|
await StitchRouteTilesAsync(allTiles.Values.ToList(), stitchedImagePath, route.ZoomLevel, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var summaryPath = Path.Combine(readyDir, $"route_{routeId}_summary.txt");
|
||||||
|
await GenerateRouteSummaryAsync(summaryPath, route, allTiles.Count, totalTilesFromRegions, duplicateTiles, cancellationToken);
|
||||||
|
|
||||||
|
route.MapsReady = true;
|
||||||
|
route.CsvFilePath = csvPath;
|
||||||
|
route.SummaryFilePath = summaryPath;
|
||||||
|
route.StitchedImagePath = stitchedImagePath;
|
||||||
|
route.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _routeRepository.UpdateRouteAsync(route);
|
||||||
|
|
||||||
|
_logger.LogInformation("Route {RouteId} maps processing completed successfully", routeId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error generating maps for route {RouteId}", routeId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GenerateRouteCsvAsync(
|
||||||
|
string filePath,
|
||||||
|
IEnumerable<TileInfo> tiles,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var orderedTiles = tiles.OrderByDescending(t => t.Latitude).ThenBy(t => t.Longitude).ToList();
|
||||||
|
|
||||||
|
using var writer = new StreamWriter(filePath);
|
||||||
|
await writer.WriteLineAsync("latitude,longitude,file_path");
|
||||||
|
|
||||||
|
foreach (var tile in orderedTiles)
|
||||||
|
{
|
||||||
|
await writer.WriteLineAsync($"{tile.Latitude:F6},{tile.Longitude:F6},{tile.FilePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Route CSV generated: {FilePath} with {Count} tiles", filePath, orderedTiles.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GenerateRouteSummaryAsync(
|
||||||
|
string filePath,
|
||||||
|
DataAccess.Models.RouteEntity route,
|
||||||
|
int uniqueTiles,
|
||||||
|
int totalTilesFromRegions,
|
||||||
|
int duplicateTiles,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var summary = new System.Text.StringBuilder();
|
||||||
|
summary.AppendLine("Route Maps Summary");
|
||||||
|
summary.AppendLine("==================");
|
||||||
|
summary.AppendLine($"Route ID: {route.Id}");
|
||||||
|
summary.AppendLine($"Route Name: {route.Name}");
|
||||||
|
if (!string.IsNullOrEmpty(route.Description))
|
||||||
|
{
|
||||||
|
summary.AppendLine($"Description: {route.Description}");
|
||||||
|
}
|
||||||
|
summary.AppendLine($"Total Points: {route.TotalPoints}");
|
||||||
|
summary.AppendLine($"Total Distance: {route.TotalDistanceMeters:F2} meters");
|
||||||
|
summary.AppendLine($"Region Size: {route.RegionSizeMeters:F0} meters");
|
||||||
|
summary.AppendLine($"Zoom Level: {route.ZoomLevel}");
|
||||||
|
summary.AppendLine();
|
||||||
|
summary.AppendLine("Tile Statistics:");
|
||||||
|
summary.AppendLine($"- Unique Tiles: {uniqueTiles}");
|
||||||
|
summary.AppendLine($"- Total Tiles from Regions: {totalTilesFromRegions}");
|
||||||
|
summary.AppendLine($"- Duplicate Tiles (overlap): {duplicateTiles}");
|
||||||
|
summary.AppendLine();
|
||||||
|
summary.AppendLine("Files Created:");
|
||||||
|
summary.AppendLine($"- CSV: route_{route.Id}_ready.csv");
|
||||||
|
summary.AppendLine($"- Summary: route_{route.Id}_summary.txt");
|
||||||
|
if (route.RequestMaps)
|
||||||
|
{
|
||||||
|
summary.AppendLine($"- Stitched Map: route_{route.Id}_stitched.jpg");
|
||||||
|
}
|
||||||
|
summary.AppendLine();
|
||||||
|
summary.AppendLine($"Completed: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(filePath, summary.ToString(), cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("Route summary generated: {FilePath}", filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StitchRouteTilesAsync(
|
||||||
|
List<TileInfo> tiles,
|
||||||
|
string outputPath,
|
||||||
|
int zoomLevel,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (tiles.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No tiles to stitch for route map");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int tileSizePixels = 256;
|
||||||
|
|
||||||
|
var tileCoords = tiles.Select(t =>
|
||||||
|
{
|
||||||
|
var (tileX, tileY) = ExtractTileCoordinatesFromFilename(t.FilePath);
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
t.Latitude,
|
||||||
|
t.Longitude,
|
||||||
|
t.FilePath,
|
||||||
|
TileX = tileX,
|
||||||
|
TileY = tileY
|
||||||
|
};
|
||||||
|
}).Where(t => t.TileX >= 0 && t.TileY >= 0).ToList();
|
||||||
|
|
||||||
|
var minX = tileCoords.Min(t => t.TileX);
|
||||||
|
var maxX = tileCoords.Max(t => t.TileX);
|
||||||
|
var minY = tileCoords.Min(t => t.TileY);
|
||||||
|
var maxY = tileCoords.Max(t => t.TileY);
|
||||||
|
|
||||||
|
var gridWidth = maxX - minX + 1;
|
||||||
|
var gridHeight = maxY - minY + 1;
|
||||||
|
var imageWidth = gridWidth * tileSizePixels;
|
||||||
|
var imageHeight = gridHeight * tileSizePixels;
|
||||||
|
|
||||||
|
_logger.LogInformation("Stitching route map: {Width}x{Height} pixels (grid: {GridWidth}x{GridHeight} tiles)",
|
||||||
|
imageWidth, imageHeight, gridWidth, gridHeight);
|
||||||
|
_logger.LogInformation("Bounding box: top={MinY}, left={MinX}, bottom={MaxY}, right={MaxX}",
|
||||||
|
minY, minX, maxY, maxX);
|
||||||
|
|
||||||
|
using var stitchedImage = new Image<Rgb24>(imageWidth, imageHeight);
|
||||||
|
stitchedImage.Mutate(ctx => ctx.BackgroundColor(Color.Black));
|
||||||
|
|
||||||
|
var uniqueTileCoords = tileCoords
|
||||||
|
.GroupBy(t => $"{t.TileX}_{t.TileY}")
|
||||||
|
.Select(g => g.First())
|
||||||
|
.OrderBy(t => t.TileY)
|
||||||
|
.ThenBy(t => t.TileX)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_logger.LogInformation("Unique tiles to place: {Count}", uniqueTileCoords.Count);
|
||||||
|
_logger.LogInformation("Sample tiles (first 5):");
|
||||||
|
foreach (var sample in uniqueTileCoords.Take(5))
|
||||||
|
{
|
||||||
|
_logger.LogInformation(" Tile ({TileX}, {TileY}) from ({Lat:F6}, {Lon:F6})",
|
||||||
|
sample.TileX, sample.TileY, sample.Latitude, sample.Longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
int placedTiles = 0;
|
||||||
|
int missingTiles = 0;
|
||||||
|
|
||||||
|
foreach (var tile in uniqueTileCoords)
|
||||||
|
{
|
||||||
|
var destX = (tile.TileX - minX) * tileSizePixels;
|
||||||
|
var destY = (tile.TileY - minY) * tileSizePixels;
|
||||||
|
|
||||||
|
if (File.Exists(tile.FilePath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var tileImage = await Image.LoadAsync<Rgb24>(tile.FilePath, cancellationToken);
|
||||||
|
|
||||||
|
if (tileImage.Width != tileSizePixels || tileImage.Height != tileSizePixels)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Tile {FilePath} has wrong size {Width}x{Height}, expected {Expected}x{Expected}",
|
||||||
|
tile.FilePath, tileImage.Width, tileImage.Height, tileSizePixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
stitchedImage.Mutate(ctx => ctx.DrawImage(tileImage, new Point(destX, destY), 1f));
|
||||||
|
placedTiles++;
|
||||||
|
|
||||||
|
if (placedTiles <= 3)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Placed tile {Count}: ({TileX},{TileY}) at pixel ({DestX},{DestY})",
|
||||||
|
placedTiles, tile.TileX, tile.TileY, destX, destY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to load tile at {FilePath}, leaving black", tile.FilePath);
|
||||||
|
missingTiles++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Tile file not found: {FilePath}, leaving black", tile.FilePath);
|
||||||
|
missingTiles++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await stitchedImage.SaveAsJpegAsync(outputPath, cancellationToken);
|
||||||
|
|
||||||
|
var totalPossibleTiles = gridWidth * gridHeight;
|
||||||
|
var uncoveredTiles = totalPossibleTiles - placedTiles - missingTiles;
|
||||||
|
|
||||||
|
_logger.LogInformation("Route map stitched: {OutputPath}", outputPath);
|
||||||
|
_logger.LogInformation(" Tiles placed: {PlacedTiles}", placedTiles);
|
||||||
|
_logger.LogInformation(" Tiles missing (file issues): {MissingTiles}", missingTiles);
|
||||||
|
_logger.LogInformation(" Uncovered area (black): {UncoveredTiles} tiles", uncoveredTiles);
|
||||||
|
_logger.LogInformation(" Total canvas: {TotalTiles} tiles ({GridWidth}x{GridHeight})",
|
||||||
|
totalPossibleTiles, gridWidth, gridHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int TileX, int TileY) ExtractTileCoordinatesFromFilename(string filePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var filename = Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
var parts = filename.Split('_');
|
||||||
|
|
||||||
|
if (parts.Length >= 4 && parts[0] == "tile")
|
||||||
|
{
|
||||||
|
if (int.TryParse(parts[2], out var tileX) && int.TryParse(parts[3], out var tileY))
|
||||||
|
{
|
||||||
|
return (tileX, tileY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return (-1, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TileInfo
|
||||||
|
{
|
||||||
|
public double Latitude { get; set; }
|
||||||
|
public double Longitude { get; set; }
|
||||||
|
public string FilePath { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -10,14 +10,17 @@ namespace SatelliteProvider.Services;
|
|||||||
public class RouteService : IRouteService
|
public class RouteService : IRouteService
|
||||||
{
|
{
|
||||||
private readonly IRouteRepository _routeRepository;
|
private readonly IRouteRepository _routeRepository;
|
||||||
|
private readonly IRegionService _regionService;
|
||||||
private readonly ILogger<RouteService> _logger;
|
private readonly ILogger<RouteService> _logger;
|
||||||
private const double MAX_POINT_SPACING_METERS = 200.0;
|
private const double MAX_POINT_SPACING_METERS = 200.0;
|
||||||
|
|
||||||
public RouteService(
|
public RouteService(
|
||||||
IRouteRepository routeRepository,
|
IRouteRepository routeRepository,
|
||||||
|
IRegionService regionService,
|
||||||
ILogger<RouteService> logger)
|
ILogger<RouteService> logger)
|
||||||
{
|
{
|
||||||
_routeRepository = routeRepository;
|
_routeRepository = routeRepository;
|
||||||
|
_regionService = regionService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,10 +57,10 @@ public class RouteService : IRouteService
|
|||||||
var geoPoint = new GeoPoint(currentPoint.Latitude, currentPoint.Longitude);
|
var geoPoint = new GeoPoint(currentPoint.Latitude, currentPoint.Longitude);
|
||||||
|
|
||||||
double? distanceFromPrevious = null;
|
double? distanceFromPrevious = null;
|
||||||
if (segmentIndex > 0)
|
if (allPoints.Count > 0)
|
||||||
{
|
{
|
||||||
var prevPoint = request.Points[segmentIndex - 1];
|
var lastAddedPoint = allPoints[^1];
|
||||||
var prevGeoPoint = new GeoPoint(prevPoint.Latitude, prevPoint.Longitude);
|
var prevGeoPoint = new GeoPoint(lastAddedPoint.Latitude, lastAddedPoint.Longitude);
|
||||||
distanceFromPrevious = GeoUtils.CalculateDistance(prevGeoPoint, geoPoint);
|
distanceFromPrevious = GeoUtils.CalculateDistance(prevGeoPoint, geoPoint);
|
||||||
totalDistance += distanceFromPrevious.Value;
|
totalDistance += distanceFromPrevious.Value;
|
||||||
}
|
}
|
||||||
@@ -87,9 +90,8 @@ public class RouteService : IRouteService
|
|||||||
|
|
||||||
foreach (var intermediateGeo in intermediatePoints)
|
foreach (var intermediateGeo in intermediatePoints)
|
||||||
{
|
{
|
||||||
var prevGeo = sequenceNumber == 1 ? startGeo : new GeoPoint(
|
var lastAddedPoint = allPoints[^1];
|
||||||
allPoints[sequenceNumber - 1].Latitude,
|
var prevGeo = new GeoPoint(lastAddedPoint.Latitude, lastAddedPoint.Longitude);
|
||||||
allPoints[sequenceNumber - 1].Longitude);
|
|
||||||
|
|
||||||
var distFromPrev = GeoUtils.CalculateDistance(prevGeo, intermediateGeo);
|
var distFromPrev = GeoUtils.CalculateDistance(prevGeo, intermediateGeo);
|
||||||
totalDistance += distFromPrev;
|
totalDistance += distFromPrev;
|
||||||
@@ -120,6 +122,8 @@ public class RouteService : IRouteService
|
|||||||
ZoomLevel = request.ZoomLevel,
|
ZoomLevel = request.ZoomLevel,
|
||||||
TotalDistanceMeters = totalDistance,
|
TotalDistanceMeters = totalDistance,
|
||||||
TotalPoints = allPoints.Count,
|
TotalPoints = allPoints.Count,
|
||||||
|
RequestMaps = request.RequestMaps,
|
||||||
|
MapsReady = false,
|
||||||
CreatedAt = now,
|
CreatedAt = now,
|
||||||
UpdatedAt = now
|
UpdatedAt = now
|
||||||
};
|
};
|
||||||
@@ -141,6 +145,30 @@ public class RouteService : IRouteService
|
|||||||
|
|
||||||
await _routeRepository.InsertRoutePointsAsync(pointEntities);
|
await _routeRepository.InsertRoutePointsAsync(pointEntities);
|
||||||
|
|
||||||
|
if (request.RequestMaps)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Route {RouteId}: Requesting regions for all {Count} points",
|
||||||
|
request.Id, allPoints.Count);
|
||||||
|
|
||||||
|
foreach (var point in allPoints)
|
||||||
|
{
|
||||||
|
var regionId = Guid.NewGuid();
|
||||||
|
|
||||||
|
await _regionService.RequestRegionAsync(
|
||||||
|
regionId,
|
||||||
|
point.Latitude,
|
||||||
|
point.Longitude,
|
||||||
|
request.RegionSizeMeters,
|
||||||
|
request.ZoomLevel,
|
||||||
|
stitchTiles: false);
|
||||||
|
|
||||||
|
await _routeRepository.LinkRouteToRegionAsync(request.Id, regionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Route {RouteId}: {Count} regions requested",
|
||||||
|
request.Id, allPoints.Count);
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Route {RouteId} created successfully", request.Id);
|
_logger.LogInformation("Route {RouteId} created successfully", request.Id);
|
||||||
|
|
||||||
return new RouteResponse
|
return new RouteResponse
|
||||||
@@ -153,6 +181,11 @@ public class RouteService : IRouteService
|
|||||||
TotalDistanceMeters = routeEntity.TotalDistanceMeters,
|
TotalDistanceMeters = routeEntity.TotalDistanceMeters,
|
||||||
TotalPoints = routeEntity.TotalPoints,
|
TotalPoints = routeEntity.TotalPoints,
|
||||||
Points = allPoints,
|
Points = allPoints,
|
||||||
|
RequestMaps = routeEntity.RequestMaps,
|
||||||
|
MapsReady = routeEntity.MapsReady,
|
||||||
|
CsvFilePath = routeEntity.CsvFilePath,
|
||||||
|
SummaryFilePath = routeEntity.SummaryFilePath,
|
||||||
|
StitchedImagePath = routeEntity.StitchedImagePath,
|
||||||
CreatedAt = routeEntity.CreatedAt,
|
CreatedAt = routeEntity.CreatedAt,
|
||||||
UpdatedAt = routeEntity.UpdatedAt
|
UpdatedAt = routeEntity.UpdatedAt
|
||||||
};
|
};
|
||||||
@@ -186,6 +219,11 @@ public class RouteService : IRouteService
|
|||||||
SegmentIndex = p.SegmentIndex,
|
SegmentIndex = p.SegmentIndex,
|
||||||
DistanceFromPrevious = p.DistanceFromPrevious
|
DistanceFromPrevious = p.DistanceFromPrevious
|
||||||
}).ToList(),
|
}).ToList(),
|
||||||
|
RequestMaps = route.RequestMaps,
|
||||||
|
MapsReady = route.MapsReady,
|
||||||
|
CsvFilePath = route.CsvFilePath,
|
||||||
|
SummaryFilePath = route.SummaryFilePath,
|
||||||
|
StitchedImagePath = route.StitchedImagePath,
|
||||||
CreatedAt = route.CreatedAt,
|
CreatedAt = route.CreatedAt,
|
||||||
UpdatedAt = route.UpdatedAt
|
UpdatedAt = route.UpdatedAt
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ services:
|
|||||||
container_name: satellite-provider-integration-tests
|
container_name: satellite-provider-integration-tests
|
||||||
environment:
|
environment:
|
||||||
- API_URL=http://api:8080
|
- API_URL=http://api:8080
|
||||||
|
volumes:
|
||||||
|
- ./ready:/app/ready
|
||||||
|
- ./tiles:/app/tiles
|
||||||
depends_on:
|
depends_on:
|
||||||
api:
|
api:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
|
|||||||
Reference in New Issue
Block a user