mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 08:21:14 +00:00
[AZ-312] [AZ-313] [AZ-314] Split Services into per-component csprojs
Phase B of architecture coupling refactor (epic AZ-309). Replaces the monolithic SatelliteProvider.Services with three per-component csprojs to add a compiler-enforced module boundary (resolves F4): - SatelliteProvider.Services.TileDownloader - SatelliteProvider.Services.RegionProcessing - SatelliteProvider.Services.RouteManagement DI registrations relocated into per-component AddTileDownloader / AddRegionProcessing / AddRouteManagement extension methods called from Program.cs. RateLimitException moved to Common/Exceptions/ to keep the three new csprojs as siblings (no Region->TileDownloader ProjectReference). Dockerfiles and consumer csprojs (Api, Tests) rewired to the new project paths. No DI lifetime or hosted-service order changes. Build: 0 warn, 0 err. Unit tests: 40/40. Smoke integration: green. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SatelliteProvider.Common.Configs;
|
||||
using SatelliteProvider.Common.Interfaces;
|
||||
|
||||
namespace SatelliteProvider.Services.RegionProcessing;
|
||||
|
||||
public class RegionProcessingService : BackgroundService
|
||||
{
|
||||
private readonly IRegionRequestQueue _queue;
|
||||
private readonly IRegionService _regionService;
|
||||
private readonly ProcessingConfig _processingConfig;
|
||||
private readonly ILogger<RegionProcessingService> _logger;
|
||||
|
||||
public RegionProcessingService(
|
||||
IRegionRequestQueue queue,
|
||||
IRegionService regionService,
|
||||
IOptions<ProcessingConfig> processingConfig,
|
||||
ILogger<RegionProcessingService> logger)
|
||||
{
|
||||
_queue = queue;
|
||||
_regionService = regionService;
|
||||
_processingConfig = processingConfig.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Region Processing Service started with {MaxConcurrent} parallel workers",
|
||||
_processingConfig.MaxConcurrentRegions);
|
||||
|
||||
var workers = new List<Task>();
|
||||
for (int i = 0; i < _processingConfig.MaxConcurrentRegions; i++)
|
||||
{
|
||||
var workerId = i + 1;
|
||||
workers.Add(ProcessRegionWorkerAsync(workerId, stoppingToken));
|
||||
}
|
||||
|
||||
await Task.WhenAll(workers);
|
||||
|
||||
_logger.LogInformation("Region Processing Service stopped");
|
||||
}
|
||||
|
||||
private async Task ProcessRegionWorkerAsync(int workerId, CancellationToken stoppingToken)
|
||||
{
|
||||
if (workerId > 1)
|
||||
{
|
||||
var startupDelay = Random.Shared.Next(100, 500);
|
||||
await Task.Delay(startupDelay, stoppingToken);
|
||||
}
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = await _queue.DequeueAsync(stoppingToken);
|
||||
|
||||
if (request != null)
|
||||
{
|
||||
await _regionService.ProcessRegionAsync(request.Id, stoppingToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Worker {WorkerId}: Error processing region request", workerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SatelliteProvider.Common.Configs;
|
||||
using SatelliteProvider.Common.Interfaces;
|
||||
|
||||
namespace SatelliteProvider.Services.RegionProcessing;
|
||||
|
||||
public static class RegionProcessingServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddRegionProcessing(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IRegionRequestQueue>(sp =>
|
||||
{
|
||||
var processingConfig = sp.GetRequiredService<IOptions<ProcessingConfig>>().Value;
|
||||
var logger = sp.GetRequiredService<ILogger<RegionRequestQueue>>();
|
||||
return new RegionRequestQueue(processingConfig.QueueCapacity, logger);
|
||||
});
|
||||
services.AddSingleton<IRegionService, RegionService>();
|
||||
services.AddHostedService<RegionProcessingService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Threading.Channels;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SatelliteProvider.Common.DTO;
|
||||
using SatelliteProvider.Common.Interfaces;
|
||||
|
||||
namespace SatelliteProvider.Services.RegionProcessing;
|
||||
|
||||
public class RegionRequestQueue : IRegionRequestQueue
|
||||
{
|
||||
private readonly Channel<RegionRequest> _queue;
|
||||
private readonly ILogger<RegionRequestQueue>? _logger;
|
||||
private int _totalEnqueued = 0;
|
||||
private int _totalDequeued = 0;
|
||||
|
||||
public RegionRequestQueue(int capacity, ILogger<RegionRequestQueue>? logger = null)
|
||||
{
|
||||
var options = new BoundedChannelOptions(capacity)
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.Wait
|
||||
};
|
||||
_queue = Channel.CreateBounded<RegionRequest>(options);
|
||||
_logger = logger;
|
||||
_logger?.LogInformation("RegionRequestQueue created with capacity {Capacity}", capacity);
|
||||
}
|
||||
|
||||
public async ValueTask EnqueueAsync(RegionRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_totalEnqueued++;
|
||||
await _queue.Writer.WriteAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
public async ValueTask<RegionRequest?> DequeueAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (await _queue.Reader.WaitToReadAsync(cancellationToken))
|
||||
{
|
||||
if (_queue.Reader.TryRead(out var request))
|
||||
{
|
||||
_totalDequeued++;
|
||||
return request;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int Count => _queue.Reader.Count;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SatelliteProvider.Common.Configs;
|
||||
using SatelliteProvider.Common.DTO;
|
||||
using SatelliteProvider.Common.Exceptions;
|
||||
using SatelliteProvider.Common.Interfaces;
|
||||
using SatelliteProvider.Common.Utils;
|
||||
using SatelliteProvider.DataAccess.Models;
|
||||
using SatelliteProvider.DataAccess.Repositories;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace SatelliteProvider.Services.RegionProcessing;
|
||||
|
||||
public class RegionService : IRegionService
|
||||
{
|
||||
private readonly IRegionRepository _regionRepository;
|
||||
private readonly IRegionRequestQueue _queue;
|
||||
private readonly ITileService _tileService;
|
||||
private readonly StorageConfig _storageConfig;
|
||||
private readonly ILogger<RegionService> _logger;
|
||||
|
||||
public RegionService(
|
||||
IRegionRepository regionRepository,
|
||||
IRegionRequestQueue queue,
|
||||
ITileService tileService,
|
||||
IOptions<StorageConfig> storageConfig,
|
||||
ILogger<RegionService> logger)
|
||||
{
|
||||
_regionRepository = regionRepository;
|
||||
_queue = queue;
|
||||
_tileService = tileService;
|
||||
_storageConfig = storageConfig.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RegionStatus> RequestRegionAsync(Guid id, double latitude, double longitude, double sizeMeters, int zoomLevel, bool stitchTiles = false)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var region = new RegionEntity
|
||||
{
|
||||
Id = id,
|
||||
Latitude = latitude,
|
||||
Longitude = longitude,
|
||||
SizeMeters = sizeMeters,
|
||||
ZoomLevel = zoomLevel,
|
||||
StitchTiles = stitchTiles,
|
||||
Status = "queued",
|
||||
TilesDownloaded = 0,
|
||||
TilesReused = 0,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
|
||||
await _regionRepository.InsertAsync(region);
|
||||
|
||||
var request = new RegionRequest
|
||||
{
|
||||
Id = id,
|
||||
Latitude = latitude,
|
||||
Longitude = longitude,
|
||||
SizeMeters = sizeMeters,
|
||||
ZoomLevel = zoomLevel,
|
||||
StitchTiles = stitchTiles
|
||||
};
|
||||
|
||||
await _queue.EnqueueAsync(request);
|
||||
|
||||
return MapToStatus(region);
|
||||
}
|
||||
|
||||
public async Task<RegionStatus?> GetRegionStatusAsync(Guid id)
|
||||
{
|
||||
var region = await _regionRepository.GetByIdAsync(id);
|
||||
return region != null ? MapToStatus(region) : null;
|
||||
}
|
||||
|
||||
public async Task ProcessRegionAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var startTime = DateTime.UtcNow;
|
||||
|
||||
var region = await _regionRepository.GetByIdAsync(id);
|
||||
if (region == null)
|
||||
{
|
||||
_logger.LogWarning("Region {RegionId} not found in database", id);
|
||||
return;
|
||||
}
|
||||
|
||||
region.Status = "processing";
|
||||
region.UpdatedAt = DateTime.UtcNow;
|
||||
await _regionRepository.UpdateAsync(region);
|
||||
|
||||
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||
|
||||
string? errorMessage = null;
|
||||
List<TileMetadata>? tiles = null;
|
||||
int tilesDownloaded = 0;
|
||||
int tilesReused = 0;
|
||||
|
||||
try
|
||||
{
|
||||
var processingStartTime = DateTime.UtcNow;
|
||||
|
||||
var tilesBeforeDownload = await _tileService.GetTilesByRegionAsync(
|
||||
region.Latitude,
|
||||
region.Longitude,
|
||||
region.SizeMeters,
|
||||
region.ZoomLevel);
|
||||
var existingTileIds = new HashSet<Guid>(tilesBeforeDownload.Select(t => t.Id));
|
||||
|
||||
tiles = await _tileService.DownloadAndStoreTilesAsync(
|
||||
region.Latitude,
|
||||
region.Longitude,
|
||||
region.SizeMeters,
|
||||
region.ZoomLevel,
|
||||
linkedCts.Token);
|
||||
|
||||
tilesDownloaded = tiles.Count(t => !existingTileIds.Contains(t.Id));
|
||||
tilesReused = tiles.Count(t => existingTileIds.Contains(t.Id));
|
||||
|
||||
var readyDir = _storageConfig.ReadyDirectory;
|
||||
Directory.CreateDirectory(readyDir);
|
||||
|
||||
var csvPath = Path.Combine(readyDir, $"region_{id}_ready.csv");
|
||||
var summaryPath = Path.Combine(readyDir, $"region_{id}_summary.txt");
|
||||
string? stitchedImagePath = null;
|
||||
|
||||
await GenerateCsvFileAsync(csvPath, tiles, linkedCts.Token);
|
||||
|
||||
if (region.StitchTiles)
|
||||
{
|
||||
stitchedImagePath = Path.Combine(readyDir, $"region_{id}_stitched.jpg");
|
||||
await StitchTilesAsync(tiles, region.Latitude, region.Longitude, region.ZoomLevel, stitchedImagePath, linkedCts.Token);
|
||||
}
|
||||
|
||||
await GenerateSummaryFileAsync(summaryPath, id, region, tiles, tilesDownloaded, tilesReused, stitchedImagePath, processingStartTime, linkedCts.Token, errorMessage);
|
||||
|
||||
region.Status = "completed";
|
||||
region.CsvFilePath = csvPath;
|
||||
region.SummaryFilePath = summaryPath;
|
||||
region.TilesDownloaded = tilesDownloaded;
|
||||
region.TilesReused = tilesReused;
|
||||
region.UpdatedAt = DateTime.UtcNow;
|
||||
await _regionRepository.UpdateAsync(region);
|
||||
}
|
||||
catch (TaskCanceledException ex) when (timeoutCts.IsCancellationRequested)
|
||||
{
|
||||
errorMessage = "Processing timed out after 5 minutes. Unable to download tiles within the time limit.";
|
||||
_logger.LogError(ex, "Region {RegionId} processing timed out after 5 minutes", id);
|
||||
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
|
||||
}
|
||||
catch (TaskCanceledException ex) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
errorMessage = "Processing was cancelled externally (likely application shutdown).";
|
||||
_logger.LogError(ex, "Region {RegionId} processing was cancelled externally", id);
|
||||
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
errorMessage = $"Request cancelled or timed out: {ex.Message}. This may indicate HttpClient timeout or network issues.";
|
||||
_logger.LogError(ex, "Region {RegionId} processing was cancelled (TaskCanceledException)", id);
|
||||
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
|
||||
}
|
||||
catch (OperationCanceledException ex) when (timeoutCts.IsCancellationRequested)
|
||||
{
|
||||
errorMessage = "Processing timed out after 5 minutes. Unable to download tiles within the time limit.";
|
||||
_logger.LogError(ex, "Region {RegionId} processing timed out after 5 minutes", id);
|
||||
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
errorMessage = $"Operation cancelled: {ex.Message}";
|
||||
_logger.LogError(ex, "Region {RegionId} processing was cancelled", id);
|
||||
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
|
||||
}
|
||||
catch (RateLimitException ex)
|
||||
{
|
||||
errorMessage = $"Rate limit exceeded: {ex.Message}. Google Maps API rate limit was reached and retries were exhausted.";
|
||||
_logger.LogError(ex, "Rate limit exceeded for region {RegionId}. Google is throttling requests. Consider reducing request rate.", id);
|
||||
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
errorMessage = $"Network error (HTTP {ex.StatusCode}): {ex.Message}. Failed to download tiles from Google Maps.";
|
||||
_logger.LogError(ex, "Network error processing region {RegionId}. StatusCode: {StatusCode}, Message: {Message}",
|
||||
id, ex.StatusCode, ex.Message);
|
||||
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Unexpected error ({ex.GetType().Name}): {ex.Message}";
|
||||
_logger.LogError(ex, "Failed to process region {RegionId}. Type: {ExceptionType}, Message: {Message}",
|
||||
id, ex.GetType().Name, ex.Message);
|
||||
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleProcessingFailureAsync(
|
||||
Guid id,
|
||||
RegionEntity region,
|
||||
DateTime startTime,
|
||||
List<TileMetadata>? tiles,
|
||||
int tilesDownloaded,
|
||||
int tilesReused,
|
||||
string errorMessage)
|
||||
{
|
||||
region.Status = "failed";
|
||||
region.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
try
|
||||
{
|
||||
var readyDir = _storageConfig.ReadyDirectory;
|
||||
Directory.CreateDirectory(readyDir);
|
||||
|
||||
var summaryPath = Path.Combine(readyDir, $"region_{id}_summary.txt");
|
||||
region.SummaryFilePath = summaryPath;
|
||||
|
||||
await GenerateSummaryFileAsync(
|
||||
summaryPath,
|
||||
id,
|
||||
region,
|
||||
tiles ?? new List<TileMetadata>(),
|
||||
tilesDownloaded,
|
||||
tilesReused,
|
||||
null,
|
||||
startTime,
|
||||
CancellationToken.None,
|
||||
errorMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to generate error summary for region {RegionId}", id);
|
||||
}
|
||||
|
||||
await _regionRepository.UpdateAsync(region);
|
||||
}
|
||||
|
||||
private async Task<string> StitchTilesAsync(
|
||||
List<TileMetadata> tiles,
|
||||
double centerLatitude,
|
||||
double centerLongitude,
|
||||
int zoomLevel,
|
||||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (tiles.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No tiles to stitch");
|
||||
}
|
||||
|
||||
var tileSizePixels = tiles.First().TileSizePixels;
|
||||
|
||||
var tileCoords = tiles.Select(t =>
|
||||
{
|
||||
var (x, y) = GeoUtils.WorldToTilePos(new GeoPoint(t.Latitude, t.Longitude), zoomLevel);
|
||||
return (x, y, t.FilePath);
|
||||
}).ToList();
|
||||
|
||||
var minX = tileCoords.Min(t => t.x);
|
||||
var maxX = tileCoords.Max(t => t.x);
|
||||
var minY = tileCoords.Min(t => t.y);
|
||||
var maxY = tileCoords.Max(t => t.y);
|
||||
|
||||
var gridWidth = maxX - minX + 1;
|
||||
var gridHeight = maxY - minY + 1;
|
||||
var imageWidth = gridWidth * tileSizePixels;
|
||||
var imageHeight = gridHeight * tileSizePixels;
|
||||
|
||||
using var stitchedImage = new Image<Rgb24>(imageWidth, imageHeight);
|
||||
|
||||
foreach (var (x, y, filePath) in tileCoords)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
_logger.LogWarning("Tile file not found: {FilePath}", filePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
using var tileImage = await Image.LoadAsync<Rgb24>(filePath, cancellationToken);
|
||||
|
||||
var destX = (x - minX) * tileSizePixels;
|
||||
var destY = (y - minY) * tileSizePixels;
|
||||
|
||||
stitchedImage.Mutate(ctx => ctx.DrawImage(tileImage, new Point(destX, destY), 1f));
|
||||
}
|
||||
|
||||
var (centerTileX, centerTileY) = GeoUtils.WorldToTilePos(new GeoPoint(centerLatitude, centerLongitude), zoomLevel);
|
||||
|
||||
var n = Math.Pow(2.0, zoomLevel);
|
||||
var centerTilePixelX = ((centerLongitude + 180.0) / 360.0 * n - centerTileX) * tileSizePixels;
|
||||
var centerTilePixelY = ((1.0 - Math.Log(Math.Tan(centerLatitude * Math.PI / 180.0) + 1.0 / Math.Cos(centerLatitude * Math.PI / 180.0)) / Math.PI) / 2.0 * n - centerTileY) * tileSizePixels;
|
||||
|
||||
var crossX = (int)Math.Round((centerTileX - minX) * tileSizePixels + centerTilePixelX);
|
||||
var crossY = (int)Math.Round((centerTileY - minY) * tileSizePixels + centerTilePixelY);
|
||||
|
||||
var red = new Rgb24(255, 0, 0);
|
||||
stitchedImage.Mutate(ctx =>
|
||||
{
|
||||
for (int i = -5; i < 5; i++)
|
||||
{
|
||||
var hx = crossX + i;
|
||||
var vy = crossY + i;
|
||||
|
||||
if (hx >= 0 && hx < imageWidth && crossY >= 0 && crossY < imageHeight)
|
||||
{
|
||||
stitchedImage[hx, crossY] = red;
|
||||
}
|
||||
|
||||
if (crossX >= 0 && crossX < imageWidth && vy >= 0 && vy < imageHeight)
|
||||
{
|
||||
stitchedImage[crossX, vy] = red;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await stitchedImage.SaveAsJpegAsync(outputPath, cancellationToken);
|
||||
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
private async Task GenerateCsvFileAsync(string filePath, List<TileMetadata> 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}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GenerateSummaryFileAsync(
|
||||
string filePath,
|
||||
Guid regionId,
|
||||
RegionEntity region,
|
||||
List<TileMetadata> tiles,
|
||||
int tilesDownloaded,
|
||||
int tilesReused,
|
||||
string? stitchedImagePath,
|
||||
DateTime startTime,
|
||||
CancellationToken cancellationToken,
|
||||
string? errorMessage = null)
|
||||
{
|
||||
var endTime = DateTime.UtcNow;
|
||||
var processingTime = (endTime - startTime).TotalSeconds;
|
||||
|
||||
var summary = new System.Text.StringBuilder();
|
||||
summary.AppendLine("Region Processing Summary");
|
||||
summary.AppendLine("========================");
|
||||
summary.AppendLine($"Region ID: {regionId}");
|
||||
summary.AppendLine($"Center: {region.Latitude:F6}, {region.Longitude:F6}");
|
||||
summary.AppendLine($"Size: {region.SizeMeters:F0} meters");
|
||||
summary.AppendLine($"Zoom Level: {region.ZoomLevel}");
|
||||
summary.AppendLine($"Status: {region.Status}");
|
||||
summary.AppendLine();
|
||||
|
||||
if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
summary.AppendLine("ERROR:");
|
||||
summary.AppendLine(errorMessage);
|
||||
summary.AppendLine();
|
||||
}
|
||||
|
||||
summary.AppendLine("Processing Statistics:");
|
||||
summary.AppendLine($"- Tiles Downloaded: {tilesDownloaded}");
|
||||
summary.AppendLine($"- Tiles Reused from Cache: {tilesReused}");
|
||||
summary.AppendLine($"- Total Tiles: {tiles.Count}");
|
||||
summary.AppendLine($"- Processing Time: {processingTime:F2} seconds");
|
||||
summary.AppendLine($"- Started: {startTime:yyyy-MM-dd HH:mm:ss} UTC");
|
||||
|
||||
if (region.Status == "completed")
|
||||
{
|
||||
summary.AppendLine($"- Completed: {endTime:yyyy-MM-dd HH:mm:ss} UTC");
|
||||
}
|
||||
else
|
||||
{
|
||||
summary.AppendLine($"- Failed: {endTime:yyyy-MM-dd HH:mm:ss} UTC");
|
||||
}
|
||||
|
||||
summary.AppendLine();
|
||||
summary.AppendLine("Files Created:");
|
||||
|
||||
if (tiles.Count > 0)
|
||||
{
|
||||
summary.AppendLine($"- CSV: {Path.GetFileName(filePath).Replace("_summary.txt", "_ready.csv")}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(stitchedImagePath))
|
||||
{
|
||||
summary.AppendLine($"- Stitched Image: {Path.GetFileName(stitchedImagePath)}");
|
||||
summary.AppendLine($"- Stitched Image Path: {stitchedImagePath}");
|
||||
}
|
||||
|
||||
summary.AppendLine($"- Summary: {Path.GetFileName(filePath)}");
|
||||
|
||||
await File.WriteAllTextAsync(filePath, summary.ToString(), cancellationToken);
|
||||
}
|
||||
|
||||
private static RegionStatus MapToStatus(RegionEntity region)
|
||||
{
|
||||
return new RegionStatus
|
||||
{
|
||||
Id = region.Id,
|
||||
Status = region.Status,
|
||||
CsvFilePath = region.CsvFilePath,
|
||||
SummaryFilePath = region.SummaryFilePath,
|
||||
TilesDownloaded = region.TilesDownloaded,
|
||||
TilesReused = region.TilesReused,
|
||||
CreatedAt = region.CreatedAt,
|
||||
UpdatedAt = region.UpdatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.10" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SatelliteProvider.Common\SatelliteProvider.Common.csproj" />
|
||||
<ProjectReference Include="..\SatelliteProvider.DataAccess\SatelliteProvider.DataAccess.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user