first region implementation

This commit is contained in:
Anton Martynenko
2025-10-28 15:56:16 +01:00
parent 12f3bf890a
commit bbb112940d
10 changed files with 576 additions and 0 deletions
@@ -0,0 +1,53 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SatelliteProvider.Common.Interfaces;
namespace SatelliteProvider.Services;
public class RegionProcessingService : BackgroundService
{
private readonly IRegionRequestQueue _queue;
private readonly IRegionService _regionService;
private readonly ILogger<RegionProcessingService> _logger;
public RegionProcessingService(
IRegionRequestQueue queue,
IRegionService regionService,
ILogger<RegionProcessingService> logger)
{
_queue = queue;
_regionService = regionService;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Region Processing Service started");
while (!stoppingToken.IsCancellationRequested)
{
try
{
var request = await _queue.DequeueAsync(stoppingToken);
if (request != null)
{
_logger.LogInformation("Dequeued region request {RegionId}", request.Id);
await _regionService.ProcessRegionAsync(request.Id, stoppingToken);
}
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing region request");
}
}
_logger.LogInformation("Region Processing Service stopped");
}
}
@@ -0,0 +1,39 @@
using System.Threading.Channels;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Common.Interfaces;
namespace SatelliteProvider.Services;
public class RegionRequestQueue : IRegionRequestQueue
{
private readonly Channel<RegionRequest> _queue;
public RegionRequestQueue(int capacity)
{
var options = new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.Wait
};
_queue = Channel.CreateBounded<RegionRequest>(options);
}
public async ValueTask EnqueueAsync(RegionRequest request, CancellationToken cancellationToken = default)
{
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))
{
return request;
}
}
return null;
}
public int Count => _queue.Reader.Count;
}
+213
View File
@@ -0,0 +1,213 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Common.Interfaces;
using SatelliteProvider.DataAccess.Models;
using SatelliteProvider.DataAccess.Repositories;
namespace SatelliteProvider.Services;
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)
{
var now = DateTime.UtcNow;
var region = new RegionEntity
{
Id = id,
Latitude = latitude,
Longitude = longitude,
SizeMeters = sizeMeters,
ZoomLevel = zoomLevel,
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
};
await _queue.EnqueueAsync(request);
_logger.LogInformation("Region {RegionId} queued for processing", id);
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)
{
_logger.LogInformation("Processing region {RegionId}", id);
var startTime = DateTime.UtcNow;
var region = await _regionRepository.GetByIdAsync(id);
if (region == null)
{
_logger.LogWarning("Region {RegionId} not found", id);
return;
}
region.Status = "processing";
region.UpdatedAt = DateTime.UtcNow;
await _regionRepository.UpdateAsync(region);
try
{
_logger.LogInformation("Downloading tiles for region {RegionId} at ({Lat}, {Lon}) size {Size}m zoom {Zoom}",
id, region.Latitude, region.Longitude, region.SizeMeters, region.ZoomLevel);
var processingStartTime = DateTime.UtcNow;
_logger.LogInformation("Checking for existing tiles in region {RegionId}", id);
var tilesBeforeDownload = await _tileService.GetTilesByRegionAsync(
region.Latitude,
region.Longitude,
region.SizeMeters,
region.ZoomLevel);
var existingTileIds = new HashSet<Guid>(tilesBeforeDownload.Select(t => t.Id));
_logger.LogInformation("Found {Count} existing tiles for region {RegionId}", existingTileIds.Count, id);
_logger.LogInformation("Starting tile download for region {RegionId}", id);
var tiles = await _tileService.DownloadAndStoreTilesAsync(
region.Latitude,
region.Longitude,
region.SizeMeters,
region.ZoomLevel,
cancellationToken);
var tilesDownloaded = tiles.Count(t => !existingTileIds.Contains(t.Id));
var tilesReused = tiles.Count(t => existingTileIds.Contains(t.Id));
_logger.LogInformation("Region {RegionId}: Downloaded {Downloaded} tiles, Reused {Reused} tiles",
id, tilesDownloaded, tilesReused);
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");
await GenerateCsvFileAsync(csvPath, tiles, cancellationToken);
await GenerateSummaryFileAsync(summaryPath, id, region, tiles, tilesDownloaded, tilesReused, processingStartTime, cancellationToken);
region.Status = "completed";
region.CsvFilePath = csvPath;
region.SummaryFilePath = summaryPath;
region.TilesDownloaded = tilesDownloaded;
region.TilesReused = tilesReused;
region.UpdatedAt = DateTime.UtcNow;
await _regionRepository.UpdateAsync(region);
var duration = (DateTime.UtcNow - startTime).TotalSeconds;
_logger.LogInformation("Region {RegionId} processing completed in {Duration:F2}s", id, duration);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process region {RegionId}: {Message}", id, ex.Message);
region.Status = "failed";
region.UpdatedAt = DateTime.UtcNow;
await _regionRepository.UpdateAsync(region);
}
}
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,
DateTime startTime,
CancellationToken cancellationToken)
{
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();
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");
summary.AppendLine($"- Completed: {endTime:yyyy-MM-dd HH:mm:ss} UTC");
summary.AppendLine();
summary.AppendLine("Files Created:");
summary.AppendLine($"- CSV: {Path.GetFileName(filePath).Replace("_summary.txt", "_ready.csv")}");
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
};
}
}
@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.10" />