mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 06:51:13 +00:00
[AZ-284] Autodev baseline + testability refactor
Phase A baseline outputs from /autodev (Steps 1-5): - Problem & solution docs (_docs/00_problem, _docs/01_solution) - Codebase documentation (_docs/02_document) incl. architecture, module-layout, glossary, system-flows, baseline compliance scan - Test specs (blackbox, performance, resilience, security, resource, traceability matrix) - Test task decomposition (_docs/02_tasks/todo): AZ-285..AZ-290 - Testability refactor (_docs/04_refactoring/01-testability-refactoring): - TC-01 Move DownloadedTileInfoV2 + new ExistingTileInfo to Common.DTO - TC-02 Replace dead ISatelliteDownloader API with real signatures - TC-03 GoogleMapsDownloaderV2 implements ISatelliteDownloader - TC-04 TileService depends on ISatelliteDownloader (mockable) - TC-05 DI + endpoints use ISatelliteDownloader - Test runner scripts (scripts/run-tests.sh, run-performance-tests.sh) - Autodev state pointer (_docs/_autodev_state.md) Prepares the codebase for AZ-285..AZ-290 unit/integration test work. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -31,7 +31,7 @@ builder.Services.AddSingleton<IRouteRepository>(sp => new RouteRepository(connec
|
||||
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddMemoryCache();
|
||||
builder.Services.AddSingleton<GoogleMapsDownloaderV2>();
|
||||
builder.Services.AddSingleton<ISatelliteDownloader, GoogleMapsDownloaderV2>();
|
||||
builder.Services.AddSingleton<ITileService, TileService>();
|
||||
|
||||
var allowedOrigins = builder.Configuration.GetSection("CorsConfig:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
|
||||
@@ -138,7 +138,7 @@ app.MapGet("/api/satellite/route/{id:guid}", GetRoute)
|
||||
|
||||
app.Run();
|
||||
|
||||
async Task<IResult> ServeTile(int z, int x, int y, HttpContext httpContext, ITileRepository tileRepository, GoogleMapsDownloaderV2 downloader, IMemoryCache cache, ILogger<Program> logger)
|
||||
async Task<IResult> ServeTile(int z, int x, int y, HttpContext httpContext, ITileRepository tileRepository, ISatelliteDownloader downloader, IMemoryCache cache, ILogger<Program> logger)
|
||||
{
|
||||
var cacheKey = $"tile_{z}_{x}_{y}";
|
||||
try
|
||||
@@ -203,7 +203,7 @@ async Task<IResult> ServeTile(int z, int x, int y, HttpContext httpContext, ITil
|
||||
}
|
||||
}
|
||||
|
||||
async Task<IResult> GetTileByLatLon([FromQuery] double Latitude, [FromQuery] double Longitude, [FromQuery] int ZoomLevel, GoogleMapsDownloaderV2 downloader, ITileRepository tileRepository, ILogger<Program> logger)
|
||||
async Task<IResult> GetTileByLatLon([FromQuery] double Latitude, [FromQuery] double Longitude, [FromQuery] int ZoomLevel, ISatelliteDownloader downloader, ITileRepository tileRepository, ILogger<Program> logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace SatelliteProvider.Common.DTO;
|
||||
|
||||
public record DownloadedTileInfoV2(
|
||||
int X, int Y, int ZoomLevel,
|
||||
double CenterLatitude, double CenterLongitude,
|
||||
string FilePath, double TileSizeMeters);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SatelliteProvider.Common.DTO;
|
||||
|
||||
public record ExistingTileInfo(double Latitude, double Longitude, int TileZoom);
|
||||
@@ -4,5 +4,12 @@ namespace SatelliteProvider.Common.Interfaces;
|
||||
|
||||
public interface ISatelliteDownloader
|
||||
{
|
||||
Task GetTiles(GeoPoint geoPoint, double radiusM, int zoomLevel, CancellationToken token = default);
|
||||
Task<DownloadedTileInfoV2> DownloadSingleTileAsync(
|
||||
double latitude, double longitude, int zoomLevel,
|
||||
CancellationToken token = default);
|
||||
|
||||
Task<List<DownloadedTileInfoV2>> GetTilesWithMetadataAsync(
|
||||
GeoPoint centerGeoPoint, double radiusM, int zoomLevel,
|
||||
IEnumerable<ExistingTileInfo> existingTiles,
|
||||
CancellationToken token = default);
|
||||
}
|
||||
@@ -5,18 +5,17 @@ using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using SatelliteProvider.Common.Configs;
|
||||
using SatelliteProvider.Common.DTO;
|
||||
using SatelliteProvider.Common.Interfaces;
|
||||
using SatelliteProvider.Common.Utils;
|
||||
|
||||
namespace SatelliteProvider.Services;
|
||||
|
||||
public record DownloadedTileInfoV2(int X, int Y, int ZoomLevel, double CenterLatitude, double CenterLongitude, string FilePath, double TileSizeMeters);
|
||||
|
||||
public class RateLimitException : Exception
|
||||
{
|
||||
public RateLimitException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
public class GoogleMapsDownloaderV2
|
||||
public class GoogleMapsDownloaderV2 : 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 string USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36";
|
||||
@@ -231,7 +230,7 @@ public class GoogleMapsDownloaderV2
|
||||
GeoPoint centerGeoPoint,
|
||||
double radiusM,
|
||||
int zoomLevel,
|
||||
IEnumerable<DataAccess.Models.TileEntity> existingTiles,
|
||||
IEnumerable<ExistingTileInfo> existingTiles,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (!ALLOWED_ZOOM_LEVELS.Contains(zoomLevel))
|
||||
|
||||
@@ -8,12 +8,12 @@ namespace SatelliteProvider.Services;
|
||||
|
||||
public class TileService : ITileService
|
||||
{
|
||||
private readonly GoogleMapsDownloaderV2 _downloader;
|
||||
private readonly ISatelliteDownloader _downloader;
|
||||
private readonly ITileRepository _tileRepository;
|
||||
private readonly ILogger<TileService> _logger;
|
||||
|
||||
public TileService(
|
||||
GoogleMapsDownloaderV2 downloader,
|
||||
ISatelliteDownloader downloader,
|
||||
ITileRepository tileRepository,
|
||||
ILogger<TileService> logger)
|
||||
{
|
||||
@@ -36,11 +36,15 @@ public class TileService : ITileService
|
||||
|
||||
var centerPoint = new GeoPoint(latitude, longitude);
|
||||
|
||||
var existingTileInfos = existingTilesList
|
||||
.Select(t => new ExistingTileInfo(t.Latitude, t.Longitude, t.TileZoom))
|
||||
.ToList();
|
||||
|
||||
var downloadedTiles = await _downloader.GetTilesWithMetadataAsync(
|
||||
centerPoint,
|
||||
sizeMeters / 2,
|
||||
zoomLevel,
|
||||
existingTilesList,
|
||||
existingTileInfos,
|
||||
cancellationToken);
|
||||
|
||||
var result = new List<TileMetadata>();
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Acceptance Criteria
|
||||
|
||||
## Tile Download
|
||||
|
||||
| # | Criterion | Measurable Value | Source |
|
||||
|---|----------|-----------------|--------|
|
||||
| T1 | Tiles are cached and not re-downloaded | 0 duplicate downloads for same (lat, lon, zoom, version) | Unique index idx_tiles_unique_location |
|
||||
| T2 | Concurrent download limit is enforced | Max 4 simultaneous HTTP requests to Google Maps | ProcessingConfig.MaxConcurrentDownloads |
|
||||
| T3 | Tile stored on disk with correct path | File exists at `./tiles/{zoom}/{x}/{y}.jpg` | TileService storage logic |
|
||||
| T4 | Tile metadata persisted in database | TileEntity row created with all fields populated | TileRepository.InsertAsync |
|
||||
|
||||
## Region Processing
|
||||
|
||||
| # | Criterion | Measurable Value | Source |
|
||||
|---|----------|-----------------|--------|
|
||||
| R1 | Region transitions through correct states | pending → processing → completed (or failed) | RegionProcessingService state updates |
|
||||
| R2 | CSV manifest generated on completion | File exists at `./ready/region_{id}_ready.csv` | RegionService.ProcessRegionAsync |
|
||||
| R3 | Summary file generated on completion | File exists at `./ready/region_{id}_summary.txt` | RegionService.GenerateSummaryFileAsync |
|
||||
| R4 | Stitched image generated when requested | File exists at `./ready/region_{id}_stitched.jpg` when stitch_tiles=true | RegionService.StitchTilesAsync |
|
||||
| R5 | Stitched image has valid content | File size > 1024 bytes | Integration test assertion |
|
||||
| R6 | Region processing is bounded | Max 20 concurrent regions | ProcessingConfig.MaxConcurrentRegions |
|
||||
|
||||
## Route Management
|
||||
|
||||
| # | Criterion | Measurable Value | Source |
|
||||
|---|----------|-----------------|--------|
|
||||
| RT1 | Points interpolated at correct interval | Intermediate points every ~200m along path | RouteService (InterpolatePoints) |
|
||||
| RT2 | Point types correctly assigned | "original" for input waypoints, "intermediate" for generated | RoutePointEntity.PointType |
|
||||
| RT3 | Total distance calculated | Haversine sum matches within acceptable precision | RouteService.CreateRoute |
|
||||
| RT4 | Geofence filtering applied | Only points inside geofence rectangles generate regions | RouteService (point-in-rectangle check) |
|
||||
| RT5 | ZIP archive within size limit | ≤ 50 MB | RouteProcessingService ZIP generation |
|
||||
| RT6 | Route map stitched when maps requested | Stitched image > 1024 bytes when request_maps=true | Integration test assertion |
|
||||
|
||||
## API Behavior
|
||||
|
||||
| # | Criterion | Measurable Value | Source |
|
||||
|---|----------|-----------------|--------|
|
||||
| A1 | Region request returns immediately | HTTP 200 with region_id (async processing) | POST /api/satellite/request |
|
||||
| A2 | Status endpoint reflects real state | Returns current status and file paths | GET /api/satellite/region/{id} |
|
||||
| A3 | Route creation returns computed metadata | Response includes total_points, total_distance_meters | POST /api/satellite/route |
|
||||
|
||||
## System Reliability
|
||||
|
||||
| # | Criterion | Measurable Value | Source |
|
||||
|---|----------|-----------------|--------|
|
||||
| S1 | Database migrations run on startup | All numbered scripts executed in order | DatabaseMigrator.Migrate() |
|
||||
| S2 | Queue rejects when full | Channel capacity = 1000, bounded wait | RegionRequestQueue (BoundedChannelOptions) |
|
||||
| S3 | Failed regions marked as failed | Status = "failed" on unrecoverable error | RegionProcessingService error handling |
|
||||
@@ -0,0 +1,91 @@
|
||||
# Data Parameters
|
||||
|
||||
## Input Data
|
||||
|
||||
### API Request: Single Tile Download
|
||||
|
||||
| Parameter | Type | Required | Constraints | Description |
|
||||
|-----------|------|----------|-------------|-------------|
|
||||
| latitude | double | yes | -90 to 90 | Center latitude |
|
||||
| longitude | double | yes | -180 to 180 | Center longitude |
|
||||
| zoomLevel | int | yes | 1–20 | Google Maps zoom level |
|
||||
|
||||
### API Request: Region
|
||||
|
||||
| Parameter | Type | Required | Constraints | Description |
|
||||
|-----------|------|----------|-------------|-------------|
|
||||
| latitude | double | yes | -90 to 90 | Region center latitude |
|
||||
| longitude | double | yes | -180 to 180 | Region center longitude |
|
||||
| sizeMeters | double | yes | > 0 | Square region side length in meters |
|
||||
| zoomLevel | int | yes | 1–20 | Tile zoom level |
|
||||
| stitchTiles | bool | no | default: false | Whether to produce composite image |
|
||||
|
||||
### API Request: Route Creation
|
||||
|
||||
| Parameter | Type | Required | Constraints | Description |
|
||||
|-----------|------|----------|-------------|-------------|
|
||||
| id | UUID | yes | — | Client-generated route ID |
|
||||
| name | string | yes | max 200 chars | Human-readable route name |
|
||||
| description | string | no | — | Optional description |
|
||||
| regionSizeMeters | double | yes | > 0 | Size of region per route point |
|
||||
| zoomLevel | int | yes | 1–20 | Tile zoom level |
|
||||
| points | array | yes | ≥ 2 waypoints | Ordered route waypoints |
|
||||
| points[].lat | double | yes | -90 to 90 | Waypoint latitude |
|
||||
| points[].lon | double | yes | -180 to 180 | Waypoint longitude |
|
||||
| geofences | object | no | — | Optional geofence definitions |
|
||||
| geofences.polygons[] | array | no | — | Rectangle boundaries |
|
||||
| geofences.polygons[].northWest | GeoPoint | yes (if polygon) | valid lat/lon, non-zero | NW corner |
|
||||
| geofences.polygons[].southEast | GeoPoint | yes (if polygon) | valid lat/lon, non-zero | SE corner |
|
||||
| requestMaps | bool | no | default: false | Whether to download map tiles for route |
|
||||
| createTilesZip | bool | no | default: false | Whether to produce ZIP archive |
|
||||
|
||||
## Output Data
|
||||
|
||||
### Tile File
|
||||
|
||||
- **Format**: JPEG
|
||||
- **Path**: `./tiles/{zoom}/{x}/{y}.jpg`
|
||||
- **Size**: ~50–100 KB per tile (typical at zoom 18)
|
||||
|
||||
### Region Outputs
|
||||
|
||||
| File | Format | Path Pattern | Content |
|
||||
|------|--------|-------------|---------|
|
||||
| CSV manifest | CSV | `./ready/region_{id}_ready.csv` | Tile coordinates and file paths |
|
||||
| Summary | TXT | `./ready/region_{id}_summary.txt` | Processing statistics |
|
||||
| Stitched image | JPEG | `./ready/region_{id}_stitched.jpg` | Composite tile image |
|
||||
|
||||
### Route Outputs
|
||||
|
||||
| File | Format | Path Pattern | Content |
|
||||
|------|--------|-------------|---------|
|
||||
| Stitched map | JPEG | `./ready/route_{id}_stitched.jpg` | Full route composite with markers |
|
||||
| Tiles ZIP | ZIP | `./ready/route_{id}_tiles.zip` | All tiles (max 50 MB) |
|
||||
| CSV | CSV | `./ready/route_{id}_ready.csv` | Tile manifest |
|
||||
| Summary | TXT | `./ready/route_{id}_summary.txt` | Route processing statistics |
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
### MapConfig (provider-specific; e.g., Google Maps — each provider has its own config section)
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ApiKey | string | — | Provider authentication token |
|
||||
| TileSizePixels | int | 256 | Tile image dimension |
|
||||
| MaxZoomLevel | int | 20 | Maximum allowed zoom |
|
||||
| DefaultZoomLevel | int | 18 | Default when not specified |
|
||||
|
||||
### StorageConfig
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| TilesDirectory | string | "./tiles" | Root tile storage path |
|
||||
| ReadyDirectory | string | "./ready" | Output artifacts path |
|
||||
|
||||
### ProcessingConfig
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| MaxConcurrentDownloads | int | 4 | Parallel Google Maps requests |
|
||||
| MaxConcurrentRegions | int | 20 | Parallel region processing |
|
||||
| QueueCapacity | int | 1000 | Max pending region requests |
|
||||
@@ -0,0 +1,57 @@
|
||||
# Expected Results Report
|
||||
|
||||
## Tile Expected Results
|
||||
|
||||
| Input ID | Expected Result | Tolerance | Pass/Fail Criterion |
|
||||
|----------|----------------|-----------|---------------------|
|
||||
| TILE-01 | Tile downloaded and stored | — | HTTP 200; response has: zoomLevel=18, tileSizePixels=256, imageType="jpg", filePath non-empty |
|
||||
| TILE-01 (reuse) | Same tile returned from cache | — | Second request returns same tile ID; no re-download |
|
||||
|
||||
## Region Expected Results
|
||||
|
||||
| Input ID | Expected Result | Tolerance | Pass/Fail Criterion |
|
||||
|----------|----------------|-----------|---------------------|
|
||||
| REG-01 | Region processes to completion | < 240s | status="completed"; csvFilePath non-empty; summaryFilePath non-empty |
|
||||
| REG-02 | Region processes to completion | < 240s | status="completed"; csvFilePath non-empty; summaryFilePath non-empty |
|
||||
| REG-03 | Region with stitching completes | < 240s | status="completed"; csvFilePath non-empty; summaryFilePath non-empty; stitched image file > 1024 bytes |
|
||||
| REG-01 (tile count) | Tiles downloaded > 0 | — | tilesDownloaded + tilesReused > 0 |
|
||||
|
||||
## Route Expected Results
|
||||
|
||||
| Input ID | Expected Result | Tolerance | Pass/Fail Criterion |
|
||||
|----------|----------------|-----------|---------------------|
|
||||
| ROUTE-01 | Route created with interpolated points | — | totalPoints > 2; all intermediate point spacing ≤ 200m; original point count = 2 (first + last) |
|
||||
| ROUTE-01 (retrieval) | Route retrievable by ID | — | GET /api/satellite/route/{id} returns same route with all points |
|
||||
| ROUTE-02 | Route maps processed | < 180s | mapsReady=true; stitchedImagePath non-empty; csvFilePath non-empty; stitched image > 1024 bytes |
|
||||
| ROUTE-03 | Route with ZIP created | < 180s | mapsReady=true; tilesZipPath non-empty; ZIP file > 1024 bytes; ZIP entry count = unique tile count from CSV; ZIP entries start with "tiles/" path prefix; ZIP has directory structure (≥5 path parts) |
|
||||
| ROUTE-04 | Complex route maps processed | < 240s | mapsReady=true; uniqueTileCount ≥ 10; stitched image > 1024 bytes |
|
||||
| ROUTE-05 | Route with geofences processed | < 240s | mapsReady=true; uniqueTileCount ≥ 10; stitched image > 1024 bytes; geofence regions created |
|
||||
| ROUTE-06 | Extended route maps processed | < 360s | mapsReady=true; uniqueTileCount ≥ 20; stitched image > 1024 bytes |
|
||||
|
||||
## Point Interpolation Expected Results
|
||||
|
||||
| Input | Expected | Tolerance | Pass/Fail Criterion |
|
||||
|-------|----------|-----------|---------------------|
|
||||
| ROUTE-01 (2 points, ~1.2km apart) | Points interpolated every ~200m | spacing ≤ 200m | Every consecutive point pair has distance ≤ 200m; point_type for first = "original", intermediates = "intermediate", last = "original" |
|
||||
| ROUTE-04 (10 points) | Original points: first + last = 2; intermediate points = 8 per pair | — | ValidatePointTypes(route, 1, 1, 8) — 1 first-original, 1 last-original, 8 intermediate |
|
||||
| ROUTE-06 (20 points) | Original points: first + last = 2; intermediate points = 18 | — | ValidatePointTypes(route, 1, 1, 18) |
|
||||
|
||||
## API Behavior Expected Results
|
||||
|
||||
| Scenario | Expected | Pass/Fail Criterion |
|
||||
|----------|----------|---------------------|
|
||||
| Region request (POST) | Immediate response with pending status | HTTP 200; response.status = "pending" or "processing" |
|
||||
| Region poll (GET) | Status transitions correctly | Eventually reaches "completed" or "failed" |
|
||||
| Invalid route (< 2 points) | Rejected | HTTP 400 or validation error |
|
||||
| Tile reuse | Same tile not re-downloaded | tilesReused > 0 when requesting overlapping regions |
|
||||
|
||||
## Timing Constraints
|
||||
|
||||
| Operation | Max Duration | Source |
|
||||
|-----------|-------------|--------|
|
||||
| Single tile download | 30s | HttpClient timeout |
|
||||
| Region processing (200m, zoom 18) | 240s | Integration test poll limit |
|
||||
| Route map processing (2 points) | 180s | Integration test poll limit |
|
||||
| Route map processing (10 points) | 240s | Integration test poll limit |
|
||||
| Route map processing (20 points) | 360s | Integration test poll limit |
|
||||
| API readiness on startup | 60s | WaitForApiReady (30 retries × 2s) |
|
||||
@@ -0,0 +1,95 @@
|
||||
# Test Coordinates and Input Data
|
||||
|
||||
## Geographic Test Region
|
||||
|
||||
All test data is centered around eastern Ukraine (Donetsk oblast area):
|
||||
- Primary tile/region test point: 47.461747°N, 37.647063°E
|
||||
- Primary route test area: ~48.27°N, 37.38°E
|
||||
|
||||
## Tile Inputs
|
||||
|
||||
| ID | Latitude | Longitude | Zoom Level |
|
||||
|----|----------|-----------|------------|
|
||||
| TILE-01 | 47.461747 | 37.647063 | 18 |
|
||||
|
||||
## Region Inputs
|
||||
|
||||
| ID | Latitude | Longitude | Size (m) | Zoom | Stitch |
|
||||
|----|----------|-----------|----------|------|--------|
|
||||
| REG-01 | 47.461747 | 37.647063 | 200 | 18 | false |
|
||||
| REG-02 | 47.461747 | 37.647063 | 400 | 17 | false |
|
||||
| REG-03 | 47.461747 | 37.647063 | 500 | 18 | true |
|
||||
|
||||
## Route Inputs
|
||||
|
||||
### ROUTE-01: Simple (2 points, no maps)
|
||||
|
||||
| Point | Latitude | Longitude |
|
||||
|-------|----------|-----------|
|
||||
| 1 (original) | 48.276067180586544 | 37.38445758819581 |
|
||||
| 2 (original) | 48.27074009522731 | 37.374029159545906 |
|
||||
|
||||
Config: regionSizeMeters=500, zoomLevel=18, requestMaps=false
|
||||
|
||||
### ROUTE-02: With Region Processing (2 points, maps)
|
||||
|
||||
Same points as ROUTE-01.
|
||||
Config: regionSizeMeters=300, zoomLevel=18, requestMaps=true
|
||||
|
||||
### ROUTE-03: With Tiles ZIP (2 points, maps + zip)
|
||||
|
||||
Same points as ROUTE-01.
|
||||
Config: regionSizeMeters=500, zoomLevel=18, requestMaps=true, createTilesZip=true
|
||||
|
||||
### ROUTE-04: Complex (10 points, maps)
|
||||
|
||||
| Point | Latitude | Longitude |
|
||||
|-------|----------|-----------|
|
||||
| 1 | 48.276067180586544 | 37.38445758819581 |
|
||||
| 2 | 48.27074009522731 | 37.374029159545906 |
|
||||
| 3 | 48.263312668696855 | 37.37707614898682 |
|
||||
| 4 | 48.26539817051818 | 37.36587524414063 |
|
||||
| 5 | 48.25851283439989 | 37.35952377319337 |
|
||||
| 6 | 48.254426906081555 | 37.374801635742195 |
|
||||
| 7 | 48.25914140977405 | 37.39068031311036 |
|
||||
| 8 | 48.25354110233028 | 37.401752471923835 |
|
||||
| 9 | 48.25902712391726 | 37.416257858276374 |
|
||||
| 10 | 48.26828345053738 | 37.402009963989265 |
|
||||
|
||||
Config: regionSizeMeters=300, zoomLevel=18, requestMaps=true
|
||||
|
||||
### ROUTE-05: Complex with Geofences (10 points + 2 geofences, maps)
|
||||
|
||||
Same 10 points as ROUTE-04.
|
||||
Geofences:
|
||||
- Polygon 1: NW(48.280, 37.370) → SE(48.265, 37.395)
|
||||
- Polygon 2: NW(48.265, 37.390) → SE(48.250, 37.420)
|
||||
|
||||
Config: regionSizeMeters=300, zoomLevel=18, requestMaps=true
|
||||
|
||||
### ROUTE-06: Extended (20 points, maps)
|
||||
|
||||
| Point | Latitude | Longitude |
|
||||
|-------|----------|-----------|
|
||||
| 1 | 48.276067180586544 | 37.51945758819581 |
|
||||
| 2 | 48.27074009522731 | 37.509029159545906 |
|
||||
| 3 | 48.263312668696855 | 37.51207614898682 |
|
||||
| 4 | 48.26539817051818 | 37.50087524414063 |
|
||||
| 5 | 48.25851283439989 | 37.49452377319337 |
|
||||
| 6 | 48.254426906081555 | 37.509801635742195 |
|
||||
| 7 | 48.25914140977405 | 37.52568031311036 |
|
||||
| 8 | 48.25354110233028 | 37.536752471923835 |
|
||||
| 9 | 48.25902712391726 | 37.551257858276374 |
|
||||
| 10 | 48.26828345053738 | 37.537009963989265 |
|
||||
| 11 | 48.27421563182974 | 37.52345758819581 |
|
||||
| 12 | 48.26889854647051 | 37.513029159545906 |
|
||||
| 13 | 48.26147111993905 | 37.51607614898682 |
|
||||
| 14 | 48.26355662176038 | 37.50487524414063 |
|
||||
| 15 | 48.25667128564209 | 37.49852377319337 |
|
||||
| 16 | 48.25258535732375 | 37.513801635742195 |
|
||||
| 17 | 48.25729986101625 | 37.52968031311036 |
|
||||
| 18 | 48.25169955357248 | 37.540752471923835 |
|
||||
| 19 | 48.25718557515946 | 37.555257858276374 |
|
||||
| 20 | 48.26644190177958 | 37.541009963989265 |
|
||||
|
||||
Config: regionSizeMeters=300, zoomLevel=18, requestMaps=true
|
||||
@@ -0,0 +1,36 @@
|
||||
# Problem Statement
|
||||
|
||||
## What is the system?
|
||||
|
||||
Satellite Provider is a backend service that supplies satellite imagery to a GPS-denied UAV navigation system. It pre-downloads and caches satellite tiles from external imagery providers (Layer 1) and will accept UAV-captured nadir camera imagery (Layer 2) uploaded post-flight. The downloader component is implementation-agnostic — the architecture supports multiple satellite imagery providers (e.g., Google Maps) via the `ISatelliteDownloader` interface.
|
||||
|
||||
## What problem does it solve?
|
||||
|
||||
UAVs operating without GPS rely on visual navigation by matching real-time camera imagery against pre-existing satellite maps. This requires:
|
||||
|
||||
1. **Pre-mission planning**: satellite imagery for the planned route must be downloaded and available before flight, regardless of which imagery provider is used
|
||||
2. **Post-mission ingestion**: ground truth imagery captured during flight must be stored for future reference and improved accuracy
|
||||
3. **Tile management**: imagery must be organized by geographic coordinates and zoom level, deduplicated, and served efficiently
|
||||
4. **Provider flexibility**: the system must not be locked to a single satellite imagery source — providers may change or multiply
|
||||
|
||||
Without this service, operators would need to manually download and organize satellite imagery — an error-prone process that doesn't scale to multiple routes or large coverage areas.
|
||||
|
||||
## Who are the users?
|
||||
|
||||
- **GPS-Denied Navigation Service** — the primary consumer, requesting tiles for route planning and accessing cached imagery for visual positioning
|
||||
- **Mission Planners** — define routes and regions to pre-download satellite coverage
|
||||
- **UAV Systems** (planned) — upload nadir camera tiles post-flight for Layer 2 enrichment
|
||||
|
||||
## How does it work at a high level?
|
||||
|
||||
1. A client defines a geographic area (region) or path (route) via the REST API
|
||||
2. The service calculates which satellite tiles are needed to cover that area
|
||||
3. Tiles are downloaded from the configured satellite imagery provider (abstracted via `ISatelliteDownloader` interface; e.g., Google Maps)
|
||||
4. Tiles are stored on disk and indexed in PostgreSQL
|
||||
5. Optionally, tiles are stitched into composite images and packaged as ZIP archives
|
||||
6. The client polls for completion and retrieves output artifacts
|
||||
|
||||
For routes, the service additionally:
|
||||
- Interpolates intermediate points every ~200m along the path
|
||||
- Applies geofence filters to limit tile downloads to areas of interest
|
||||
- Generates consolidated route maps with geofence overlays
|
||||
@@ -0,0 +1,40 @@
|
||||
# Restrictions
|
||||
|
||||
## Software Constraints
|
||||
|
||||
| Constraint | Value | Source |
|
||||
|-----------|-------|--------|
|
||||
| Runtime | .NET 8.0 (LTS) | global.json, Dockerfile |
|
||||
| Database | PostgreSQL 16 | docker-compose.yml |
|
||||
| Container runtime | Docker | Dockerfile, docker-compose.yml |
|
||||
| Image processing | SixLabors.ImageSharp 3.1.11 | SatelliteProvider.Services.csproj |
|
||||
| CI platform | Woodpecker CI | .woodpecker/*.yml |
|
||||
| Target architecture | ARM64 (primary), AMD64 (prepared) | .woodpecker/02-build-push.yml |
|
||||
|
||||
## Operational Constraints
|
||||
|
||||
| Constraint | Value | Source |
|
||||
|-----------|-------|--------|
|
||||
| Deployment model | Single instance (no horizontal scaling) | In-process Channel queue, no distributed state |
|
||||
| Max concurrent tile downloads | 4 (configurable) | ProcessingConfig |
|
||||
| Max concurrent region processing | 20 (configurable) | ProcessingConfig |
|
||||
| Queue capacity | 1000 requests | ProcessingConfig |
|
||||
| Max ZIP archive size | 50 MB | RouteProcessingService |
|
||||
| Tile versioning | Year-based integer (e.g., 2025) | Migration 004 |
|
||||
|
||||
## Environment Constraints
|
||||
|
||||
| Constraint | Value | Source |
|
||||
|-----------|-------|--------|
|
||||
| Storage | Local filesystem (./tiles, ./ready, ./logs) | docker-compose.yml volumes |
|
||||
| Network | Outbound HTTPS to Google Maps required | GoogleMapsDownloaderV2 |
|
||||
| Authentication | None (internal/trusted network only) | Program.cs (no auth middleware) |
|
||||
| Database persistence | Docker volume (postgres_data) | docker-compose.yml |
|
||||
|
||||
## Dependencies on External Services
|
||||
|
||||
| Service | Criticality | Failure Impact |
|
||||
|---------|-------------|----------------|
|
||||
| Satellite imagery provider (provider-agnostic via `ISatelliteDownloader`; e.g., Google Maps) | High | No new tiles can be downloaded; cached tiles still served |
|
||||
| PostgreSQL | Critical | Service cannot start; all state operations fail |
|
||||
| File system | Critical | Cannot store tiles or produce output artifacts |
|
||||
@@ -0,0 +1,72 @@
|
||||
# Satellite Provider — Solution
|
||||
|
||||
## Product Solution Description
|
||||
|
||||
Satellite Provider is a backend service that acquires, stores, and composites satellite imagery for a GPS-denied UAV navigation system. It operates as a tile cache and map-generation engine, bridging Google Maps satellite imagery (Layer 1) with UAV-captured nadir camera tiles (Layer 2, planned).
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Client[GPS-Denied Service] -->|HTTP| API[WebApi]
|
||||
API --> RS[RouteService]
|
||||
API --> RgS[RegionService]
|
||||
API --> TS[TileService]
|
||||
RS --> RgS
|
||||
RgS --> TS
|
||||
TS --> GM[Google Maps]
|
||||
TS --> FS[File System]
|
||||
RS --> DB[(PostgreSQL)]
|
||||
RgS --> DB
|
||||
TS --> DB
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The system implements a layered monolith with asynchronous background processing.
|
||||
|
||||
### Per-Component Solution
|
||||
|
||||
| Component | Solution | Tools | Advantages | Limitations | Requirements Met | Security | Cost | Fit |
|
||||
|-----------|----------|-------|-----------|-------------|-----------------|----------|------|-----|
|
||||
| Common | Shared contracts + geo-math library | C# records, static utility class | Type-safe contracts, reusable Haversine/Mercator math | Static utility class limits testability of geo functions | Cross-component type sharing, coordinate calculations | N/A | Zero runtime cost | High |
|
||||
| DataAccess | Dapper + DbUp repositories | Dapper, Npgsql, DbUp, PostgreSQL 16 | Raw SQL performance, simple migration model | No change tracking, manual mapping | Tile metadata persistence, region/route state tracking | Parameterized queries (SQL injection safe) | Minimal overhead | High |
|
||||
| TileDownloader | Provider-agnostic concurrent downloader with dedup cache via `ISatelliteDownloader` (first implementation: Google Maps) | HttpClient, SemaphoreSlim, ConcurrentDictionary | Prevents duplicate downloads, controlled concurrency, provider-swappable | Single-instance only, no distributed dedup | Tile acquisition from satellite imagery providers, disk caching | Provider-specific auth (e.g., session token) | Per-tile provider API cost | High |
|
||||
| RegionProcessing | Queue-based async processor with tile stitching | System.Threading.Channels, ImageSharp | Decoupled request/processing, bounded memory | Queue lost on restart, no retry persistence | Batch tile download for regions, composite image output | N/A | CPU-bound stitching | High |
|
||||
| RouteManagement | Point interpolation + geofenced region generation | Haversine math, point-in-rectangle test | Automated route coverage, geofence filtering | Rectangular geofences only (not arbitrary polygons) | Route-to-region expansion, selective tile coverage | N/A | Linear in point count | Medium-High |
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- **Framework**: Custom console application (`SatelliteProvider.IntegrationTests`)
|
||||
- **Execution**: Docker Compose with dependent services (API + PostgreSQL)
|
||||
- **Coverage areas**:
|
||||
- Single tile download (lat/lon + zoom → file stored)
|
||||
- Region request lifecycle (pending → processing → completed)
|
||||
- Route creation with point interpolation
|
||||
- Complex routes with geofences and stitching
|
||||
- Extended routes with map request processing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- **Framework**: xUnit + Moq (`SatelliteProvider.Tests`)
|
||||
- **Current state**: Minimal (placeholder test exists)
|
||||
- **Gap**: No unit test coverage for business logic (services, geo calculations)
|
||||
|
||||
### Non-Functional Tests
|
||||
|
||||
- No dedicated performance/load tests
|
||||
- Integration tests implicitly verify end-to-end latency
|
||||
- File size assertions (stitched image > 1KB) serve as basic output validation
|
||||
|
||||
## References
|
||||
|
||||
| Artifact | Path | Purpose |
|
||||
|----------|------|---------|
|
||||
| Dockerfile | `SatelliteProvider.Api/Dockerfile` | Multi-stage .NET 8.0 container build |
|
||||
| Docker Compose | `docker-compose.yml` | Service orchestration (API + PostgreSQL) |
|
||||
| Docker Compose Tests | `docker-compose.tests.yml` | Integration test execution environment |
|
||||
| CI - Unit Tests | `.woodpecker/01-test.yml` | Automated test gate on push/PR |
|
||||
| CI - Build & Push | `.woodpecker/02-build-push.yml` | Container image build and registry push |
|
||||
| App Config | `SatelliteProvider.Api/appsettings.json` | Default configuration values |
|
||||
| Dev Config | `SatelliteProvider.Api/appsettings.Development.json` | Development overrides |
|
||||
| Migrations | `SatelliteProvider.DataAccess/Migrations/*.sql` | Database schema (11 sequential scripts) |
|
||||
@@ -0,0 +1,208 @@
|
||||
# Codebase Discovery
|
||||
|
||||
## Directory Tree
|
||||
|
||||
```
|
||||
satellite-provider/
|
||||
├── SatelliteProvider.sln
|
||||
├── global.json
|
||||
├── docker-compose.yml
|
||||
├── docker-compose.tests.yml
|
||||
├── goal.md
|
||||
├── README.md
|
||||
├── AGENTS.md
|
||||
├── .woodpecker/
|
||||
│ ├── 01-test.yml
|
||||
│ └── 02-build-push.yml
|
||||
├── SatelliteProvider.Api/
|
||||
│ ├── Dockerfile
|
||||
│ ├── Program.cs
|
||||
│ ├── SatelliteProvider.Api.csproj
|
||||
│ ├── Properties/launchSettings.json
|
||||
│ ├── appsettings.json
|
||||
│ └── appsettings.Development.json
|
||||
├── SatelliteProvider.Common/
|
||||
│ ├── SatelliteProvider.Common.csproj
|
||||
│ ├── Configs/
|
||||
│ │ ├── DatabaseConfig.cs
|
||||
│ │ ├── MapConfig.cs
|
||||
│ │ ├── ProcessingConfig.cs
|
||||
│ │ └── StorageConfig.cs
|
||||
│ ├── DTO/
|
||||
│ │ ├── CreateRouteRequest.cs
|
||||
│ │ ├── Direction.cs
|
||||
│ │ ├── GeoPoint.cs
|
||||
│ │ ├── GeofencePolygon.cs
|
||||
│ │ ├── RegionRequest.cs
|
||||
│ │ ├── RegionStatus.cs
|
||||
│ │ ├── RoutePoint.cs
|
||||
│ │ ├── RoutePointDto.cs
|
||||
│ │ ├── RouteResponse.cs
|
||||
│ │ ├── SatTile.cs
|
||||
│ │ └── TileMetadata.cs
|
||||
│ ├── Interfaces/
|
||||
│ │ ├── IRegionRequestQueue.cs
|
||||
│ │ ├── IRegionService.cs
|
||||
│ │ ├── IRouteService.cs
|
||||
│ │ ├── ISatelliteDownloader.cs
|
||||
│ │ └── ITileService.cs
|
||||
│ └── Utils/
|
||||
│ └── GeoUtils.cs
|
||||
├── SatelliteProvider.DataAccess/
|
||||
│ ├── SatelliteProvider.DataAccess.csproj
|
||||
│ ├── DatabaseMigrator.cs
|
||||
│ ├── Migrations/
|
||||
│ │ ├── 001_CreateTilesTable.sql
|
||||
│ │ ├── 002_CreateRegionsTable.sql
|
||||
│ │ ├── 003_CreateIndexes.sql
|
||||
│ │ ├── 004_AddVersionColumn.sql
|
||||
│ │ ├── 005_CreateRoutesTables.sql
|
||||
│ │ ├── 006_AddStitchTilesToRegions.sql
|
||||
│ │ ├── 007_AddRouteMapFields.sql
|
||||
│ │ ├── 008_AddGeofenceFlagToRouteRegions.sql
|
||||
│ │ ├── 009_AddGeofencePolygonIndex.sql
|
||||
│ │ ├── 010_AddTilesZipToRoutes.sql
|
||||
│ │ └── 011_AddTileCoordinates.sql
|
||||
│ ├── Models/
|
||||
│ │ ├── RegionEntity.cs
|
||||
│ │ ├── RouteEntity.cs
|
||||
│ │ ├── RoutePointEntity.cs
|
||||
│ │ └── TileEntity.cs
|
||||
│ └── Repositories/
|
||||
│ ├── IRegionRepository.cs
|
||||
│ ├── IRouteRepository.cs
|
||||
│ ├── ITileRepository.cs
|
||||
│ ├── RegionRepository.cs
|
||||
│ ├── RouteRepository.cs
|
||||
│ └── TileRepository.cs
|
||||
├── SatelliteProvider.Services/
|
||||
│ ├── SatelliteProvider.Services.csproj
|
||||
│ ├── GoogleMapsDownloaderV2.cs
|
||||
│ ├── RegionProcessingService.cs
|
||||
│ ├── RegionRequestQueue.cs
|
||||
│ ├── RegionService.cs
|
||||
│ ├── RouteProcessingService.cs
|
||||
│ ├── RouteService.cs
|
||||
│ └── TileService.cs
|
||||
├── SatelliteProvider.Tests/
|
||||
│ ├── SatelliteProvider.Tests.csproj
|
||||
│ ├── GoogleMapsDownloaderTests.cs
|
||||
│ └── appsettings.json
|
||||
└── SatelliteProvider.IntegrationTests/
|
||||
├── SatelliteProvider.IntegrationTests.csproj
|
||||
├── Dockerfile
|
||||
├── Program.cs
|
||||
├── Models.cs
|
||||
├── BasicRouteTests.cs
|
||||
├── ComplexRouteTests.cs
|
||||
├── ExtendedRouteTests.cs
|
||||
├── RegionTests.cs
|
||||
├── TileTests.cs
|
||||
└── RouteTestHelpers.cs
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Category | Technology | Version |
|
||||
|----------|-----------|---------|
|
||||
| Language | C# | 12 (.NET 8.0) |
|
||||
| Framework | ASP.NET Core (Minimal API) | 8.0 |
|
||||
| Database | PostgreSQL | 16 (Docker image) |
|
||||
| ORM/Data Access | Dapper | 2.1.35 |
|
||||
| DB Migrations | DbUp (PostgreSQL) | 6.0.3 |
|
||||
| Logging | Serilog (Console + File) | 8.0.3 |
|
||||
| Image Processing | SixLabors.ImageSharp | 3.1.11 |
|
||||
| JSON Serialization | Newtonsoft.Json + System.Text.Json | 13.0.4 |
|
||||
| API Docs | Swagger / Swashbuckle | 6.6.2 |
|
||||
| HTTP Client | IHttpClientFactory | built-in |
|
||||
| Containerization | Docker (multi-stage) | - |
|
||||
| Orchestration | Docker Compose | - |
|
||||
| CI/CD | Woodpecker CI | - |
|
||||
| Unit Testing | xUnit + Moq + FluentAssertions | 2.5.3 / 4.20.72 / 8.8.0 |
|
||||
| Integration Testing | Console app (custom harness) | - |
|
||||
| SDK | .NET 8.0 (latestMinor rollForward) | 8.0.0+ |
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
### Project References
|
||||
|
||||
```
|
||||
SatelliteProvider.Common (leaf — no project references)
|
||||
SatelliteProvider.DataAccess (leaf — no project references; NuGet: Dapper, Npgsql, DbUp)
|
||||
SatelliteProvider.Services → Common, DataAccess
|
||||
SatelliteProvider.Api → Common, DataAccess, Services
|
||||
SatelliteProvider.Tests → Services, Common
|
||||
SatelliteProvider.IntegrationTests (standalone console app, no project references)
|
||||
```
|
||||
|
||||
### Mermaid Dependency Diagram
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Api[SatelliteProvider.Api] --> Services[SatelliteProvider.Services]
|
||||
Api --> DataAccess[SatelliteProvider.DataAccess]
|
||||
Api --> Common[SatelliteProvider.Common]
|
||||
Services --> DataAccess
|
||||
Services --> Common
|
||||
Tests[SatelliteProvider.Tests] --> Services
|
||||
Tests --> Common
|
||||
IntTests[SatelliteProvider.IntegrationTests] -.->|HTTP calls| Api
|
||||
```
|
||||
|
||||
## Topological Processing Order
|
||||
|
||||
Leaf modules first, then dependent modules:
|
||||
|
||||
1. **SatelliteProvider.Common** — DTOs, interfaces, configs, geo utilities (no internal dependencies)
|
||||
2. **SatelliteProvider.DataAccess** — entities, repositories, migrations (no project dependencies)
|
||||
3. **SatelliteProvider.Services** — business logic (depends on Common + DataAccess)
|
||||
4. **SatelliteProvider.Api** — web layer, DI, endpoints (depends on all above)
|
||||
5. **SatelliteProvider.Tests** — unit tests (depends on Services + Common)
|
||||
6. **SatelliteProvider.IntegrationTests** — integration tests via HTTP (standalone)
|
||||
|
||||
## Entry Points
|
||||
|
||||
- **Application entry**: `SatelliteProvider.Api/Program.cs` — minimal API startup, DI registration, DB migration, endpoint mapping
|
||||
- **Background services**: `RegionProcessingService` (queue consumer), `RouteProcessingService` (polling loop)
|
||||
- **Integration test entry**: `SatelliteProvider.IntegrationTests/Program.cs`
|
||||
|
||||
## Leaf Modules
|
||||
|
||||
- `SatelliteProvider.Common/Configs/*` — configuration POCOs
|
||||
- `SatelliteProvider.Common/DTO/*` — data transfer objects
|
||||
- `SatelliteProvider.Common/Interfaces/*` — service contracts
|
||||
- `SatelliteProvider.Common/Utils/GeoUtils.cs` — static geo math utilities
|
||||
- `SatelliteProvider.DataAccess/Models/*` — database entity classes
|
||||
- `SatelliteProvider.DataAccess/Migrations/*` — SQL migration scripts
|
||||
|
||||
## Cycles
|
||||
|
||||
No dependency cycles detected. The dependency graph is a clean DAG.
|
||||
|
||||
## External Integrations
|
||||
|
||||
| Integration | Module | Protocol |
|
||||
|-------------|--------|----------|
|
||||
| Google Maps Tile API | GoogleMapsDownloaderV2 | HTTPS (tile.googleapis.com, mt*.google.com) |
|
||||
| PostgreSQL | All repositories | TCP (Npgsql, port 5432) |
|
||||
| File system (tiles) | StorageConfig, TileService, GoogleMapsDownloaderV2 | Local FS (./tiles/) |
|
||||
| File system (output) | RegionService, RouteProcessingService | Local FS (./ready/) |
|
||||
| File system (logs) | Serilog | Local FS (./logs/) |
|
||||
|
||||
## Existing Documentation
|
||||
|
||||
- `README.md` — comprehensive API docs, architecture overview, configuration guide
|
||||
- `AGENTS.md` — agent-oriented documentation with architecture details and conventions
|
||||
- `goal.md` — original requirements and TODO items
|
||||
- Swagger/OpenAPI — auto-generated at runtime (`/swagger`)
|
||||
|
||||
## Test Structure
|
||||
|
||||
- **Unit tests**: `SatelliteProvider.Tests/` — xUnit, currently contains only a dummy test (`DummyTests.Dummy_ShouldWork`)
|
||||
- **Integration tests**: `SatelliteProvider.IntegrationTests/` — console app that runs against a live API+DB instance in Docker. Tests cover tile downloads, region requests, route creation with intermediate points, geofencing, extended routes with map requests.
|
||||
|
||||
## CI/CD
|
||||
|
||||
- **Woodpecker CI** pipelines in `.woodpecker/`:
|
||||
- `01-test.yml`: runs `dotnet restore` + `dotnet test` on push/PR to dev/stage/main (ARM64)
|
||||
- `02-build-push.yml`: builds Docker image and pushes to private registry (depends on 01-test, ARM64 matrix with AMD64 slot commented out)
|
||||
@@ -0,0 +1,60 @@
|
||||
# Verification Log
|
||||
|
||||
## Summary
|
||||
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Code entities verified | 48 |
|
||||
| Entities flagged (incorrect) | 1 |
|
||||
| Corrections applied | 1 |
|
||||
| Remaining gaps | 1 (minor) |
|
||||
| Completeness | 16/16 modules documented |
|
||||
|
||||
## Corrections Applied
|
||||
|
||||
### 1. data_model.md — Removed `stitched_image_path` from regions table
|
||||
|
||||
**Issue**: Listed `stitched_image_path` as a column on the `regions` table.
|
||||
**Reality**: `RegionEntity` has no such property. Stitched images for regions are generated to disk but the path is only written to the summary text file, not stored as a DB column. `StitchedImagePath` only exists on `RouteEntity`.
|
||||
**Fix**: Removed from ERD and table definition in `data_model.md`.
|
||||
|
||||
## Entity Verification
|
||||
|
||||
All classes, interfaces, and types mentioned in documentation were cross-referenced against the codebase:
|
||||
|
||||
- **Entities** (4/4): TileEntity, RegionEntity, RouteEntity, RoutePointEntity ✓
|
||||
- **Service interfaces** (5/5): ITileService, IRegionService, IRouteService, IRegionRequestQueue, ISatelliteDownloader ✓
|
||||
- **Service implementations** (7/7): TileService, RegionService, RouteService, GoogleMapsDownloaderV2, RegionProcessingService, RouteProcessingService, RegionRequestQueue ✓
|
||||
- **Repositories** (6/6): ITileRepository, IRegionRepository, IRouteRepository, TileRepository, RegionRepository, RouteRepository ✓
|
||||
- **Config classes** (4/4): MapConfig, StorageConfig, ProcessingConfig, DatabaseConfig ✓
|
||||
- **DTOs** (10/10): GeoPoint, Direction, TileMetadata, RegionRequest, RegionStatus, RouteResponse, RoutePoint, RoutePointDto, CreateRouteRequest, GeofencePolygon ✓
|
||||
- **Utilities** (1/1): GeoUtils ✓
|
||||
- **Infrastructure** (1/1): DatabaseMigrator ✓
|
||||
|
||||
## Interface Accuracy
|
||||
|
||||
All method signatures in component/module docs verified against actual code. No discrepancies found.
|
||||
|
||||
## Flow Correctness
|
||||
|
||||
- F1 (Single Tile): TileService → TileRepo → GoogleMaps → FileSystem ✓
|
||||
- F2 (Region Request): RegionService → RegionRepo → Queue ✓
|
||||
- F3 (Region Processing): BackgroundService → TileService → FileSystem → RegionRepo ✓
|
||||
- F4 (Route Creation): RouteService → GeoUtils → RouteRepo ✓
|
||||
- F5 (Route Map Processing): RouteProcessingService → RegionService → Queue → ZIP ✓
|
||||
- F6 (Status Query): Direct DB lookup ✓
|
||||
|
||||
## Remaining Gaps (Minor)
|
||||
|
||||
1. **Tile serving endpoint**: `GET /tiles/{z}/{x}/{y}` serves raw tile images from disk. Not documented in system-flows as it's a trivial static file serve. Noted in architecture as implicit.
|
||||
|
||||
## Consistency Check
|
||||
|
||||
- Component docs ↔ Architecture doc: consistent ✓
|
||||
- Flow diagrams ↔ Component interfaces: consistent ✓
|
||||
- Data model ↔ Migration SQL: consistent (after correction) ✓
|
||||
- Module layout ↔ Actual file paths: consistent ✓
|
||||
|
||||
## Note on AGENTS.md Discrepancy
|
||||
|
||||
The project's `AGENTS.md` mentions `geofence_polygons` as a field on the `routes` table. This is inaccurate — geofence polygons are passed in `CreateRouteRequest` but are NOT persisted on the routes table. Their effects are stored indirectly via `route_regions.is_geofence` and `route_regions.geofence_polygon_index`. The generated documentation correctly omits this non-existent column.
|
||||
@@ -0,0 +1,109 @@
|
||||
# Satellite Provider — Documentation Report
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Full bottom-up documentation of the Satellite Provider service — a .NET 8.0 backend that pre-downloads, caches, and composites satellite imagery for a GPS-denied UAV navigation system. The analysis identified 5 logical components across 16 modules, documented 6 system flows, and produced a complete data model, deployment guide, and architecture reference.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
UAVs operating without GPS need pre-cached satellite imagery for visual positioning. This service automates tile acquisition from satellite imagery providers (first implementation: Google Maps), organizes tiles by coordinates/zoom, generates composite maps for routes and regions, and will accept UAV-captured imagery (Layer 2) for improved accuracy. The downloader is provider-agnostic via `ISatelliteDownloader`.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Single-instance containerized monolith with layered architecture (API → Services → DataAccess → PostgreSQL) and asynchronous background processing via in-process queues. No authentication (internal/trusted network service).
|
||||
|
||||
**Technology stack**: C# 12 / .NET 8.0, ASP.NET Core Minimal API, PostgreSQL 16, Dapper, Docker, Woodpecker CI
|
||||
|
||||
**Deployment**: Docker Compose (API + PostgreSQL), ARM64 primary, self-hosted registry
|
||||
|
||||
## Component Summary
|
||||
|
||||
| # | Component | Purpose | Dependencies |
|
||||
|---|-----------|---------|-------------|
|
||||
| 01 | Common | Shared DTOs, interfaces, configs, geospatial math | — |
|
||||
| 02 | DataAccess | PostgreSQL persistence via Dapper + DbUp migrations | Common |
|
||||
| 03 | TileDownloader | Provider-agnostic satellite tile acquisition with dedup | Common, DataAccess |
|
||||
| 04 | RegionProcessing | Batch tile downloads, stitching, CSV/summary output | Common, DataAccess, TileDownloader |
|
||||
| 05 | RouteManagement | Route interpolation, geofencing, consolidated map output | Common, DataAccess, RegionProcessing |
|
||||
| — | WebApi | HTTP endpoints, DI configuration, startup | All above |
|
||||
|
||||
**Dependency layering** (bottom-up):
|
||||
1. Common (foundation)
|
||||
2. DataAccess (persistence)
|
||||
3. TileDownloader (domain services)
|
||||
4. RegionProcessing, RouteManagement (application/orchestration)
|
||||
5. WebApi (entry point)
|
||||
|
||||
## System Flows
|
||||
|
||||
| Flow | Description | Key Components |
|
||||
|------|-------------|---------------|
|
||||
| Single Tile Download | Client requests tile by lat/lon/zoom; cache check → download → store | WebApi, TileDownloader, DataAccess |
|
||||
| Region Request | Submit region definition; queued for async processing | WebApi, RegionProcessing |
|
||||
| Region Processing | Background: calculate grid → download tiles → stitch → output files | RegionProcessing, TileDownloader |
|
||||
| Route Creation | Submit waypoints; interpolate points, persist | WebApi, RouteManagement |
|
||||
| Route Map Processing | Background: geofence filter → create regions → wait → ZIP | RouteManagement, RegionProcessing |
|
||||
| Status Query | Poll region/route by ID | WebApi, DataAccess |
|
||||
|
||||
## Risk Summary
|
||||
|
||||
| Level | Count | Key Risks |
|
||||
|-------|-------|-----------|
|
||||
| High | 1 | Queue state lost on restart (in-process Channel, no persistence) |
|
||||
| Medium | 2 | Single-instance limitation; no retry persistence for failed tiles |
|
||||
| Low | 2 | No auth layer; MGRS/Upload endpoints are stubs |
|
||||
|
||||
## Test Coverage
|
||||
|
||||
| Component | Unit Tests | Integration Tests |
|
||||
|-----------|-----------|------------------|
|
||||
| Common | None | Indirect |
|
||||
| DataAccess | None | Indirect (via integration) |
|
||||
| TileDownloader | Placeholder only | Tile download tests |
|
||||
| RegionProcessing | None | Region processing tests (multiple sizes/zooms) |
|
||||
| RouteManagement | None | Basic, complex, extended route tests |
|
||||
|
||||
**Gap**: Unit test coverage is minimal (placeholder only). Integration tests provide the primary verification via Docker Compose.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
| # | Decision | Rationale | Alternatives Rejected |
|
||||
|---|----------|-----------|----------------------|
|
||||
| 1 | Minimal API (no controllers) | Small endpoint surface, less ceremony | MVC controllers |
|
||||
| 2 | Dapper over EF Core | Raw SQL control, performance, simplicity | Entity Framework (too heavy for this use case) |
|
||||
| 3 | In-process Channel queue | No external dependencies, single instance | RabbitMQ, Redis queues |
|
||||
| 4 | File-based tile storage | Fast reads, simple backup, immutable files | Blob storage, DB binary |
|
||||
| 5 | Background hosted services | Clean lifecycle, framework-managed | Separate worker process |
|
||||
| 6 | Provider-agnostic downloader interface | Future provider flexibility | Hardcoded Google Maps calls |
|
||||
|
||||
## Open Questions
|
||||
|
||||
| # | Question | Impact | Owner |
|
||||
|---|----------|--------|-------|
|
||||
| 1 | Which additional satellite imagery providers will be integrated? | New `ISatelliteDownloader` implementations needed | Product |
|
||||
| 2 | Layer 2 upload: what orthogonal tile format/metadata will UAVs provide? | Upload endpoint design | Product |
|
||||
| 3 | MGRS endpoint: what coordinate conversion library to use? | Implementation of tile-by-MGRS | Engineering |
|
||||
| 4 | Should queue state survive restarts (persistent queue)? | Data loss risk on crash during processing | Engineering |
|
||||
|
||||
## Artifact Index
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `_docs/00_problem/problem.md` | Problem statement |
|
||||
| `_docs/00_problem/restrictions.md` | Constraints and dependencies |
|
||||
| `_docs/00_problem/acceptance_criteria.md` | Measurable acceptance criteria |
|
||||
| `_docs/00_problem/data_parameters.md` | Input/output data schemas |
|
||||
| `_docs/01_solution/solution.md` | Solution overview with per-component analysis |
|
||||
| `_docs/02_document/00_discovery.md` | Codebase discovery (structure, deps, tech stack) |
|
||||
| `_docs/02_document/architecture.md` | Full architecture document |
|
||||
| `_docs/02_document/system-flows.md` | System flows with sequence diagrams |
|
||||
| `_docs/02_document/data_model.md` | Database schema and migration history |
|
||||
| `_docs/02_document/glossary.md` | Domain and technical glossary |
|
||||
| `_docs/02_document/module-layout.md` | File ownership and layering map |
|
||||
| `_docs/02_document/04_verification_log.md` | Verification results |
|
||||
| `_docs/02_document/modules/*.md` | Per-module documentation (16 files) |
|
||||
| `_docs/02_document/components/*/description.md` | Per-component specs (5 files) |
|
||||
| `_docs/02_document/diagrams/components.md` | Component relationship diagram |
|
||||
| `_docs/02_document/deployment/containerization.md` | Docker setup |
|
||||
| `_docs/02_document/deployment/ci_cd_pipeline.md` | Woodpecker CI pipeline |
|
||||
| `_docs/02_document/deployment/environment_strategy.md` | Environment config |
|
||||
@@ -0,0 +1,182 @@
|
||||
# Satellite Provider — Architecture
|
||||
|
||||
## Architecture Vision
|
||||
|
||||
Satellite Provider is a self-hosted .NET 8.0 backend service that pre-downloads, caches, and composites Google Maps satellite imagery for offline use. It runs as a single containerized monolith with PostgreSQL, processing requests asynchronously via in-process queues. The dominant pattern is a layered architecture (API → Services → DataAccess → PostgreSQL) with background hosted services for long-running work.
|
||||
|
||||
**Components & responsibilities**:
|
||||
- **Common** — Shared contracts: DTOs, service interfaces, configuration models, geospatial math
|
||||
- **DataAccess** — PostgreSQL persistence via Dapper + DbUp migrations
|
||||
- **TileDownloader** — Provider-agnostic tile acquisition via `ISatelliteDownloader` interface (first implementation: Google Maps) with deduplication and concurrency control
|
||||
- **RegionProcessing** — Batch tile downloads for geographic areas, stitching, output generation
|
||||
- **RouteManagement** — Route interpolation, geofenced region generation, consolidated map output
|
||||
|
||||
**Major data flows**:
|
||||
- *Tile acquisition*: HTTP request → cache check → Google Maps download → disk + DB persistence
|
||||
- *Region processing*: Request queued → background worker calculates tile grid → downloads all tiles → produces CSV/summary/stitched image
|
||||
- *Route expansion*: Waypoints → interpolated points every ~200m → geofence filtering → region requests per point → optional ZIP archive
|
||||
|
||||
**Architectural principles** (inferred):
|
||||
- Single-instance deployment, no horizontal scaling requirements (`inferred-from: Channel-based queue, no distributed state`)
|
||||
- Immutable tile storage with year-based versioning for cache invalidation (`inferred-from: version column + unique index`)
|
||||
- Fire-and-forget async processing with status polling (`inferred-from: queue + background service + status endpoint`)
|
||||
- No authentication layer — designed as an internal/trusted network service (`inferred-from: no auth middleware in Program.cs`)
|
||||
|
||||
**Planned features** (confirmed by user, currently stubs):
|
||||
- MGRS endpoint — tile access via Military Grid Reference System coordinates
|
||||
- Upload endpoint — UAV nadir camera tile ingestion (Layer 2: orthogonal tiles uploaded post-flight, stored alongside Google Maps Layer 1; most recent layer returned on access)
|
||||
|
||||
**Drift signals**:
|
||||
- `geofence_polygons` mentioned in AGENTS.md as a routes table column but does not exist in schema or entity — documentation drift
|
||||
|
||||
## 1. System Context
|
||||
|
||||
**Problem being solved**: A GPS-denied UAV navigation service requires satellite imagery for positioning and route planning without GPS. This service pre-downloads Google Maps satellite tiles (Layer 1) for specified regions and routes, accepts UAV-captured nadir camera imagery uploaded post-flight (Layer 2), and serves the most recent tile layer on access. Tiles are stitched into composite images and packaged for offline use.
|
||||
|
||||
**System boundaries**: The Satellite Provider is a self-contained backend service. It receives HTTP requests (region/route definitions), downloads tiles from Google Maps, stores them on disk and in PostgreSQL, and produces output files (images, CSVs, ZIPs).
|
||||
|
||||
**External systems**:
|
||||
|
||||
| System | Integration Type | Direction | Purpose |
|
||||
|--------|-----------------|-----------|---------|
|
||||
| Satellite imagery provider (e.g., Google Maps) | HTTPS (tile download) | Outbound | Layer 1 satellite imagery source (provider-agnostic via `ISatelliteDownloader`) |
|
||||
| GPS-Denied Service (UAV) | REST API | Inbound | Layer 2 nadir camera tile uploads post-flight |
|
||||
| PostgreSQL | TCP (Npgsql) | Both | Tile metadata, region/route state |
|
||||
| File System | Local disk | Both | Tile image storage, output artifacts |
|
||||
| HTTP Clients | REST API | Inbound | Region/route requests, tile queries |
|
||||
|
||||
## 2. Technology Stack
|
||||
|
||||
| Layer | Technology | Version | Rationale |
|
||||
|-------|-----------|---------|-----------|
|
||||
| Language | C# | 12.0 | .NET ecosystem, strong typing |
|
||||
| Framework | ASP.NET Core (Minimal API) | 8.0 | Lightweight HTTP hosting |
|
||||
| Database | PostgreSQL | 15+ | Reliable RDBMS, spatial-friendly |
|
||||
| ORM | Dapper | latest | Micro-ORM, raw SQL control |
|
||||
| Migrations | DbUp | latest | Simple SQL-file-based schema migrations |
|
||||
| Image Processing | SixLabors.ImageSharp | 3.1.11 | Cross-platform image manipulation |
|
||||
| Logging | Serilog | 8.0.3 | Structured logging with file sinks |
|
||||
| Hosting | Docker (docker-compose) | — | Containerized deployment |
|
||||
| CI/CD | Woodpecker CI | — | Lightweight self-hosted CI |
|
||||
|
||||
## 3. Deployment Model
|
||||
|
||||
**Environments**: Development (docker-compose), Production (Docker)
|
||||
|
||||
**Infrastructure**:
|
||||
- Docker-based containerized deployment
|
||||
- PostgreSQL as a separate container
|
||||
- Shared volumes for tile storage and output artifacts
|
||||
- No cloud provider dependency (self-hosted capable)
|
||||
|
||||
**Environment-specific configuration**:
|
||||
|
||||
| Config | Development | Production |
|
||||
|--------|-------------|------------|
|
||||
| Database | localhost:5432 (Docker) | Container network `db:5432` |
|
||||
| Secrets | appsettings.Development.json | Environment variables |
|
||||
| Logging | Console + File | File (./logs/) |
|
||||
| API URL | http://localhost:5100 | http://0.0.0.0:5100 |
|
||||
|
||||
## 4. Data Model Overview
|
||||
|
||||
**Core entities**:
|
||||
|
||||
| Entity | Description | Owned By Component |
|
||||
|--------|-------------|--------------------|
|
||||
| Tile | A single satellite image tile with coordinates and zoom | TileDownloader |
|
||||
| Region | A square area request with processing status | RegionProcessing |
|
||||
| Route | A named path with geofence polygons | RouteManagement |
|
||||
| RoutePoint | An individual point (original or interpolated) on a route | RouteManagement |
|
||||
|
||||
**Key relationships**:
|
||||
- Route → RoutePoint: one-to-many (a route has many sequential points)
|
||||
- Route → Region: many-to-many via `route_regions` (each route point generates a region)
|
||||
- Region → Tile: implicit (a processed region references tiles by coordinate/zoom)
|
||||
|
||||
**Data flow summary**:
|
||||
- Client → API → Queue → BackgroundService → GoogleMaps → FileSystem + DB: tile acquisition pipeline
|
||||
- Client → API → RouteService → PointInterpolation → RegionCreation → Queue: route-to-region expansion
|
||||
|
||||
## 5. Integration Points
|
||||
|
||||
### Internal Communication
|
||||
|
||||
| From | To | Protocol | Pattern | Notes |
|
||||
|------|----|----------|---------|-------|
|
||||
| WebApi | RegionProcessing | In-process queue (Channel) | Fire-and-forget | Request queued, status polled |
|
||||
| WebApi | TileDownloader | Direct method call | Request-Response | Synchronous single-tile download |
|
||||
| RegionProcessing | TileDownloader | Direct method call | Request-Response | Per-tile within region processing |
|
||||
| RouteManagement | RegionProcessing | In-process queue (Channel) | Fire-and-forget | Route regions submitted to queue |
|
||||
| All Services | DataAccess | Direct method call | Repository pattern | Dapper queries |
|
||||
|
||||
### External Integrations
|
||||
|
||||
| External System | Protocol | Auth | Rate Limits | Failure Mode |
|
||||
|----------------|----------|------|-------------|--------------|
|
||||
| Satellite imagery provider (abstracted via `ISatelliteDownloader`; first implementation: Google Maps) | HTTPS GET | Provider-specific (e.g., session token) | Configured concurrency (MaxConcurrentDownloads) | Retry with backoff, mark region failed |
|
||||
|
||||
## 6. Non-Functional Requirements
|
||||
|
||||
| Requirement | Target | Measurement | Priority |
|
||||
|------------|--------|-------------|----------|
|
||||
| Concurrent Downloads | 4 (configurable) | SemaphoreSlim limit | High |
|
||||
| Concurrent Regions | 20 (configurable) | Processing config | Medium |
|
||||
| Queue Capacity | 1000 requests | Channel bounded capacity | Medium |
|
||||
| Tile Deduplication | 100% (no re-download) | DB lookup before fetch | High |
|
||||
| Max Zip Size | 50 MB | Route zip output | Medium |
|
||||
|
||||
## 7. Security Architecture
|
||||
|
||||
**Authentication**: None (internal service, no auth layer)
|
||||
|
||||
**Authorization**: None (all endpoints are open)
|
||||
|
||||
**Data protection**:
|
||||
- At rest: No encryption (tiles stored as plain JPEG files)
|
||||
- In transit: HTTPS for Google Maps calls; API itself on HTTP
|
||||
- Secrets management: Google Maps session token in appsettings / env vars
|
||||
|
||||
**Audit logging**: Serilog writes to file; logs exceptions and processing state transitions
|
||||
|
||||
## 8. Key Architectural Decisions
|
||||
|
||||
### ADR-001: Minimal API over Controller-based
|
||||
|
||||
**Context**: Project needed a lightweight HTTP layer for a small set of endpoints.
|
||||
|
||||
**Decision**: Use ASP.NET Core Minimal APIs (no controllers, no MVC).
|
||||
|
||||
**Consequences**: Less ceremony, all routing in `Program.cs`, but less structure for future growth.
|
||||
|
||||
### ADR-002: Dapper over Entity Framework
|
||||
|
||||
**Context**: Database access is straightforward CRUD with some spatial queries.
|
||||
|
||||
**Decision**: Use Dapper for raw SQL control and performance, paired with DbUp for schema migrations.
|
||||
|
||||
**Consequences**: Full SQL control, no ORM overhead; trade-off is manual mapping and no change tracking.
|
||||
|
||||
### ADR-003: In-Process Queue over External Message Broker
|
||||
|
||||
**Context**: Region/route processing needs to be asynchronous but the system is a single service.
|
||||
|
||||
**Decision**: Use `System.Threading.Channels` as an in-process bounded queue.
|
||||
|
||||
**Consequences**: Simple, no external dependencies; but limited to single-instance deployment — no horizontal scaling of workers.
|
||||
|
||||
### ADR-004: File-Based Tile Storage
|
||||
|
||||
**Context**: Tiles are immutable JPEG images that need fast random access.
|
||||
|
||||
**Decision**: Store tiles as files in a directory hierarchy (`./tiles/{zoom}/{x}/{y}.jpg`) with metadata in PostgreSQL.
|
||||
|
||||
**Consequences**: Fast reads, easy backup/migration, but requires shared filesystem for multi-instance (which is not currently needed).
|
||||
|
||||
### ADR-005: Background Hosted Services for Processing
|
||||
|
||||
**Context**: Region and route processing is long-running and should not block HTTP requests.
|
||||
|
||||
**Decision**: Use `IHostedService` implementations that consume from the in-process queue.
|
||||
|
||||
**Consequences**: Clean separation of request handling and processing; lifecycle managed by the host.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Architecture Compliance Baseline
|
||||
|
||||
**Date**: 2026-05-10
|
||||
**Mode**: Baseline (Phase 1 + Phase 7)
|
||||
**Scope**: Full existing codebase
|
||||
**Verdict**: PASS_WITH_WARNINGS
|
||||
|
||||
## Findings
|
||||
|
||||
| # | Severity | Category | File:Line | Title |
|
||||
|---|----------|----------|-----------|-------|
|
||||
| 1 | High | Architecture | SatelliteProvider.Services/TileService.cs:11 | Concrete dependency on GoogleMapsDownloaderV2 bypasses ISatelliteDownloader |
|
||||
| 2 | High | Architecture | SatelliteProvider.Common/Interfaces/ISatelliteDownloader.cs | ISatelliteDownloader interface is dead code |
|
||||
| 3 | Medium | Architecture | SatelliteProvider.Api/Program.cs:141 | API endpoint directly injects concrete downloader + repository |
|
||||
| 4 | Medium | Architecture | SatelliteProvider.Services/ | No physical boundary between logical components in shared project |
|
||||
| 5 | Low | Architecture | module-layout.md | DataAccess documented as importing Common but actually has zero cross-project dependencies |
|
||||
|
||||
## Finding Details
|
||||
|
||||
**F1: Concrete dependency on GoogleMapsDownloaderV2** (High / Architecture)
|
||||
- Location: `SatelliteProvider.Services/TileService.cs:11`
|
||||
- Description: `TileService` depends on the concrete class `GoogleMapsDownloaderV2` instead of `ISatelliteDownloader`. DI registration is also concrete (`AddSingleton<GoogleMapsDownloaderV2>()`). This couples the entire tile pipeline to a single provider.
|
||||
- Impact: Adding a new satellite imagery provider requires modifying TileService and Program.cs DI wiring rather than just registering a new implementation.
|
||||
- Suggestion: Have `GoogleMapsDownloaderV2` implement `ISatelliteDownloader`, update DI to register via interface, inject interface into TileService.
|
||||
|
||||
**F2: ISatelliteDownloader is dead code** (High / Architecture)
|
||||
- Location: `SatelliteProvider.Common/Interfaces/ISatelliteDownloader.cs`
|
||||
- Description: The interface exists (declares `GetTiles(GeoPoint, double, int, CancellationToken)`) but NO class implements it and NO code references it. The actual downloader method used is `GoogleMapsDownloaderV2.GetTilesWithMetadataAsync()` which has a different signature.
|
||||
- Impact: The provider-agnostic abstraction doesn't function. Interface and implementation have diverged.
|
||||
- Suggestion: Update `ISatelliteDownloader` to match the actual API surface needed by consumers, then implement it in `GoogleMapsDownloaderV2`.
|
||||
|
||||
**F3: API endpoint bypasses service layer** (Medium / Architecture)
|
||||
- Location: `SatelliteProvider.Api/Program.cs:141` (`ServeTile`) and `:206` (`GetTileByLatLon`)
|
||||
- Description: Two API endpoints directly inject `GoogleMapsDownloaderV2` and `ITileRepository` instead of using `ITileService`. This bypasses the service layer and creates a shortcut from Layer 4 to Layer 2.
|
||||
- Impact: Business logic (caching, dedup) in TileService is bypassed for these endpoints; tile download logic is duplicated.
|
||||
- Suggestion: Route all tile operations through `ITileService`.
|
||||
|
||||
**F4: No physical boundary in Services project** (Medium / Architecture)
|
||||
- Location: `SatelliteProvider.Services/` (all files)
|
||||
- Description: Three logical components (TileDownloader, RegionProcessing, RouteManagement) share one `.csproj`. No compiler-enforced boundary prevents direct cross-component coupling.
|
||||
- Impact: Over time, services may accumulate hidden coupling that's hard to detect without code review.
|
||||
- Suggestion: Accept as-is for current scale; consider splitting into separate projects if the codebase grows significantly.
|
||||
|
||||
**F5: module-layout.md incorrect — DataAccess has no Common dependency** (Low / Architecture)
|
||||
- Location: `_docs/02_document/module-layout.md`
|
||||
- Description: DataAccess was documented as "Imports from: Common" but `SatelliteProvider.DataAccess.csproj` has no ProjectReference to Common and no `using SatelliteProvider.Common` in any file.
|
||||
- Impact: Documentation inaccuracy; no code impact.
|
||||
- Suggestion: Correct module-layout.md.
|
||||
|
||||
## Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| Critical | 0 |
|
||||
| High | 2 |
|
||||
| Medium | 2 |
|
||||
| Low | 1 |
|
||||
|
||||
The two High findings both relate to the same root cause: the `ISatelliteDownloader` abstraction was created but never wired into the system. The concrete `GoogleMapsDownloaderV2` is used directly everywhere. This is the primary architecture gap — addressing it would enable the provider-agnostic design the system intends to have.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Common (Foundation)
|
||||
|
||||
## 1. High-Level Overview
|
||||
|
||||
**Purpose**: Shared foundation layer containing configuration POCOs, data transfer objects, service interface contracts, and geographic computation utilities used by all other components.
|
||||
|
||||
**Architectural Pattern**: Shared Kernel / Contracts Library
|
||||
|
||||
**Upstream dependencies**: None (leaf)
|
||||
|
||||
**Downstream consumers**: DataAccess, TileDownloader, RegionProcessing, RouteManagement, WebApi, Tests
|
||||
|
||||
## 2. Internal Interfaces
|
||||
|
||||
This component defines the service contracts that other components implement:
|
||||
|
||||
### Interface: ITileService
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `DownloadAndStoreTilesAsync` | lat, lon, sizeMeters, zoomLevel, CancellationToken | `List<TileMetadata>` | Yes | Exception |
|
||||
| `GetTileAsync` | Guid id | `TileMetadata?` | Yes | Exception |
|
||||
| `GetTilesByRegionAsync` | lat, lon, sizeMeters, zoomLevel | `IEnumerable<TileMetadata>` | Yes | Exception |
|
||||
|
||||
### Interface: IRegionService
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `RequestRegionAsync` | id, lat, lon, sizeMeters, zoomLevel, stitchTiles | `RegionStatus` | Yes | Exception |
|
||||
| `GetRegionStatusAsync` | Guid id | `RegionStatus?` | Yes | Exception |
|
||||
| `ProcessRegionAsync` | Guid id, CancellationToken | void | Yes | RateLimitException, HttpRequestException, TimeoutException |
|
||||
|
||||
### Interface: IRouteService
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `CreateRouteAsync` | `CreateRouteRequest` | `RouteResponse` | Yes | ArgumentException |
|
||||
| `GetRouteAsync` | Guid id | `RouteResponse?` | Yes | Exception |
|
||||
|
||||
### Interface: IRegionRequestQueue
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `EnqueueAsync` | `RegionRequest`, CancellationToken | void | Yes | OperationCanceledException |
|
||||
| `DequeueAsync` | CancellationToken | `RegionRequest?` | Yes | OperationCanceledException |
|
||||
|
||||
### Static: GeoUtils
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `WorldToTilePos` | GeoPoint, zoom | (x, y) | No | - |
|
||||
| `TileToWorldPos` | x, y, zoom | GeoPoint | No | - |
|
||||
| `CalculateIntermediatePoints` | start, end, maxSpacing | `List<GeoPoint>` | No | - |
|
||||
| `CalculateDistance` | p1, p2 | double (meters) | No | - |
|
||||
| `GetBoundingBox` | center, radiusM | (minLat, maxLat, minLon, maxLon) | No | - |
|
||||
| `DirectionTo` (ext) | p1, p2 | Direction | No | - |
|
||||
| `GoDirection` (ext) | start, direction | GeoPoint | No | - |
|
||||
|
||||
## 3. External API Specification
|
||||
|
||||
N/A — internal-only component.
|
||||
|
||||
## 4. Data Access Patterns
|
||||
|
||||
N/A — no data access.
|
||||
|
||||
## 5. Implementation Details
|
||||
|
||||
**State Management**: Stateless (pure data types and static utilities)
|
||||
|
||||
**Key Dependencies**: None (no NuGet packages)
|
||||
|
||||
**Algorithmic Complexity**: GeoUtils uses Haversine formula (O(1) per calculation). `CalculateIntermediatePoints` is O(n) where n = ceil(distance / maxSpacing).
|
||||
|
||||
## 6. Extensions and Helpers
|
||||
|
||||
| Helper | Purpose | Used By |
|
||||
|--------|---------|---------|
|
||||
| GeoUtils | Coordinate conversions, distance/bearing math, point interpolation | TileDownloader, RegionProcessing, RouteManagement, WebApi |
|
||||
|
||||
## 7. Caveats & Edge Cases
|
||||
|
||||
- `GeoPoint` equality uses a tolerance of 0.00005° (~5.5m), which may cause false positives for closely-spaced tiles at high zoom levels
|
||||
- `DatabaseConfig` is defined but never wired via DI — connection string is read directly from `IConfiguration`
|
||||
- `ISatelliteDownloader` interface exists but is not implemented by `GoogleMapsDownloaderV2` (legacy artifact)
|
||||
|
||||
## 8. Dependency Graph
|
||||
|
||||
**Must be implemented after**: nothing
|
||||
**Can be implemented in parallel with**: DataAccess
|
||||
**Blocks**: TileDownloader, RegionProcessing, RouteManagement, WebApi
|
||||
|
||||
## 9. Logging Strategy
|
||||
|
||||
N/A — no logging in this component.
|
||||
@@ -0,0 +1,107 @@
|
||||
# DataAccess (Persistence)
|
||||
|
||||
## 1. High-Level Overview
|
||||
|
||||
**Purpose**: Database persistence layer providing Dapper-based repositories for tiles, regions, routes, and route points, plus DbUp-driven schema migrations.
|
||||
|
||||
**Architectural Pattern**: Repository pattern with raw SQL (Dapper)
|
||||
|
||||
**Upstream dependencies**: None at project level (uses Microsoft.Extensions abstractions from NuGet)
|
||||
|
||||
**Downstream consumers**: TileDownloader (TileRepository), RegionProcessing (RegionRepository), RouteManagement (RouteRepository, RegionRepository), WebApi (TileRepository for ServeTile)
|
||||
|
||||
## 2. Internal Interfaces
|
||||
|
||||
### Interface: ITileRepository
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `GetByIdAsync` | Guid | `TileEntity?` | Yes | NpgsqlException |
|
||||
| `GetByTileCoordinatesAsync` | zoom, x, y | `TileEntity?` | Yes | NpgsqlException |
|
||||
| `FindExistingTileAsync` | lat, lon, tileSizeM, zoom, version | `TileEntity?` | Yes | NpgsqlException |
|
||||
| `GetTilesByRegionAsync` | lat, lon, sizeM, zoom | `IEnumerable<TileEntity>` | Yes | NpgsqlException |
|
||||
| `InsertAsync` | `TileEntity` | Guid | Yes | NpgsqlException |
|
||||
| `UpdateAsync` | `TileEntity` | int | Yes | NpgsqlException |
|
||||
| `DeleteAsync` | Guid | int | Yes | NpgsqlException |
|
||||
|
||||
### Interface: IRegionRepository
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `GetByIdAsync` | Guid | `RegionEntity?` | Yes | NpgsqlException |
|
||||
| `GetByStatusAsync` | string | `IEnumerable<RegionEntity>` | Yes | NpgsqlException |
|
||||
| `InsertAsync` | `RegionEntity` | Guid | Yes | NpgsqlException |
|
||||
| `UpdateAsync` | `RegionEntity` | int | Yes | NpgsqlException |
|
||||
| `DeleteAsync` | Guid | int | Yes | NpgsqlException |
|
||||
|
||||
### Interface: IRouteRepository
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `GetByIdAsync` | Guid | `RouteEntity?` | Yes | NpgsqlException |
|
||||
| `GetRoutePointsAsync` | Guid routeId | `IEnumerable<RoutePointEntity>` | Yes | NpgsqlException |
|
||||
| `InsertRouteAsync` | `RouteEntity` | Guid | Yes | NpgsqlException |
|
||||
| `InsertRoutePointsAsync` | `IEnumerable<RoutePointEntity>` | void | Yes | NpgsqlException |
|
||||
| `UpdateRouteAsync` | `RouteEntity` | int | Yes | NpgsqlException |
|
||||
| `LinkRouteToRegionAsync` | routeId, regionId, isGeofence, polygonIndex | void | Yes | NpgsqlException |
|
||||
| `GetRegionIdsByRouteAsync` | Guid routeId | `IEnumerable<Guid>` | Yes | NpgsqlException |
|
||||
| `GetGeofenceRegionIdsByRouteAsync` | Guid routeId | `IEnumerable<Guid>` | Yes | NpgsqlException |
|
||||
| `GetGeofenceRegionsByPolygonAsync` | Guid routeId | `Dictionary<int, List<Guid>>` | Yes | NpgsqlException |
|
||||
| `GetRoutesWithPendingMapsAsync` | — | `IEnumerable<RouteEntity>` | Yes | NpgsqlException |
|
||||
|
||||
### Class: DatabaseMigrator
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `RunMigrations` | — | bool | No | Exception |
|
||||
|
||||
## 4. Data Access Patterns
|
||||
|
||||
### Queries
|
||||
| Query | Frequency | Hot Path | Index Needed |
|
||||
|-------|-----------|----------|--------------|
|
||||
| GetByTileCoordinatesAsync (tile lookup) | Very High | Yes | `(tile_zoom, tile_x, tile_y)` |
|
||||
| GetTilesByRegionAsync (spatial) | High | Yes | `(latitude, longitude, tile_zoom)` |
|
||||
| InsertAsync (tile upsert) | High | Yes | Composite unique on `(lat, lon, zoom, size, version)` |
|
||||
| GetByStatusAsync (region polling) | Medium | No | `(status)` |
|
||||
| GetRoutesWithPendingMapsAsync | Low | No | `(request_maps, maps_ready)` |
|
||||
|
||||
### Storage Estimates
|
||||
| Table | Est. Row Count (1yr) | Row Size | Growth Rate |
|
||||
|-------|---------------------|----------|-------------|
|
||||
| tiles | ~100K–1M (depends on usage) | ~200B | Variable |
|
||||
| regions | ~10K–50K | ~150B | Proportional to tile requests |
|
||||
| routes | ~1K–5K | ~200B | Low |
|
||||
| route_points | ~50K–500K | ~100B | Proportional to routes |
|
||||
| route_regions | ~10K–100K | ~50B | Proportional to routes |
|
||||
|
||||
## 5. Implementation Details
|
||||
|
||||
**State Management**: Stateless — each repository creates a new Npgsql connection per method call. Npgsql handles internal connection pooling.
|
||||
|
||||
**Key Dependencies**:
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| Dapper | 2.1.35 | Micro-ORM for SQL queries |
|
||||
| Npgsql | 9.0.2 | PostgreSQL ADO.NET driver |
|
||||
| dbup-postgresql | 6.0.3 | Schema migration runner |
|
||||
|
||||
**Error Handling**: Exceptions propagate to callers. No retry logic at the repository level.
|
||||
|
||||
## 7. Caveats & Edge Cases
|
||||
|
||||
- Repository interfaces are defined in this project (not in Common), creating a dependency from Services to DataAccess
|
||||
- Column mapping uses SQL aliases (`tile_zoom as TileZoom`) rather than Dapper attribute mapping
|
||||
- TileRepository.InsertAsync uses an upsert pattern; concurrent inserts of the same tile won't conflict
|
||||
- No soft-delete; `DeleteAsync` is a hard delete
|
||||
|
||||
## 8. Dependency Graph
|
||||
|
||||
**Must be implemented after**: nothing (parallel with Common)
|
||||
**Can be implemented in parallel with**: Common
|
||||
**Blocks**: TileDownloader, RegionProcessing, RouteManagement, WebApi
|
||||
|
||||
## 9. Logging Strategy
|
||||
|
||||
| Log Level | When | Example |
|
||||
|-----------|------|---------|
|
||||
| INFO | Migration start/complete | `Starting database migrations...` |
|
||||
| ERROR | Migration failure | `Database migration failed` |
|
||||
|
||||
Structured logging via `ILogger<T>`. Logger injected but rarely used in repositories.
|
||||
@@ -0,0 +1,74 @@
|
||||
# TileDownloader
|
||||
|
||||
## 1. High-Level Overview
|
||||
|
||||
**Purpose**: Acquires satellite imagery tiles from Google Maps, stores them on disk, and persists metadata to the database. Handles session tokens, concurrent downloads, retry logic, and tile deduplication.
|
||||
|
||||
**Architectural Pattern**: Service + Gateway (wraps external API with retry/throttling)
|
||||
|
||||
**Upstream dependencies**: Common (DTOs, GeoUtils, configs), DataAccess (TileEntity, ITileRepository)
|
||||
|
||||
**Downstream consumers**: RegionProcessing (via ITileService), WebApi (GoogleMapsDownloaderV2 directly for single-tile endpoints)
|
||||
|
||||
## 2. Internal Interfaces
|
||||
|
||||
### Class: GoogleMapsDownloaderV2
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `DownloadSingleTileAsync` | lat, lon, zoomLevel, CancellationToken | `DownloadedTileInfoV2` | Yes | ArgumentException, RateLimitException, HttpRequestException |
|
||||
| `GetTilesWithMetadataAsync` | center, radiusM, zoom, existingTiles, CancellationToken | `List<DownloadedTileInfoV2>` | Yes | ArgumentException, RateLimitException, HttpRequestException |
|
||||
|
||||
### Service: TileService (implements ITileService)
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `DownloadAndStoreTilesAsync` | lat, lon, sizeM, zoom, CancellationToken | `List<TileMetadata>` | Yes | propagated from downloader |
|
||||
| `GetTileAsync` | Guid | `TileMetadata?` | Yes | NpgsqlException |
|
||||
| `GetTilesByRegionAsync` | lat, lon, sizeM, zoom | `IEnumerable<TileMetadata>` | Yes | NpgsqlException |
|
||||
|
||||
## 4. Data Access Patterns
|
||||
|
||||
### Caching Strategy
|
||||
| Data | Cache Type | TTL | Invalidation |
|
||||
|------|-----------|-----|-------------|
|
||||
| Tile bytes | In-memory (IMemoryCache, WebApi layer) | 1h absolute, 30min sliding | None (manual restart) |
|
||||
| Tile metadata | Database | Until year rollover | Version-based (current year) |
|
||||
| Active downloads | ConcurrentDictionary | Duration of download | Removed on completion |
|
||||
|
||||
## 5. Implementation Details
|
||||
|
||||
**Algorithmic Complexity**: Tile grid calculation is O(w×h) where w×h is the number of tiles covering the bounding box.
|
||||
|
||||
**State Management**: `_activeDownloads` (ConcurrentDictionary) prevents duplicate concurrent downloads. `_downloadSemaphore` limits parallelism.
|
||||
|
||||
**Key Dependencies**:
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| Newtonsoft.Json | 13.0.4 | Serialize session creation request body |
|
||||
| IHttpClientFactory | built-in | Create HttpClient instances per request |
|
||||
|
||||
**Error Handling**:
|
||||
- Exponential backoff retry for 429 (rate limit) and 5xx errors: 1s → 2s → 4s → 8s → 16s, max 30s, 5 retries
|
||||
- Immediate throw for 401/403 (auth errors) and cancellation
|
||||
- `RateLimitException` thrown after exhausting retries on 429
|
||||
|
||||
## 7. Caveats & Edge Cases
|
||||
|
||||
- `GoogleMapsDownloaderV2` is registered as a concrete singleton (not behind an interface), creating tight coupling in `TileService` and `Program.cs`
|
||||
- User-Agent header spoofs Chrome — could be rejected if Google changes detection
|
||||
- Allowed zoom levels hardcoded to [15,16,17,18,19] — throws for others
|
||||
- Session token rotation threshold (100 tiles) is an educated guess; Google's actual limit is not documented
|
||||
- Static `_activeDownloads` dictionary means deduplication is process-wide, surviving service scope boundaries
|
||||
|
||||
## 8. Dependency Graph
|
||||
|
||||
**Must be implemented after**: Common, DataAccess
|
||||
**Can be implemented in parallel with**: nothing (needs both foundations)
|
||||
**Blocks**: RegionProcessing
|
||||
|
||||
## 9. Logging Strategy
|
||||
|
||||
| Log Level | When | Example |
|
||||
|-----------|------|---------|
|
||||
| ERROR | Download failure, session token failure | `Tile download failed. Tile: (X, Y), Status: {StatusCode}` |
|
||||
| WARN | Rate limiting retry | `Rate limited (429). Waiting {Delay}s before retry` |
|
||||
| INFO | — | (no INFO-level logs in this component) |
|
||||
@@ -0,0 +1,71 @@
|
||||
# RegionProcessing
|
||||
|
||||
## 1. High-Level Overview
|
||||
|
||||
**Purpose**: Manages the lifecycle of geographic region tile requests — from API submission through a bounded queue to background processing that downloads tiles, generates CSV/summary files, and optionally stitches tiles into composite images.
|
||||
|
||||
**Architectural Pattern**: Producer-Consumer with Background Workers
|
||||
|
||||
**Upstream dependencies**: Common (DTOs, interfaces, configs, GeoUtils), DataAccess (RegionRepository), TileDownloader (ITileService)
|
||||
|
||||
**Downstream consumers**: RouteManagement (creates regions for route points and geofences), WebApi (RequestRegion/GetRegionStatus endpoints)
|
||||
|
||||
## 2. Internal Interfaces
|
||||
|
||||
### Service: RegionService (implements IRegionService)
|
||||
See Common component for interface definition. Key implementation details:
|
||||
- `RequestRegionAsync`: creates DB record, enqueues to bounded channel
|
||||
- `ProcessRegionAsync`: 5-minute timeout, comprehensive error handling, generates CSV + summary + optional stitched image
|
||||
|
||||
### BackgroundService: RegionProcessingService
|
||||
- `ExecuteAsync`: spawns N parallel workers (configurable via `MaxConcurrentRegions`) with staggered startup
|
||||
|
||||
### Queue: RegionRequestQueue (implements IRegionRequestQueue)
|
||||
- Bounded `Channel<RegionRequest>` with `BoundedChannelFullMode.Wait`
|
||||
|
||||
## 4. Data Access Patterns
|
||||
|
||||
### Queries
|
||||
| Query | Frequency | Hot Path | Index Needed |
|
||||
|-------|-----------|----------|--------------|
|
||||
| Region GetByIdAsync | Very High (per processing) | Yes | PK |
|
||||
| Region UpdateAsync (status transitions) | High | Yes | PK |
|
||||
| Region InsertAsync | Medium | No | — |
|
||||
|
||||
## 5. Implementation Details
|
||||
|
||||
**State Management**: Region status tracked in database (queued → processing → completed/failed). Queue state is in-memory (Channel<T>).
|
||||
|
||||
**Key Dependencies**:
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| SixLabors.ImageSharp | 3.1.11 | Tile stitching into composite JPEG |
|
||||
| System.Threading.Channels | built-in | Bounded async queue |
|
||||
|
||||
**Error Handling**:
|
||||
- 5-minute processing timeout per region
|
||||
- Separate catch blocks for: timeout, external cancellation, rate limiting, HTTP errors, generic errors
|
||||
- All failures produce a summary file with error details and set status to "failed"
|
||||
|
||||
## 7. Caveats & Edge Cases
|
||||
|
||||
- Queue is in-memory: pending requests are lost on process restart (no persistence)
|
||||
- 5-minute timeout is hardcoded, not configurable
|
||||
- Stitching crosshair is drawn with a fixed 10-pixel arm length (±5 pixels)
|
||||
- Region status "queued" in code vs "pending" mentioned in some API documentation
|
||||
- `RegionProcessingService` workers have random startup delay (100–500ms) to avoid thundering herd on queue
|
||||
|
||||
## 8. Dependency Graph
|
||||
|
||||
**Must be implemented after**: Common, DataAccess, TileDownloader
|
||||
**Can be implemented in parallel with**: nothing at this layer
|
||||
**Blocks**: RouteManagement (uses IRegionService to create regions)
|
||||
|
||||
## 9. Logging Strategy
|
||||
|
||||
| Log Level | When | Example |
|
||||
|-----------|------|---------|
|
||||
| ERROR | Processing failure | `Failed to process region {RegionId}` |
|
||||
| ERROR | Rate limit exceeded | `Rate limit exceeded for region {RegionId}` |
|
||||
| WARN | Region not found, missing tile file | `Region {RegionId} not found in database` |
|
||||
| INFO | Service start/stop, queue creation | `Region Processing Service started with {N} workers` |
|
||||
@@ -0,0 +1,75 @@
|
||||
# RouteManagement
|
||||
|
||||
## 1. High-Level Overview
|
||||
|
||||
**Purpose**: Creates routes from user-defined waypoints, calculates intermediate points along the path, manages geofence regions, and generates consolidated route maps (stitched images, CSVs, summaries, ZIP archives) from completed region tile data.
|
||||
|
||||
**Architectural Pattern**: Service + Background Poller
|
||||
|
||||
**Upstream dependencies**: Common (DTOs, GeoUtils, configs), DataAccess (RouteRepository, RegionRepository), RegionProcessing (IRegionService for region creation)
|
||||
|
||||
**Downstream consumers**: WebApi (CreateRoute/GetRoute endpoints)
|
||||
|
||||
## 2. Internal Interfaces
|
||||
|
||||
### Service: RouteService (implements IRouteService)
|
||||
See Common component for interface definition. Key implementation details:
|
||||
- `CreateRouteAsync`: validates, interpolates points every ≤200m, persists, creates geofence grid regions
|
||||
- `GetRouteAsync`: reads route + points from DB
|
||||
|
||||
### BackgroundService: RouteProcessingService
|
||||
- `ExecuteAsync`: polls every 5 seconds for routes with `request_maps=true AND maps_ready=false`
|
||||
- `ProcessRouteSequentiallyAsync`: checks region completion, retries failed regions, generates maps when ready
|
||||
|
||||
## 4. Data Access Patterns
|
||||
|
||||
### Queries
|
||||
| Query | Frequency | Hot Path | Index Needed |
|
||||
|-------|-----------|----------|--------------|
|
||||
| GetRoutesWithPendingMapsAsync (polling) | Every 5s | No | `(request_maps, maps_ready)` |
|
||||
| GetRoutePointsAsync | Per route processing | Yes | `(route_id, sequence_number)` |
|
||||
| GetRegionIdsByRouteAsync | Per route processing | Yes | `(route_id)` |
|
||||
| InsertRoutePointsAsync (bulk) | Per route creation | No | — |
|
||||
|
||||
## 5. Implementation Details
|
||||
|
||||
**Algorithmic Complexity**: Point interpolation is O(n×m) where n = input points and m = max intermediate points per segment. Geofence grid creation is O(latSteps × lonSteps). Route-region matching uses O(points × regions) nearest-neighbor.
|
||||
|
||||
**State Management**: Route state tracked in database (`request_maps`, `maps_ready` flags). Processing is polling-based (not queue-based like regions).
|
||||
|
||||
**Key Dependencies**:
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| SixLabors.ImageSharp | 3.1.11 | Route map stitching with geofence borders and route markers |
|
||||
| System.IO.Compression | built-in | ZIP archive creation for tiles |
|
||||
|
||||
**Error Handling**:
|
||||
- Route creation validates: min 2 points, size range, name required, geofence coordinate validity
|
||||
- RouteProcessingService catches exceptions per-route and continues to next
|
||||
- Failed regions are retried by creating new region requests
|
||||
- Tile coordinate extraction from filenames has a fallback returning (-1,-1) for unparseable names
|
||||
|
||||
## 7. Caveats & Edge Cases
|
||||
|
||||
- 200m max point spacing is hardcoded constant (`MAX_POINT_SPACING_METERS`)
|
||||
- Polling interval (5s) is hardcoded
|
||||
- `RouteProcessingService` resolves `IRegionService` via `IServiceProvider.CreateScope()` to avoid circular DI
|
||||
- Route map stitching extracts tile coordinates from filenames (`tile_{z}_{x}_{y}_{ts}.jpg`); format change would break stitching
|
||||
- ZIP creation runs on `Task.Run` (ThreadPool) — could consume a thread for large archives
|
||||
- `MatchRegionsToRoutePoints` uses O(n²) nearest-neighbor matching; could be slow for routes with many points
|
||||
- Region file cleanup deletes individual region CSVs/summaries after consolidation into route-level files
|
||||
- `catch` in `ExtractTileCoordinatesFromFilename` silently swallows all exceptions
|
||||
|
||||
## 8. Dependency Graph
|
||||
|
||||
**Must be implemented after**: Common, DataAccess, RegionProcessing
|
||||
**Can be implemented in parallel with**: nothing
|
||||
**Blocks**: nothing (top of the dependency chain alongside WebApi)
|
||||
|
||||
## 9. Logging Strategy
|
||||
|
||||
| Log Level | When | Example |
|
||||
|-----------|------|---------|
|
||||
| ERROR | Route processing failure | `Error processing route {RouteId}` |
|
||||
| WARN | Missing tile files, route not found, parse failures | `Tile file not found: {FilePath}` |
|
||||
| INFO | Processing complete, CSV/summary/zip generated | `Route {RouteId} maps processing completed` |
|
||||
@@ -0,0 +1,217 @@
|
||||
# Satellite Provider — Data Model
|
||||
|
||||
## Entity-Relationship Diagram
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
TILES {
|
||||
uuid id PK
|
||||
int tile_zoom
|
||||
float latitude
|
||||
float longitude
|
||||
float tile_size_meters
|
||||
int tile_size_pixels
|
||||
varchar image_type
|
||||
varchar maps_version
|
||||
int version
|
||||
varchar file_path
|
||||
int tile_x
|
||||
int tile_y
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
REGIONS {
|
||||
uuid id PK
|
||||
float latitude
|
||||
float longitude
|
||||
float size_meters
|
||||
int zoom_level
|
||||
varchar status
|
||||
bool stitch_tiles
|
||||
varchar csv_file_path
|
||||
varchar summary_file_path
|
||||
int tiles_downloaded
|
||||
int tiles_reused
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
ROUTES {
|
||||
uuid id PK
|
||||
varchar name
|
||||
text description
|
||||
float region_size_meters
|
||||
int zoom_level
|
||||
float total_distance_meters
|
||||
int total_points
|
||||
bool request_maps
|
||||
bool maps_ready
|
||||
bool create_tiles_zip
|
||||
varchar tiles_zip_path
|
||||
varchar csv_file_path
|
||||
varchar summary_file_path
|
||||
varchar stitched_image_path
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
ROUTE_POINTS {
|
||||
uuid id PK
|
||||
uuid route_id FK
|
||||
int sequence_number
|
||||
float latitude
|
||||
float longitude
|
||||
varchar point_type
|
||||
int segment_index
|
||||
float distance_from_previous
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
ROUTE_REGIONS {
|
||||
uuid route_id FK
|
||||
uuid region_id FK
|
||||
bool is_geofence
|
||||
int geofence_polygon_index
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
ROUTES ||--o{ ROUTE_POINTS : "has many"
|
||||
ROUTES ||--o{ ROUTE_REGIONS : "has many"
|
||||
REGIONS ||--o{ ROUTE_REGIONS : "linked via"
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
### tiles
|
||||
|
||||
Stores metadata for downloaded satellite imagery tiles. Each tile is a single image at a specific geographic coordinate and zoom level.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | Unique tile identifier |
|
||||
| tile_zoom | INT | NOT NULL | Google Maps zoom level (1-20) |
|
||||
| latitude | DOUBLE PRECISION | NOT NULL | Center latitude |
|
||||
| longitude | DOUBLE PRECISION | NOT NULL | Center longitude |
|
||||
| tile_size_meters | DOUBLE PRECISION | NOT NULL | Ground coverage in meters |
|
||||
| tile_size_pixels | INT | NOT NULL | Image dimension in pixels |
|
||||
| image_type | VARCHAR(10) | NOT NULL | Image format (e.g., "jpg") |
|
||||
| maps_version | VARCHAR(50) | | Google Maps version string |
|
||||
| version | INT | NOT NULL, DEFAULT 2025 | Year-based versioning for cache invalidation |
|
||||
| file_path | VARCHAR(500) | NOT NULL | Relative path to stored image |
|
||||
| tile_x | INT | NOT NULL | Tile X coordinate (Slippy Map) |
|
||||
| tile_y | INT | NOT NULL | Tile Y coordinate (Slippy Map) |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW | |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW | |
|
||||
|
||||
**Indexes**:
|
||||
- `idx_tiles_unique_location` UNIQUE (latitude, longitude, tile_zoom, tile_size_meters, version)
|
||||
- `idx_tiles_coordinates` (tile_zoom, tile_x, tile_y, version)
|
||||
- `idx_tiles_zoom` (tile_zoom)
|
||||
|
||||
### regions
|
||||
|
||||
Tracks region download requests and their processing status.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | Region request identifier |
|
||||
| latitude | DOUBLE PRECISION | NOT NULL | Center latitude |
|
||||
| longitude | DOUBLE PRECISION | NOT NULL | Center longitude |
|
||||
| size_meters | DOUBLE PRECISION | NOT NULL | Square region side length |
|
||||
| zoom_level | INT | NOT NULL | Zoom level for tiles |
|
||||
| status | VARCHAR(20) | NOT NULL | pending / processing / completed / failed |
|
||||
| stitch_tiles | BOOLEAN | NOT NULL, DEFAULT false | Whether to produce stitched image |
|
||||
| csv_file_path | VARCHAR(500) | | Path to tile manifest CSV |
|
||||
| summary_file_path | VARCHAR(500) | | Path to summary text |
|
||||
| tiles_downloaded | INT | DEFAULT 0 | Count of newly downloaded tiles |
|
||||
| tiles_reused | INT | DEFAULT 0 | Count of cache-hit tiles |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW | |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW | |
|
||||
|
||||
**Indexes**:
|
||||
- `idx_regions_status` (status)
|
||||
|
||||
### routes
|
||||
|
||||
Defines route paths with configuration for map tile generation.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | Route identifier |
|
||||
| name | VARCHAR(200) | NOT NULL | Human-readable name |
|
||||
| description | TEXT | | Optional description |
|
||||
| region_size_meters | DOUBLE PRECISION | NOT NULL | Size of region per point |
|
||||
| zoom_level | INT | NOT NULL | Zoom level for regions |
|
||||
| total_distance_meters | DOUBLE PRECISION | NOT NULL | Total route length |
|
||||
| total_points | INT | NOT NULL | Total point count (original + interpolated) |
|
||||
| request_maps | BOOLEAN | NOT NULL, DEFAULT false | Whether to generate map tiles |
|
||||
| maps_ready | BOOLEAN | NOT NULL, DEFAULT false | Whether map generation is complete |
|
||||
| create_tiles_zip | BOOLEAN | NOT NULL, DEFAULT false | Whether to produce ZIP archive |
|
||||
| tiles_zip_path | VARCHAR(500) | | Path to output ZIP |
|
||||
| csv_file_path | VARCHAR(500) | | Route-level CSV |
|
||||
| summary_file_path | VARCHAR(500) | | Route-level summary |
|
||||
| stitched_image_path | VARCHAR(500) | | Route-level stitched image |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW | |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW | |
|
||||
|
||||
### route_points
|
||||
|
||||
Stores all points along a route (both original waypoints and interpolated intermediate points).
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | Point identifier |
|
||||
| route_id | UUID | FK → routes.id, CASCADE | Parent route |
|
||||
| sequence_number | INT | NOT NULL, UNIQUE(route_id, seq) | Order along route |
|
||||
| latitude | DOUBLE PRECISION | NOT NULL | Point latitude |
|
||||
| longitude | DOUBLE PRECISION | NOT NULL | Point longitude |
|
||||
| point_type | VARCHAR(20) | NOT NULL | "original" or "intermediate" |
|
||||
| segment_index | INT | NOT NULL | Which segment (between original points) |
|
||||
| distance_from_previous | DOUBLE PRECISION | | Meters from previous point |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW | |
|
||||
|
||||
**Indexes**:
|
||||
- `idx_route_points_route` (route_id, sequence_number)
|
||||
- `idx_route_points_coords` (latitude, longitude)
|
||||
|
||||
### route_regions
|
||||
|
||||
Junction table linking routes to their generated region requests, with geofence metadata.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| route_id | UUID | FK → routes.id, CASCADE, PK | |
|
||||
| region_id | UUID | FK → regions.id, CASCADE, PK | |
|
||||
| is_geofence | BOOLEAN | NOT NULL, DEFAULT false | Whether point is inside a geofence |
|
||||
| geofence_polygon_index | INTEGER | | Which polygon (0-based) the point is in |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW | |
|
||||
|
||||
**Indexes**:
|
||||
- `idx_route_regions_route` (route_id)
|
||||
- `idx_route_regions_region` (region_id)
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
- **Tool**: DbUp (embedded SQL scripts)
|
||||
- **Execution**: Automatic on application startup (`DatabaseMigrator.Migrate()`)
|
||||
- **Naming**: `NNN_DescriptiveName.sql` (sequential numbering)
|
||||
- **Storage**: Embedded resources in `SatelliteProvider.DataAccess` assembly
|
||||
- **Tracking**: DbUp's internal `schemaversions` table records which scripts have run
|
||||
- **Rollback**: Not supported — forward-only migrations
|
||||
|
||||
## Migration History
|
||||
|
||||
| # | Migration | Purpose |
|
||||
|---|-----------|---------|
|
||||
| 001 | CreateTilesTable | Base tiles table |
|
||||
| 002 | CreateRegionsTable | Region request tracking |
|
||||
| 003 | CreateIndexes | Performance indexes |
|
||||
| 004 | AddVersionColumn | Year-based tile versioning + dedup |
|
||||
| 005 | CreateRoutesTables | Routes, route_points, route_regions |
|
||||
| 006 | AddStitchTilesToRegions | Stitch flag on regions |
|
||||
| 007 | AddRouteMapFields | request_maps, maps_ready, file paths on routes |
|
||||
| 008 | AddGeofenceFlagToRouteRegions | is_geofence flag |
|
||||
| 009 | AddGeofencePolygonIndex | Polygon index tracking |
|
||||
| 010 | AddTilesZipToRoutes | ZIP generation fields |
|
||||
| 011 | AddTileCoordinates | Slippy map X/Y + rename zoom_level → tile_zoom |
|
||||
@@ -0,0 +1,49 @@
|
||||
# CI/CD Pipeline
|
||||
|
||||
## Platform
|
||||
|
||||
**CI Server**: Woodpecker CI (self-hosted)
|
||||
**Agent architecture**: ARM64 (AMD64 prepared but not yet active)
|
||||
|
||||
## Pipeline Stages
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Push[Push/PR to dev/stage/main] --> Test[01-test]
|
||||
Test --> Build[02-build-push]
|
||||
```
|
||||
|
||||
### 01-test (Unit Tests)
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Trigger | push, pull_request, manual |
|
||||
| Branches | dev, stage, main |
|
||||
| Image | mcr.microsoft.com/dotnet/sdk:8.0 |
|
||||
| Steps | `dotnet restore` → `dotnet test` (Release config) |
|
||||
| Output | TRX test results |
|
||||
|
||||
### 02-build-push (Docker Build & Push)
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Trigger | push, manual |
|
||||
| Branches | dev, stage, main |
|
||||
| Depends on | 01-test (must pass) |
|
||||
| Image | docker (DinD via socket mount) |
|
||||
| Tag format | `{branch}-arm` (e.g., `dev-arm`) |
|
||||
| Registry | Private (from secrets: registry_host, registry_user, registry_token) |
|
||||
|
||||
## Multi-Architecture Strategy
|
||||
|
||||
- Currently: ARM64 only
|
||||
- Prepared: AMD64 entry commented out in matrix
|
||||
- Tag suffix distinguishes architectures (`-arm`, `-amd`)
|
||||
|
||||
## Secrets
|
||||
|
||||
| Secret | Purpose |
|
||||
|--------|---------|
|
||||
| registry_host | Container registry URL |
|
||||
| registry_user | Registry username |
|
||||
| registry_token | Registry password/token |
|
||||
@@ -0,0 +1,45 @@
|
||||
# Containerization
|
||||
|
||||
## Docker Image
|
||||
|
||||
**Base image**: `mcr.microsoft.com/dotnet/aspnet:8.0`
|
||||
**Build image**: `mcr.microsoft.com/dotnet/sdk:8.0`
|
||||
**Build strategy**: Multi-stage (restore → build → publish → runtime)
|
||||
**Exposed ports**: 8080 (HTTP), 8081 (management/metrics)
|
||||
|
||||
## Container Composition (docker-compose.yml)
|
||||
|
||||
| Service | Image | Ports (host:container) | Purpose |
|
||||
|---------|-------|------------------------|---------|
|
||||
| postgres | postgres:16 | 5432:5432 | Database |
|
||||
| api | Custom (Dockerfile) | 18980:8080, 18981:8081 | Application |
|
||||
|
||||
## Volumes
|
||||
|
||||
| Mount | Container Path | Purpose |
|
||||
|-------|---------------|---------|
|
||||
| ./tiles | /app/tiles | Tile image storage |
|
||||
| ./ready | /app/ready | Output artifacts (CSV, summary, stitched, ZIP) |
|
||||
| ./logs | /app/logs | Serilog file output |
|
||||
| postgres_data (named) | /var/lib/postgresql/data | Database persistence |
|
||||
|
||||
## Health Checks
|
||||
|
||||
- **PostgreSQL**: `pg_isready -U postgres` (interval 5s, timeout 5s, retries 5)
|
||||
- **API**: depends on postgres health (startup ordering)
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Source | Purpose |
|
||||
|----------|--------|---------|
|
||||
| ASPNETCORE_ENVIRONMENT | docker-compose | Environment selection |
|
||||
| ASPNETCORE_URLS | docker-compose | Listen address |
|
||||
| ConnectionStrings__DefaultConnection | docker-compose | DB connection string |
|
||||
| MapConfig__ApiKey | Host env `GOOGLE_MAPS_API_KEY` | Google Maps API key |
|
||||
| AZAION_REVISION | Build arg (CI_COMMIT_SHA) | Git revision tracking |
|
||||
|
||||
## Build Labels (OCI)
|
||||
|
||||
- `org.opencontainers.image.revision` — Git commit SHA
|
||||
- `org.opencontainers.image.created` — Build timestamp
|
||||
- `org.opencontainers.image.source` — Repository URL
|
||||
@@ -0,0 +1,32 @@
|
||||
# Environment Strategy
|
||||
|
||||
## Environments
|
||||
|
||||
| Environment | Purpose | Configuration Source |
|
||||
|-------------|---------|---------------------|
|
||||
| Development | Local development via docker-compose | appsettings.Development.json + docker-compose env |
|
||||
| Production | Deployed container | Environment variables |
|
||||
|
||||
## Configuration Hierarchy
|
||||
|
||||
1. `appsettings.json` — base defaults
|
||||
2. `appsettings.{Environment}.json` — environment overrides
|
||||
3. Environment variables — final override (production secrets)
|
||||
|
||||
## Key Differences
|
||||
|
||||
| Concern | Development | Production |
|
||||
|---------|-------------|------------|
|
||||
| Database host | localhost / postgres (container) | Environment variable |
|
||||
| Google Maps key | appsettings.Development.json | `MapConfig__ApiKey` env var |
|
||||
| Logging | Console + File | File only |
|
||||
| Swagger UI | Enabled | Enabled (no auth gate currently) |
|
||||
| Ports | 18980 (mapped from 8080) | 8080 |
|
||||
|
||||
## Observability
|
||||
|
||||
- **Logging**: Serilog writing to `./logs/` directory (file sink)
|
||||
- **Log format**: Structured (Serilog default)
|
||||
- **Metrics**: None currently implemented
|
||||
- **Health checks**: PostgreSQL readiness via `pg_isready`
|
||||
- **Tracing**: None currently implemented
|
||||
@@ -0,0 +1,52 @@
|
||||
# Component Relationship Diagram
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "External"
|
||||
Client[HTTP Clients]
|
||||
GoogleMaps[Google Maps API]
|
||||
PG[(PostgreSQL)]
|
||||
FS[File System]
|
||||
end
|
||||
|
||||
subgraph "SatelliteProvider"
|
||||
WebApi[WebApi<br/>Program.cs endpoints]
|
||||
Route[RouteManagement<br/>RouteService + RouteProcessingService]
|
||||
Region[RegionProcessing<br/>RegionService + Queue + Workers]
|
||||
Tile[TileDownloader<br/>GoogleMapsDownloaderV2 + TileService]
|
||||
DA[DataAccess<br/>Repositories + Migrations]
|
||||
Common[Common<br/>DTOs + Interfaces + Configs + GeoUtils]
|
||||
end
|
||||
|
||||
Client -->|HTTP| WebApi
|
||||
WebApi --> Route
|
||||
WebApi --> Region
|
||||
WebApi --> Tile
|
||||
Route --> Region
|
||||
Route --> DA
|
||||
Region --> Tile
|
||||
Region --> DA
|
||||
Tile --> DA
|
||||
Tile -->|HTTPS| GoogleMaps
|
||||
Tile --> FS
|
||||
Region --> FS
|
||||
Route --> FS
|
||||
DA --> PG
|
||||
WebApi --> DA
|
||||
WebApi --> Common
|
||||
Route --> Common
|
||||
Region --> Common
|
||||
Tile --> Common
|
||||
DA --> Common
|
||||
```
|
||||
|
||||
## Component Summary
|
||||
|
||||
| # | Component | Project(s) | Responsibility |
|
||||
|---|-----------|-----------|---------------|
|
||||
| 1 | Common | SatelliteProvider.Common | Shared DTOs, interfaces, configs, GeoUtils |
|
||||
| 2 | DataAccess | SatelliteProvider.DataAccess | Database entities, Dapper repositories, DbUp migrations |
|
||||
| 3 | TileDownloader | SatelliteProvider.Services (GoogleMapsDownloaderV2, TileService) | Google Maps tile acquisition, storage, caching |
|
||||
| 4 | RegionProcessing | SatelliteProvider.Services (RegionService, RegionProcessingService, RegionRequestQueue) | Region request lifecycle, tile stitching, CSV/summary output |
|
||||
| 5 | RouteManagement | SatelliteProvider.Services (RouteService, RouteProcessingService) | Route creation, point interpolation, geofencing, consolidated map output |
|
||||
| — | WebApi | SatelliteProvider.Api (Program.cs) | HTTP endpoints, DI configuration, startup |
|
||||
@@ -0,0 +1,40 @@
|
||||
# Glossary
|
||||
|
||||
## Domain Terms
|
||||
|
||||
| Term | Definition | Source |
|
||||
|------|-----------|--------|
|
||||
| Tile | A single satellite imagery square (typically 256×256 px) at a specific zoom level and coordinate | modules/services_tile_service.md |
|
||||
| Region | A square geographic area defined by center point and size in meters; the unit of work for batch tile downloads | modules/services_region_service.md |
|
||||
| Route | An ordered sequence of geographic waypoints with interpolated intermediate points | modules/services_route_service.md |
|
||||
| Route Point | A single lat/lon coordinate on a route; either "original" (user-provided waypoint) or "intermediate" (system-generated) | modules/dataaccess_models.md |
|
||||
| Geofence | A rectangular geographic boundary (NW + SE corners) used to filter which route points receive map tile coverage | components/05_route_management/description.md |
|
||||
| Zoom Level | Google Maps tile resolution level (1–20); higher = more detail, smaller ground coverage per tile | modules/common_configs.md |
|
||||
| Stitch | Compositing multiple tiles into a single larger image with optional markers/borders | modules/services_region_service.md |
|
||||
| Layer 1 | Satellite imagery from external providers (provider-agnostic; first implementation: Google Maps) | user clarification |
|
||||
| Layer 2 | UAV-captured nadir camera imagery (orthogonal tiles uploaded post-flight) | user clarification |
|
||||
| Nadir Camera | Downward-facing camera on a UAV capturing ground imagery during flight | user clarification |
|
||||
| GPS-Denied Service | The consuming system: a UAV navigation service operating without GPS, using satellite/UAV imagery for positioning | user clarification |
|
||||
| Slippy Map Coordinates | Tile X/Y indices in the Web Mercator projection grid (standard for web map tile servers) | data_model.md |
|
||||
| Version | Integer year (e.g., 2025) used to invalidate tile cache when Google Maps imagery is updated | data_model.md |
|
||||
|
||||
## Technical Terms
|
||||
|
||||
| Term | Definition | Source |
|
||||
|------|-----------|--------|
|
||||
| Region Request Queue | In-process bounded `Channel<Guid>` that decouples HTTP request submission from background processing | modules/services_region_request_queue.md |
|
||||
| Session Token | Provider-specific authentication token (e.g., Google Maps) embedded in tile download URLs; each provider may use different auth mechanisms | modules/services_google_maps_downloader.md |
|
||||
| ISatelliteDownloader | Interface abstracting satellite imagery providers; first implementation: Google Maps (GoogleMapsDownloaderV2) | modules/common_interfaces.md |
|
||||
| DbUp | .NET library for forward-only SQL schema migrations via numbered embedded scripts | modules/dataaccess_database_migrator.md |
|
||||
| Tile Deduplication | Mechanism using DB unique index + ConcurrentDictionary to prevent re-downloading identical tiles | modules/services_google_maps_downloader.md |
|
||||
|
||||
## Abbreviations
|
||||
|
||||
| Abbrev | Meaning |
|
||||
|--------|---------|
|
||||
| MGRS | Military Grid Reference System (endpoint planned, currently stub) |
|
||||
| UAV | Unmanned Aerial Vehicle |
|
||||
| NFR | Non-Functional Requirement |
|
||||
| DI | Dependency Injection |
|
||||
| DTO | Data Transfer Object |
|
||||
| CSV | Comma-Separated Values (tile manifest output format) |
|
||||
@@ -0,0 +1,140 @@
|
||||
# Module Layout
|
||||
|
||||
**Status**: derived-from-code
|
||||
|
||||
**Language**: csharp
|
||||
**Layout Convention**: custom (flat service project, per-project component separation)
|
||||
**Root**: ./
|
||||
**Last Updated**: 2026-05-10
|
||||
|
||||
## Layout Rules
|
||||
|
||||
1. Each component owns ONE top-level project directory (`.csproj` boundary), except the Services project which hosts three logical components in a flat layout.
|
||||
2. Shared code lives under `SatelliteProvider.Common/` — the foundation layer.
|
||||
3. Cross-cutting concerns (DTOs, interfaces, configs, geo-math) all reside in Common.
|
||||
4. Public API surface per component = `public` types in the namespace root. Everything marked `internal` or private is internal.
|
||||
5. Tests live in separate projects: `SatelliteProvider.Tests/` (unit) and `SatelliteProvider.IntegrationTests/` (integration).
|
||||
|
||||
## Per-Component Mapping
|
||||
|
||||
### Component: Common
|
||||
|
||||
- **Directory**: `SatelliteProvider.Common/`
|
||||
- **Public API**:
|
||||
- `SatelliteProvider.Common/Configs/MapConfig.cs`
|
||||
- `SatelliteProvider.Common/Configs/StorageConfig.cs`
|
||||
- `SatelliteProvider.Common/Configs/ProcessingConfig.cs`
|
||||
- `SatelliteProvider.Common/Configs/DatabaseConfig.cs`
|
||||
- `SatelliteProvider.Common/DTO/*.cs` (all DTOs)
|
||||
- `SatelliteProvider.Common/Interfaces/*.cs` (all service interfaces)
|
||||
- `SatelliteProvider.Common/Utils/GeoUtils.cs`
|
||||
- **Internal**: (none — all types are public, shared across components)
|
||||
- **Owns**: `SatelliteProvider.Common/**`
|
||||
- **Imports from**: (none)
|
||||
- **Consumed by**: DataAccess, TileDownloader, RegionProcessing, RouteManagement, WebApi
|
||||
|
||||
### Component: DataAccess
|
||||
|
||||
- **Directory**: `SatelliteProvider.DataAccess/`
|
||||
- **Public API**:
|
||||
- `SatelliteProvider.DataAccess/Models/TileEntity.cs`
|
||||
- `SatelliteProvider.DataAccess/Models/RegionEntity.cs`
|
||||
- `SatelliteProvider.DataAccess/Models/RouteEntity.cs`
|
||||
- `SatelliteProvider.DataAccess/Models/RoutePointEntity.cs`
|
||||
- `SatelliteProvider.DataAccess/Repositories/ITileRepository.cs`
|
||||
- `SatelliteProvider.DataAccess/Repositories/IRegionRepository.cs`
|
||||
- `SatelliteProvider.DataAccess/Repositories/IRouteRepository.cs`
|
||||
- `SatelliteProvider.DataAccess/Repositories/TileRepository.cs`
|
||||
- `SatelliteProvider.DataAccess/Repositories/RegionRepository.cs`
|
||||
- `SatelliteProvider.DataAccess/Repositories/RouteRepository.cs`
|
||||
- `SatelliteProvider.DataAccess/DatabaseMigrator.cs`
|
||||
- **Internal**: (none — all repository types are public for DI registration)
|
||||
- **Owns**: `SatelliteProvider.DataAccess/**`
|
||||
- **Imports from**: (none — fully self-contained, no project references)
|
||||
- **Consumed by**: TileDownloader, RegionProcessing, RouteManagement, WebApi
|
||||
|
||||
### Component: TileDownloader
|
||||
|
||||
- **Directory**: `SatelliteProvider.Services/` (shared project)
|
||||
- **Public API**:
|
||||
- `SatelliteProvider.Services/GoogleMapsDownloaderV2.cs` (implements `ISatelliteDownloader`)
|
||||
- `SatelliteProvider.Services/TileService.cs` (implements `ITileService`)
|
||||
- **Internal**: (none — flat project, classes are public)
|
||||
- **Owns**: `SatelliteProvider.Services/GoogleMapsDownloaderV2.cs`, `SatelliteProvider.Services/TileService.cs`
|
||||
- **Imports from**: Common, DataAccess
|
||||
- **Consumed by**: RegionProcessing, WebApi
|
||||
|
||||
### Component: RegionProcessing
|
||||
|
||||
- **Directory**: `SatelliteProvider.Services/` (shared project)
|
||||
- **Public API**:
|
||||
- `SatelliteProvider.Services/RegionService.cs` (implements `IRegionService`)
|
||||
- `SatelliteProvider.Services/RegionProcessingService.cs` (background hosted service)
|
||||
- `SatelliteProvider.Services/RegionRequestQueue.cs` (implements `IRegionRequestQueue`)
|
||||
- **Internal**: (none)
|
||||
- **Owns**: `SatelliteProvider.Services/RegionService.cs`, `SatelliteProvider.Services/RegionProcessingService.cs`, `SatelliteProvider.Services/RegionRequestQueue.cs`
|
||||
- **Imports from**: Common, DataAccess, TileDownloader
|
||||
- **Consumed by**: RouteManagement, WebApi
|
||||
|
||||
### Component: RouteManagement
|
||||
|
||||
- **Directory**: `SatelliteProvider.Services/` (shared project)
|
||||
- **Public API**:
|
||||
- `SatelliteProvider.Services/RouteService.cs` (implements `IRouteService`)
|
||||
- `SatelliteProvider.Services/RouteProcessingService.cs` (background hosted service)
|
||||
- **Internal**: (none)
|
||||
- **Owns**: `SatelliteProvider.Services/RouteService.cs`, `SatelliteProvider.Services/RouteProcessingService.cs`
|
||||
- **Imports from**: Common, DataAccess, RegionProcessing
|
||||
- **Consumed by**: WebApi
|
||||
|
||||
### Component: WebApi
|
||||
|
||||
- **Directory**: `SatelliteProvider.Api/`
|
||||
- **Public API**:
|
||||
- `SatelliteProvider.Api/Program.cs` (minimal API endpoints, DI setup)
|
||||
- **Internal**: (none)
|
||||
- **Owns**: `SatelliteProvider.Api/**`
|
||||
- **Imports from**: Common, DataAccess, TileDownloader, RegionProcessing, RouteManagement
|
||||
- **Consumed by**: (none — top-level entry point)
|
||||
|
||||
## Shared / Cross-Cutting
|
||||
|
||||
### Common/Configs
|
||||
|
||||
- **Directory**: `SatelliteProvider.Common/Configs/`
|
||||
- **Purpose**: Strongly-typed configuration POCOs bound via `IOptions<T>`
|
||||
- **Consumed by**: all components
|
||||
|
||||
### Common/DTO
|
||||
|
||||
- **Directory**: `SatelliteProvider.Common/DTO/`
|
||||
- **Purpose**: Data transfer objects shared across layers (request/response models, value types)
|
||||
- **Consumed by**: all components
|
||||
|
||||
### Common/Interfaces
|
||||
|
||||
- **Directory**: `SatelliteProvider.Common/Interfaces/`
|
||||
- **Purpose**: Service contracts enabling DI and testability
|
||||
- **Consumed by**: all components (services implement, API and consumers depend on)
|
||||
|
||||
### Common/Utils
|
||||
|
||||
- **Directory**: `SatelliteProvider.Common/Utils/`
|
||||
- **Purpose**: Stateless geospatial utility functions (coordinate math, distance, bearing)
|
||||
- **Consumed by**: TileDownloader, RegionProcessing, RouteManagement
|
||||
|
||||
## Allowed Dependencies (layering)
|
||||
|
||||
| Layer | Components | May import from |
|
||||
|-------|------------|-----------------|
|
||||
| 4. API / Entry | WebApi | 1, 2, 3 |
|
||||
| 3. Application (Orchestration) | RouteManagement | 1, 2, 3 (RegionProcessing only) |
|
||||
| 3. Application (Processing) | RegionProcessing | 1, 2, 3 (TileDownloader only) |
|
||||
| 2. Domain Services | TileDownloader | 1 |
|
||||
| 1. Foundation | Common, DataAccess | Common: (none); DataAccess: (none) |
|
||||
|
||||
## Verification Needed
|
||||
|
||||
- **Shared Services project**: TileDownloader, RegionProcessing, and RouteManagement coexist in a single `SatelliteProvider.Services/` project. File-level ownership is used (not directory-level) which is unusual for .NET. A future refactor into separate projects per component would make ownership boundaries cleaner.
|
||||
- **No detected cycles**: The dependency graph is a clean DAG.
|
||||
- **DataAccess layer placement**: DataAccess is placed at Layer 1 (Foundation) alongside Common because it is consumed uniformly by all service components. An alternative layering could place it at Layer 2, but the current code treats repositories as infrastructure, not domain logic.
|
||||
@@ -0,0 +1,88 @@
|
||||
# Module: Api/Program.cs
|
||||
|
||||
## Purpose
|
||||
Application entry point. Configures DI container, sets up middleware, defines minimal API endpoints, runs database migrations on startup, and starts background services.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### API Endpoints
|
||||
| Method | Route | Handler | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| GET | `/tiles/{z}/{x}/{y}` | `ServeTile` | Slippy map tile server with in-memory caching |
|
||||
| GET | `/api/satellite/tiles/latlon` | `GetTileByLatLon` | Download single tile by lat/lon/zoom |
|
||||
| GET | `/api/satellite/tiles/mgrs` | `GetSatelliteTilesByMgrs` | MGRS stub (returns empty) |
|
||||
| POST | `/api/satellite/upload` | `UploadImage` | Image upload stub (returns `Success: false`) |
|
||||
| POST | `/api/satellite/request` | `RequestRegion` | Queue region for async tile processing |
|
||||
| GET | `/api/satellite/region/{id}` | `GetRegionStatus` | Get region processing status |
|
||||
| POST | `/api/satellite/route` | `CreateRoute` | Create route with intermediate points |
|
||||
| GET | `/api/satellite/route/{id}` | `GetRoute` | Get route with all points |
|
||||
|
||||
### Local Records (defined in Program.cs)
|
||||
- `GetSatelliteTilesResponse`, `SatelliteTile` — MGRS response stubs
|
||||
- `UploadImageRequest` — multipart form data request
|
||||
- `SaveResult` — upload response stub
|
||||
- `DownloadTileResponse` — tile download response
|
||||
- `RequestRegionRequest` — region request body
|
||||
- `ParameterDescriptionFilter` — Swagger operation filter
|
||||
|
||||
## Internal Logic
|
||||
|
||||
### DI Registration
|
||||
1. Serilog configured from `appsettings.json`
|
||||
2. Connection string extracted from `ConnectionStrings:DefaultConnection`
|
||||
3. Config bindings: `MapConfig`, `StorageConfig`, `ProcessingConfig`
|
||||
4. Singletons: repositories (`TileRepository`, `RegionRepository`, `RouteRepository`), `GoogleMapsDownloaderV2`, `ITileService`, `IRegionService`, `IRouteService`
|
||||
5. `IRegionRequestQueue` with configurable capacity
|
||||
6. Hosted services: `RegionProcessingService`, `RouteProcessingService`
|
||||
7. CORS policy: `TilesCors` — configured origins from `CorsConfig:AllowedOrigins`, falls back to allow-any
|
||||
8. JSON options: camelCase, case-insensitive
|
||||
|
||||
### Startup
|
||||
1. Database migration via `DatabaseMigrator.RunMigrations()` — throws on failure
|
||||
2. Creates tiles and ready directories
|
||||
3. Swagger enabled in Development mode
|
||||
4. HTTPS redirection, CORS applied
|
||||
|
||||
### ServeTile Handler
|
||||
1. Checks `IMemoryCache` for tile bytes (1h absolute, 30min sliding expiration)
|
||||
2. If cache miss: queries `ITileRepository.GetByTileCoordinatesAsync`
|
||||
3. If no DB record: downloads tile via `GoogleMapsDownloaderV2.DownloadSingleTileAsync`, creates `TileEntity`, inserts
|
||||
4. Returns image bytes with cache headers (`Cache-Control: public, max-age=86400`)
|
||||
|
||||
### GetTileByLatLon Handler
|
||||
Downloads a tile, persists it, returns metadata as `DownloadTileResponse`.
|
||||
|
||||
### RequestRegion Handler
|
||||
Validates size (100–10000m), delegates to `IRegionService.RequestRegionAsync`.
|
||||
|
||||
## Dependencies
|
||||
All project references: Common, DataAccess, Services.
|
||||
NuGet: `Serilog.AspNetCore`, `Swashbuckle.AspNetCore`, `Microsoft.AspNetCore.OpenApi`, `SixLabors.ImageSharp`, `Newtonsoft.Json`.
|
||||
|
||||
## Consumers
|
||||
- HTTP clients (external)
|
||||
- Integration tests (via HTTP)
|
||||
|
||||
## Data Models
|
||||
Defines several local request/response records that are not shared with other projects.
|
||||
|
||||
## Configuration
|
||||
All configuration sections are consumed here:
|
||||
- `ConnectionStrings:DefaultConnection`
|
||||
- `MapConfig`, `StorageConfig`, `ProcessingConfig`
|
||||
- `CorsConfig:AllowedOrigins`
|
||||
- `Serilog` section
|
||||
|
||||
## External Integrations
|
||||
- Google Maps (indirectly via `GoogleMapsDownloaderV2`)
|
||||
- PostgreSQL (via repositories and DatabaseMigrator)
|
||||
- File system (`./tiles/`, `./ready/`)
|
||||
|
||||
## Security
|
||||
- CORS configured (permissive by default when no origins specified)
|
||||
- Swagger only in Development
|
||||
- HTTPS redirection enabled
|
||||
- No authentication/authorization implemented
|
||||
|
||||
## Tests
|
||||
Integration tests exercise all endpoints. Unit test project has only a dummy test.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Module: Common/Configs
|
||||
|
||||
## Purpose
|
||||
Configuration POCOs that bind to `appsettings.json` sections via `IOptions<T>` pattern.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### MapConfig
|
||||
- `Service` (string): map provider name (e.g., "GoogleMaps")
|
||||
- `ApiKey` (string): API key for the map provider
|
||||
|
||||
### StorageConfig
|
||||
- `TilesDirectory` (string): base path for tile storage (default: `/tiles`)
|
||||
- `ReadyDirectory` (string): base path for output files (default: `/ready`)
|
||||
- `GetTileSubdirectoryPath(int zoomLevel, int tileX, int tileY) → string`: computes bucketed subdirectory path (`{tiles}/{zoom}/{xBucket}/{yBucket}`) using integer division by 1000
|
||||
- `GetTileFilePath(int zoomLevel, int tileX, int tileY, string timestamp) → string`: computes full file path with timestamped filename (`tile_{z}_{x}_{y}_{ts}.jpg`)
|
||||
|
||||
### ProcessingConfig
|
||||
- `MaxConcurrentDownloads` (int, default: 4): semaphore limit for parallel tile downloads
|
||||
- `MaxConcurrentRegions` (int, default: 3): parallel region processing workers
|
||||
- `DefaultZoomLevel` (int, default: 20): fallback zoom level
|
||||
- `QueueCapacity` (int, default: 100): bounded channel capacity for region request queue
|
||||
- `DelayBetweenRequestsMs` (int, default: 50): throttle delay between Google Maps requests
|
||||
- `SessionTokenReuseCount` (int, default: 100): tiles per session token before rotation
|
||||
|
||||
### DatabaseConfig
|
||||
- `ConnectionString` (string): DB connection string (unused — connection string is resolved directly from `IConfiguration` in `Program.cs`)
|
||||
|
||||
## Internal Logic
|
||||
`StorageConfig.GetTileSubdirectoryPath` buckets tiles by dividing X/Y coordinates by 1000, preventing filesystem performance degradation from too many files in one directory.
|
||||
|
||||
## Dependencies
|
||||
None (pure POCOs, no internal imports).
|
||||
|
||||
## Consumers
|
||||
- `Program.cs` — binds from config sections via `builder.Services.Configure<T>()`
|
||||
- `GoogleMapsDownloaderV2` — reads `MapConfig`, `StorageConfig`, `ProcessingConfig` via `IOptions<T>`
|
||||
- `RegionService` — reads `StorageConfig`
|
||||
- `RegionProcessingService` — reads `ProcessingConfig`
|
||||
- `RouteProcessingService` — reads `StorageConfig`
|
||||
- `RegionRequestQueue` — receives `QueueCapacity` as constructor param
|
||||
|
||||
## Data Models
|
||||
No domain entities; these are configuration DTOs.
|
||||
|
||||
## Configuration
|
||||
These classes **define** the configuration shape consumed by all services.
|
||||
|
||||
| Config Class | appsettings Section |
|
||||
|-------------|-------------------|
|
||||
| MapConfig | `MapConfig` |
|
||||
| StorageConfig | `StorageConfig` |
|
||||
| ProcessingConfig | `ProcessingConfig` |
|
||||
| DatabaseConfig | (not wired — connection string read directly) |
|
||||
|
||||
## External Integrations
|
||||
None.
|
||||
|
||||
## Security
|
||||
`MapConfig.ApiKey` holds the Google Maps API key. In production, injected via environment variable `MapConfig__ApiKey`.
|
||||
|
||||
## Tests
|
||||
No dedicated tests.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Module: Common/DTO
|
||||
|
||||
## Purpose
|
||||
Data transfer objects used across all layers — API requests/responses, inter-service communication, and queue messages.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### GeoPoint
|
||||
Geographic coordinate with tolerance-based equality.
|
||||
- `Lat` (double): latitude, JSON property `"lat"`
|
||||
- `Lon` (double): longitude, JSON property `"lon"`
|
||||
- Constructor: `GeoPoint()`, `GeoPoint(double lat, double lon)`
|
||||
- Equality: two points are equal if both coordinates differ by less than `0.00005` (PRECISION_TOLERANCE)
|
||||
- Operator overloads: `==`, `!=`
|
||||
|
||||
### Direction
|
||||
Result of a directional calculation between two points.
|
||||
- `Distance` (double): distance in meters
|
||||
- `Azimuth` (double): bearing in degrees (0–360)
|
||||
|
||||
### SatTile
|
||||
Represents a single map tile with its spatial bounds.
|
||||
- `X`, `Y` (int): tile coordinates in the slippy map scheme
|
||||
- `Zoom` (int): zoom level
|
||||
- `LeftTop`, `BottomRight` (GeoPoint): computed bounding box corners (via `GeoUtils.TileToWorldPos`)
|
||||
- `Url` (string): download URL
|
||||
- `FileName → string`: formatted as `{X}.{Y}.{Zoom}.jpg`
|
||||
|
||||
### TileMetadata
|
||||
Metadata about a stored tile (mirrors `TileEntity` but without DB-specific concerns).
|
||||
- `Id` (Guid), `TileZoom`, `TileX`, `TileY` (int), `Latitude`, `Longitude` (double)
|
||||
- `TileSizeMeters` (double), `TileSizePixels` (int), `ImageType` (string)
|
||||
- `MapsVersion` (string?), `Version` (int), `FilePath` (string)
|
||||
- `CreatedAt`, `UpdatedAt` (DateTime)
|
||||
|
||||
### RegionRequest
|
||||
Queue message for async region processing.
|
||||
- `Id` (Guid), `Latitude`, `Longitude` (double), `SizeMeters` (double)
|
||||
- `ZoomLevel` (int), `StitchTiles` (bool)
|
||||
|
||||
### RegionStatus
|
||||
Response DTO for region status queries.
|
||||
- `Id` (Guid), `Status` (string), `CsvFilePath`, `SummaryFilePath` (string?)
|
||||
- `TilesDownloaded`, `TilesReused` (int), `CreatedAt`, `UpdatedAt` (DateTime)
|
||||
|
||||
### RoutePoint
|
||||
Input point in a route creation request.
|
||||
- `Latitude` (double, JSON: `"lat"`), `Longitude` (double, JSON: `"lon"`)
|
||||
|
||||
### RoutePointDto
|
||||
Output point in a route response (includes computed fields).
|
||||
- `Latitude`, `Longitude` (double), `PointType` (string: "start"/"end"/"action"/"intermediate")
|
||||
- `SequenceNumber`, `SegmentIndex` (int), `DistanceFromPrevious` (double?)
|
||||
|
||||
### CreateRouteRequest
|
||||
API request body for route creation.
|
||||
- `Id` (Guid), `Name` (string), `Description` (string?)
|
||||
- `RegionSizeMeters` (double), `ZoomLevel` (int)
|
||||
- `Points` (List\<RoutePoint\>), `Geofences` (Geofences?)
|
||||
- `RequestMaps` (bool), `CreateTilesZip` (bool)
|
||||
|
||||
### RouteResponse
|
||||
API response for route queries.
|
||||
- All fields from the route entity plus `Points` (List\<RoutePointDto\>)
|
||||
- `MapsReady` (bool), `TilesZipPath` (string?)
|
||||
|
||||
### GeofencePolygon
|
||||
Axis-aligned bounding box defined by NW and SE corners.
|
||||
- `NorthWest` (GeoPoint?), `SouthEast` (GeoPoint?)
|
||||
|
||||
### Geofences
|
||||
Container for multiple geofence polygons.
|
||||
- `Polygons` (List\<GeofencePolygon\>)
|
||||
|
||||
## Internal Logic
|
||||
- `GeoPoint` uses a precision tolerance of `0.00005` degrees (~5.5 meters) for equality comparison.
|
||||
- `SatTile` eagerly computes its bounding box corners on construction by calling `GeoUtils.TileToWorldPos`.
|
||||
|
||||
## Dependencies
|
||||
- `GeoPoint`, `Direction` — no imports
|
||||
- `SatTile` → `SatelliteProvider.Common.Utils.GeoUtils`
|
||||
- All others — no internal dependencies (or only `System.Text.Json.Serialization`)
|
||||
|
||||
## Consumers
|
||||
- All services, repositories, and API endpoints consume these DTOs
|
||||
- `RegionRequest` is the message type for `IRegionRequestQueue`
|
||||
|
||||
## Data Models
|
||||
These ARE the data models (DTOs). They map closely to the database entities but are decoupled from the persistence layer.
|
||||
|
||||
## Configuration
|
||||
None consumed directly.
|
||||
|
||||
## External Integrations
|
||||
None.
|
||||
|
||||
## Security
|
||||
None.
|
||||
|
||||
## Tests
|
||||
No dedicated DTO tests.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Module: Common/Utils/GeoUtils
|
||||
|
||||
## Purpose
|
||||
Static geographic computation utilities: coordinate conversions, distance calculations (Haversine), bearing computation, point interpolation, and bounding box calculation.
|
||||
|
||||
## Public Interface
|
||||
|
||||
All methods are static on `GeoUtils`:
|
||||
|
||||
- `WorldToTilePos(GeoPoint point, int zoom) → (int x, int y)`: converts lat/lon to slippy map tile coordinates at given zoom
|
||||
- `TileToWorldPos(int x, int y, int zoom) → GeoPoint`: converts tile coordinates back to lat/lon (NW corner of tile)
|
||||
- `ToRadians(double degrees) → double`
|
||||
- `ToDegrees(double radians) → double`
|
||||
- `DirectionTo(this GeoPoint p1, GeoPoint p2) → Direction` (extension method): Haversine distance + forward azimuth between two points
|
||||
- `GoDirection(this GeoPoint startPoint, Direction direction) → GeoPoint` (extension method): destination point given start + bearing + distance
|
||||
- `GetBoundingBox(GeoPoint center, double radiusM) → (minLat, maxLat, minLon, maxLon)`: axis-aligned bounding box around a center point
|
||||
- `CalculateIntermediatePoints(GeoPoint start, GeoPoint end, double maxSpacingMeters) → List<GeoPoint>`: generates evenly-spaced points along a great-circle path (returns empty if distance ≤ maxSpacing)
|
||||
- `CalculateDistance(GeoPoint p1, GeoPoint p2) → double`: convenience wrapper around `DirectionTo().Distance`
|
||||
- `CalculateCenter(GeoPoint northWest, GeoPoint southEast) → GeoPoint`: simple midpoint
|
||||
- `CalculatePolygonDiagonalDistance(GeoPoint northWest, GeoPoint southEast) → double`: diagonal distance of a bounding box
|
||||
|
||||
## Internal Logic
|
||||
- Earth radius constant: `6378137` meters (WGS-84 semi-major axis)
|
||||
- Distance calculation uses the Haversine formula
|
||||
- Bearing uses `atan2` of longitude/latitude components
|
||||
- `CalculateIntermediatePoints` divides the segment into equal sub-segments, each ≤ `maxSpacingMeters`
|
||||
- Tile conversion follows the standard Web Mercator / slippy map tile numbering scheme
|
||||
|
||||
## Dependencies
|
||||
- `SatelliteProvider.Common.DTO.GeoPoint`
|
||||
- `SatelliteProvider.Common.DTO.Direction`
|
||||
|
||||
## Consumers
|
||||
- `GoogleMapsDownloaderV2` — `WorldToTilePos`, `TileToWorldPos`, `GetBoundingBox`
|
||||
- `TileService` — indirectly via downloader
|
||||
- `RegionService` — `WorldToTilePos` for tile stitching
|
||||
- `RouteService` — `CalculateIntermediatePoints`, `CalculateDistance`
|
||||
- `RouteProcessingService` — `WorldToTilePos` for map stitching
|
||||
- `SatTile` constructor — `TileToWorldPos`
|
||||
- `Program.cs` (ServeTile handler) — `TileToWorldPos`
|
||||
|
||||
## Data Models
|
||||
None.
|
||||
|
||||
## Configuration
|
||||
None.
|
||||
|
||||
## External Integrations
|
||||
None (pure math).
|
||||
|
||||
## Security
|
||||
None.
|
||||
|
||||
## Tests
|
||||
No dedicated unit tests for GeoUtils.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Module: Common/Interfaces
|
||||
|
||||
## Purpose
|
||||
Service contracts defining the application's core operations. Implementations live in `SatelliteProvider.Services`.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### ITileService
|
||||
- `DownloadAndStoreTilesAsync(double lat, double lon, double sizeMeters, int zoomLevel, CancellationToken) → Task<List<TileMetadata>>`: downloads missing tiles for a region and returns all tile metadata (existing + new)
|
||||
- `GetTileAsync(Guid id) → Task<TileMetadata?>`: retrieve a single tile by ID
|
||||
- `GetTilesByRegionAsync(double lat, double lon, double sizeMeters, int zoomLevel) → Task<IEnumerable<TileMetadata>>`: query tiles within a geographic region
|
||||
|
||||
### IRegionService
|
||||
- `RequestRegionAsync(Guid id, double lat, double lon, double sizeMeters, int zoomLevel, bool stitchTiles) → Task<RegionStatus>`: creates a region record and enqueues for async processing
|
||||
- `GetRegionStatusAsync(Guid id) → Task<RegionStatus?>`: retrieves current status of a region request
|
||||
- `ProcessRegionAsync(Guid id, CancellationToken) → Task`: executes tile downloading, CSV/summary generation, optional stitching
|
||||
|
||||
### IRouteService
|
||||
- `CreateRouteAsync(CreateRouteRequest request) → Task<RouteResponse>`: validates input, calculates intermediate points, persists route + points, optionally creates geofence regions
|
||||
- `GetRouteAsync(Guid id) → Task<RouteResponse?>`: retrieves route with all points
|
||||
|
||||
### ISatelliteDownloader
|
||||
- `GetTiles(GeoPoint geoPoint, double radiusM, int zoomLevel, CancellationToken) → Task`: legacy interface for tile downloading (not directly implemented by `GoogleMapsDownloaderV2`)
|
||||
|
||||
### IRegionRequestQueue
|
||||
- `EnqueueAsync(RegionRequest request, CancellationToken) → ValueTask`: add region request to the bounded queue
|
||||
- `DequeueAsync(CancellationToken) → ValueTask<RegionRequest?>`: consume next request (blocks until available)
|
||||
- `Count` (int): current queue depth
|
||||
|
||||
## Internal Logic
|
||||
Pure interface definitions — no logic.
|
||||
|
||||
## Dependencies
|
||||
- All interfaces reference DTOs from `SatelliteProvider.Common.DTO`
|
||||
|
||||
## Consumers
|
||||
- `Program.cs` — DI registration of implementations
|
||||
- `RegionProcessingService` — consumes `IRegionRequestQueue` and `IRegionService`
|
||||
- `RouteService` — consumes `IRegionService` (for geofence region creation)
|
||||
- `RouteProcessingService` — consumes `IRegionService` via service provider scope
|
||||
- API endpoints — consume `ITileService`, `IRegionService`, `IRouteService`
|
||||
|
||||
## Data Models
|
||||
None defined here.
|
||||
|
||||
## Configuration
|
||||
None.
|
||||
|
||||
## External Integrations
|
||||
None.
|
||||
|
||||
## Security
|
||||
None.
|
||||
|
||||
## Tests
|
||||
No dedicated interface tests.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Module: DataAccess/DatabaseMigrator
|
||||
|
||||
## Purpose
|
||||
Runs DbUp-based SQL migrations against PostgreSQL on application startup. Ensures the database schema is up to date before the API begins serving requests.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### DatabaseMigrator
|
||||
- Constructor: `DatabaseMigrator(string connectionString, ILogger<DatabaseMigrator>? logger)`
|
||||
- `RunMigrations() → bool`: creates the database if missing (`EnsureDatabase.For.PostgresqlDatabase`), then runs all embedded SQL scripts matching `.Migrations.` from the DataAccess assembly. Returns `true` on success.
|
||||
|
||||
## Internal Logic
|
||||
- Uses `DbUp.DeployChanges` fluent API targeting PostgreSQL
|
||||
- Scripts are embedded resources filtered by path containing `.Migrations.`
|
||||
- Logs to console via DbUp's built-in `LogToConsole()`
|
||||
- On failure, logs the error and returns `false`
|
||||
|
||||
## Dependencies
|
||||
- NuGet: `dbup-postgresql` (6.0.3)
|
||||
- `Microsoft.Extensions.Logging`
|
||||
- Embedded SQL resources from `SatelliteProvider.DataAccess/Migrations/`
|
||||
|
||||
## Consumers
|
||||
- `Program.cs` — instantiated directly (not via DI) and called during startup. If migration fails, the application throws and does not start.
|
||||
|
||||
## Migrations (11 scripts)
|
||||
1. `001_CreateTilesTable.sql`
|
||||
2. `002_CreateRegionsTable.sql`
|
||||
3. `003_CreateIndexes.sql`
|
||||
4. `004_AddVersionColumn.sql`
|
||||
5. `005_CreateRoutesTables.sql`
|
||||
6. `006_AddStitchTilesToRegions.sql`
|
||||
7. `007_AddRouteMapFields.sql`
|
||||
8. `008_AddGeofenceFlagToRouteRegions.sql`
|
||||
9. `009_AddGeofencePolygonIndex.sql`
|
||||
10. `010_AddTilesZipToRoutes.sql`
|
||||
11. `011_AddTileCoordinates.sql`
|
||||
|
||||
## Configuration
|
||||
Receives connection string directly as constructor parameter.
|
||||
|
||||
## External Integrations
|
||||
PostgreSQL — DDL operations via DbUp.
|
||||
|
||||
## Security
|
||||
None directly, but controls schema evolution.
|
||||
|
||||
## Tests
|
||||
No dedicated tests.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Module: DataAccess/Models
|
||||
|
||||
## Purpose
|
||||
Database entity classes that map directly to PostgreSQL tables via Dapper. Property names use PascalCase; column mapping is done with SQL aliases in repository queries.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### TileEntity
|
||||
Maps to `tiles` table.
|
||||
- `Id` (Guid), `TileZoom` (int), `TileX` (int), `TileY` (int)
|
||||
- `Latitude`, `Longitude` (double), `TileSizeMeters` (double), `TileSizePixels` (int)
|
||||
- `ImageType` (string), `MapsVersion` (string?), `Version` (int)
|
||||
- `FilePath` (string), `CreatedAt`, `UpdatedAt` (DateTime)
|
||||
|
||||
### RegionEntity
|
||||
Maps to `regions` table.
|
||||
- `Id` (Guid), `Latitude`, `Longitude` (double), `SizeMeters` (double)
|
||||
- `ZoomLevel` (int), `Status` (string: "queued"/"processing"/"completed"/"failed")
|
||||
- `CsvFilePath`, `SummaryFilePath` (string?)
|
||||
- `TilesDownloaded`, `TilesReused` (int), `StitchTiles` (bool)
|
||||
- `CreatedAt`, `UpdatedAt` (DateTime)
|
||||
|
||||
### RouteEntity
|
||||
Maps to `routes` table.
|
||||
- `Id` (Guid), `Name` (string), `Description` (string?)
|
||||
- `RegionSizeMeters` (double), `ZoomLevel` (int)
|
||||
- `TotalDistanceMeters` (double), `TotalPoints` (int)
|
||||
- `RequestMaps`, `MapsReady`, `CreateTilesZip` (bool)
|
||||
- `CsvFilePath`, `SummaryFilePath`, `StitchedImagePath`, `TilesZipPath` (string?)
|
||||
- `CreatedAt`, `UpdatedAt` (DateTime)
|
||||
|
||||
### RoutePointEntity
|
||||
Maps to `route_points` table.
|
||||
- `Id` (Guid), `RouteId` (Guid), `SequenceNumber` (int)
|
||||
- `Latitude`, `Longitude` (double), `PointType` (string)
|
||||
- `SegmentIndex` (int), `DistanceFromPrevious` (double?)
|
||||
- `CreatedAt` (DateTime)
|
||||
|
||||
## Internal Logic
|
||||
Plain POCOs with no logic.
|
||||
|
||||
## Dependencies
|
||||
None.
|
||||
|
||||
## Consumers
|
||||
- All repository implementations (TileRepository, RegionRepository, RouteRepository)
|
||||
- `TileService` — creates `TileEntity` instances for persistence
|
||||
- `RegionService` — creates/updates `RegionEntity`
|
||||
- `RouteService` — creates `RouteEntity` and `RoutePointEntity`
|
||||
- `RouteProcessingService` — reads entities from repositories
|
||||
- `GoogleMapsDownloaderV2.GetTilesWithMetadataAsync` — accepts `IEnumerable<TileEntity>` to check existing tiles
|
||||
|
||||
## Data Models
|
||||
These ARE the data model.
|
||||
|
||||
## Configuration
|
||||
None.
|
||||
|
||||
## External Integrations
|
||||
None.
|
||||
|
||||
## Security
|
||||
None.
|
||||
|
||||
## Tests
|
||||
No dedicated tests.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Module: DataAccess/Repositories/RegionRepository
|
||||
|
||||
## Purpose
|
||||
Dapper-based repository for the `regions` table. Tracks region processing requests and their status lifecycle.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### IRegionRepository (interface)
|
||||
- `GetByIdAsync(Guid id) → Task<RegionEntity?>`
|
||||
- `GetByStatusAsync(string status) → Task<IEnumerable<RegionEntity>>`: retrieves all regions with a given status, ordered by creation date ASC
|
||||
- `InsertAsync(RegionEntity region) → Task<Guid>`
|
||||
- `UpdateAsync(RegionEntity region) → Task<int>`
|
||||
- `DeleteAsync(Guid id) → Task<int>`
|
||||
|
||||
### RegionRepository (implementation)
|
||||
Same connection-per-call pattern as TileRepository.
|
||||
|
||||
## Internal Logic
|
||||
Standard CRUD. `GetByStatusAsync` orders by `created_at ASC` to process oldest requests first.
|
||||
|
||||
## Dependencies
|
||||
- NuGet: `Dapper`, `Npgsql`
|
||||
- `SatelliteProvider.DataAccess.Models.RegionEntity`
|
||||
- `Microsoft.Extensions.Logging`
|
||||
|
||||
## Consumers
|
||||
- `RegionService` — insert on request, update during processing
|
||||
- `RouteProcessingService` — reads region records to check status and get CSV paths
|
||||
|
||||
## Data Models
|
||||
Operates on `RegionEntity`.
|
||||
|
||||
## Configuration
|
||||
Connection string via constructor.
|
||||
|
||||
## External Integrations
|
||||
PostgreSQL.
|
||||
|
||||
## Security
|
||||
None.
|
||||
|
||||
## Tests
|
||||
No dedicated tests.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Module: DataAccess/Repositories/RouteRepository
|
||||
|
||||
## Purpose
|
||||
Dapper-based repository for the `routes`, `route_points`, and `route_regions` tables. Handles route persistence, point storage, and route-region linking (including geofence metadata).
|
||||
|
||||
## Public Interface
|
||||
|
||||
### IRouteRepository (interface)
|
||||
- `GetByIdAsync(Guid id) → Task<RouteEntity?>`
|
||||
- `GetRoutePointsAsync(Guid routeId) → Task<IEnumerable<RoutePointEntity>>`: ordered by `sequence_number`
|
||||
- `InsertRouteAsync(RouteEntity route) → Task<Guid>`
|
||||
- `InsertRoutePointsAsync(IEnumerable<RoutePointEntity> points) → Task`: bulk insert
|
||||
- `UpdateRouteAsync(RouteEntity route) → Task<int>`
|
||||
- `DeleteRouteAsync(Guid id) → Task<int>`
|
||||
- `LinkRouteToRegionAsync(Guid routeId, Guid regionId, bool isGeofence, int? geofencePolygonIndex) → Task`: inserts into `route_regions` with `ON CONFLICT DO NOTHING`
|
||||
- `GetRegionIdsByRouteAsync(Guid routeId) → Task<IEnumerable<Guid>>`: non-geofence region IDs
|
||||
- `GetGeofenceRegionIdsByRouteAsync(Guid routeId) → Task<IEnumerable<Guid>>`: geofence-only region IDs
|
||||
- `GetGeofenceRegionsByPolygonAsync(Guid routeId) → Task<Dictionary<int, List<Guid>>>`: groups geofence regions by polygon index
|
||||
- `GetRoutesWithPendingMapsAsync() → Task<IEnumerable<RouteEntity>>`: routes where `request_maps = true AND maps_ready = false`
|
||||
|
||||
### RouteRepository (implementation)
|
||||
Same connection-per-call pattern. `InsertRoutePointsAsync` uses Dapper's bulk execute to insert all points in a single round-trip.
|
||||
|
||||
## Internal Logic
|
||||
- `LinkRouteToRegionAsync` uses `ON CONFLICT DO NOTHING` to handle duplicate links gracefully
|
||||
- `GetGeofenceRegionsByPolygonAsync` groups results into a dictionary keyed by `geofence_polygon_index`
|
||||
- `GetRoutesWithPendingMapsAsync` drives the `RouteProcessingService` polling loop
|
||||
|
||||
## Dependencies
|
||||
- NuGet: `Dapper`, `Npgsql`
|
||||
- `SatelliteProvider.DataAccess.Models.RouteEntity`, `RoutePointEntity`
|
||||
- `Microsoft.Extensions.Logging`
|
||||
|
||||
## Consumers
|
||||
- `RouteService` — insert route, points, link regions
|
||||
- `RouteProcessingService` — read route state, points, region links; update route after map generation
|
||||
|
||||
## Data Models
|
||||
Operates on `RouteEntity`, `RoutePointEntity`, and the `route_regions` junction table.
|
||||
|
||||
## Configuration
|
||||
Connection string via constructor.
|
||||
|
||||
## External Integrations
|
||||
PostgreSQL.
|
||||
|
||||
## Security
|
||||
None.
|
||||
|
||||
## Tests
|
||||
No dedicated tests.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Module: DataAccess/Repositories/TileRepository
|
||||
|
||||
## Purpose
|
||||
Dapper-based repository for the `tiles` table. Handles CRUD operations and spatial queries for satellite tile records.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### ITileRepository (interface)
|
||||
- `GetByIdAsync(Guid id) → Task<TileEntity?>`
|
||||
- `GetByTileCoordinatesAsync(int tileZoom, int tileX, int tileY) → Task<TileEntity?>`: finds tile by slippy map coordinates, returns latest version
|
||||
- `FindExistingTileAsync(double lat, double lon, double tileSizeMeters, int zoomLevel, int version) → Task<TileEntity?>`: fuzzy coordinate match (tolerance: 0.0001° lat/lon, 1m tile size)
|
||||
- `GetTilesByRegionAsync(double lat, double lon, double sizeMeters, int zoomLevel) → Task<IEnumerable<TileEntity>>`: spatial bounding box query with expanded range to cover tile edges
|
||||
- `InsertAsync(TileEntity tile) → Task<Guid>`: upsert — `ON CONFLICT (latitude, longitude, tile_zoom, tile_size_meters, version) DO UPDATE` file_path, tile_x, tile_y, updated_at
|
||||
- `UpdateAsync(TileEntity tile) → Task<int>`
|
||||
- `DeleteAsync(Guid id) → Task<int>`
|
||||
|
||||
### TileRepository (implementation)
|
||||
Constructs a new `NpgsqlConnection` per method call (no connection pooling at the repository level; Npgsql pools connections internally).
|
||||
|
||||
## Internal Logic
|
||||
- `GetTilesByRegionAsync` calculates a bounding box by expanding the requested region by 2 × tile size to ensure edge tiles are included. Uses meters-to-degrees approximation (111,000 m/degree latitude, adjusted for longitude).
|
||||
- `InsertAsync` uses an upsert pattern to handle duplicate tile downloads gracefully.
|
||||
- `GetByTileCoordinatesAsync` orders by `version DESC` and takes the latest.
|
||||
|
||||
## Dependencies
|
||||
- NuGet: `Dapper`, `Npgsql`
|
||||
- `SatelliteProvider.DataAccess.Models.TileEntity`
|
||||
- `Microsoft.Extensions.Logging`
|
||||
|
||||
## Consumers
|
||||
- `TileService` — all read/write operations
|
||||
- `Program.cs` (ServeTile, GetTileByLatLon handlers) — `GetByTileCoordinatesAsync`, `InsertAsync`
|
||||
|
||||
## Data Models
|
||||
Operates on `TileEntity`.
|
||||
|
||||
## Configuration
|
||||
Receives connection string via constructor.
|
||||
|
||||
## External Integrations
|
||||
PostgreSQL — SQL queries via Dapper + Npgsql.
|
||||
|
||||
## Security
|
||||
None.
|
||||
|
||||
## Tests
|
||||
No dedicated repository tests.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Module: Services/GoogleMapsDownloaderV2
|
||||
|
||||
## Purpose
|
||||
Downloads satellite imagery tiles from Google Maps. Handles session token management, concurrent download throttling, retry logic with exponential backoff, and tile deduplication.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### GoogleMapsDownloaderV2
|
||||
- Constructor: `GoogleMapsDownloaderV2(ILogger, IOptions<MapConfig>, IOptions<StorageConfig>, IOptions<ProcessingConfig>, IHttpClientFactory)`
|
||||
- `DownloadSingleTileAsync(double lat, double lon, int zoomLevel, CancellationToken) → Task<DownloadedTileInfoV2>`: downloads one tile at specified coordinates. Validates zoom level, creates session token, downloads image, saves to disk.
|
||||
- `GetTilesWithMetadataAsync(GeoPoint center, double radiusM, int zoomLevel, IEnumerable<TileEntity> existingTiles, CancellationToken) → Task<List<DownloadedTileInfoV2>>`: downloads all tiles in a bounding box, skipping those already present in `existingTiles`. Manages session token rotation.
|
||||
|
||||
### DownloadedTileInfoV2 (record)
|
||||
- `X`, `Y` (int), `ZoomLevel` (int), `CenterLatitude`, `CenterLongitude` (double), `FilePath` (string), `TileSizeMeters` (double)
|
||||
|
||||
### RateLimitException (exception)
|
||||
Custom exception thrown when Google Maps returns 429 Too Many Requests and retries are exhausted.
|
||||
|
||||
## Internal Logic
|
||||
- **Allowed zoom levels**: 15, 16, 17, 18, 19 — throws `ArgumentException` for others
|
||||
- **URL template**: `https://mt{server}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}&token={token}`
|
||||
- **Session tokens**: obtained via `https://tile.googleapis.com/v1/createSession?key={apiKey}`, rotated every `SessionTokenReuseCount` tiles (default: 100)
|
||||
- **Concurrency control**: `SemaphoreSlim` limits parallel downloads to `MaxConcurrentDownloads` (default: 4)
|
||||
- **Deduplication**: `ConcurrentDictionary<string, Task<DownloadedTileInfoV2>>` (`_activeDownloads`) prevents duplicate concurrent downloads of the same tile
|
||||
- **Retry logic**: exponential backoff (1s base, 30s max, 5 retries) for 429 and 5xx errors. Cancellation and auth errors (401, 403) propagate immediately.
|
||||
- **Server selection**: `(x + y) % 4` distributes requests across `mt0`–`mt3`; single-tile downloads always use `mt0`
|
||||
- **Delay between requests**: configurable via `ProcessingConfig.DelayBetweenRequestsMs`
|
||||
- **Tile size calculation**: `CalculateTileSizeInMeters` uses Earth circumference × cos(latitude) / (2^zoom × 256)
|
||||
|
||||
## Dependencies
|
||||
- `SatelliteProvider.Common.Configs` — MapConfig, StorageConfig, ProcessingConfig
|
||||
- `SatelliteProvider.Common.DTO` — GeoPoint
|
||||
- `SatelliteProvider.Common.Utils` — GeoUtils
|
||||
- `SatelliteProvider.DataAccess.Models` — TileEntity (for existingTiles parameter)
|
||||
- NuGet: `Newtonsoft.Json`, `Microsoft.Extensions.Http`, `Microsoft.Extensions.Options`
|
||||
|
||||
## Consumers
|
||||
- `TileService` — `GetTilesWithMetadataAsync`
|
||||
- `Program.cs` (ServeTile, GetTileByLatLon) — `DownloadSingleTileAsync`
|
||||
|
||||
## Data Models
|
||||
Produces `DownloadedTileInfoV2` records; accepts `TileEntity` for cache checks.
|
||||
|
||||
## Configuration
|
||||
| Config | Key | Used For |
|
||||
|--------|-----|----------|
|
||||
| MapConfig | ApiKey | Session token requests |
|
||||
| StorageConfig | TilesDirectory | File save paths |
|
||||
| ProcessingConfig | MaxConcurrentDownloads | SemaphoreSlim capacity |
|
||||
| ProcessingConfig | DelayBetweenRequestsMs | Throttle delay |
|
||||
| ProcessingConfig | SessionTokenReuseCount | Token rotation threshold |
|
||||
|
||||
## External Integrations
|
||||
| Integration | Protocol | Details |
|
||||
|-------------|----------|---------|
|
||||
| Google Maps Tile API | HTTPS | `mt*.google.com/vt/lyrs=s` for tiles |
|
||||
| Google Maps Session API | HTTPS | `tile.googleapis.com/v1/createSession` |
|
||||
| File system | Local FS | Writes JPEG tiles to `StorageConfig.TilesDirectory` |
|
||||
|
||||
## Security
|
||||
- API key transmitted over HTTPS to Google endpoints
|
||||
- User-Agent spoofs a Chrome browser to match expected Google Maps client
|
||||
|
||||
## Tests
|
||||
No dedicated unit tests (the test file `GoogleMapsDownloaderTests.cs` contains only a dummy test).
|
||||
@@ -0,0 +1,91 @@
|
||||
# Module: Services/RegionService + RegionProcessingService + RegionRequestQueue
|
||||
|
||||
## Purpose
|
||||
End-to-end region processing pipeline: API request handling → queue → background worker → tile download → output file generation. Three closely coupled classes form the region processing subsystem.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### RegionService (implements IRegionService)
|
||||
- `RequestRegionAsync(...)`: creates `RegionEntity` with status "queued", enqueues a `RegionRequest`, returns `RegionStatus`
|
||||
- `GetRegionStatusAsync(Guid id)`: reads region record and maps to `RegionStatus`
|
||||
- `ProcessRegionAsync(Guid id, CancellationToken)`: the main processing pipeline — see Internal Logic
|
||||
|
||||
### RegionProcessingService (BackgroundService)
|
||||
- `ExecuteAsync(CancellationToken)`: spawns `MaxConcurrentRegions` parallel worker tasks, each in an infinite dequeue loop
|
||||
|
||||
### RegionRequestQueue (implements IRegionRequestQueue)
|
||||
- `EnqueueAsync(RegionRequest, CancellationToken)`: writes to a bounded `Channel<RegionRequest>`
|
||||
- `DequeueAsync(CancellationToken)`: reads from the channel (blocks until available)
|
||||
- `Count`: current queue depth
|
||||
|
||||
## Internal Logic
|
||||
|
||||
### RegionService.ProcessRegionAsync
|
||||
1. Sets region status to "processing"
|
||||
2. Creates a 5-minute timeout `CancellationTokenSource`
|
||||
3. Queries existing tiles in the region
|
||||
4. Calls `TileService.DownloadAndStoreTilesAsync` to fetch missing tiles
|
||||
5. Counts downloaded vs reused tiles
|
||||
6. Generates CSV file (`region_{id}_ready.csv`) listing tile coordinates + paths
|
||||
7. Optionally stitches tiles into a single JPEG image (if `StitchTiles` is true)
|
||||
8. Generates summary file (`region_{id}_summary.txt`)
|
||||
9. Updates region status to "completed"
|
||||
10. On any error: sets status to "failed", generates error summary
|
||||
|
||||
### Tile Stitching
|
||||
Uses ImageSharp to:
|
||||
1. Compute a tile grid from tile coordinates
|
||||
2. Create a new image of `(gridWidth × 256) × (gridHeight × 256)` pixels
|
||||
3. Place each tile image at its grid position
|
||||
4. Draw a red crosshair at the center coordinates
|
||||
5. Save as JPEG
|
||||
|
||||
### Error Handling
|
||||
Comprehensive catch blocks for:
|
||||
- `TaskCanceledException` (timeout vs external cancellation)
|
||||
- `OperationCanceledException`
|
||||
- `RateLimitException` (Google rate limiting)
|
||||
- `HttpRequestException` (with status code)
|
||||
- Generic `Exception`
|
||||
Each sets status to "failed" and writes an error summary file.
|
||||
|
||||
### RegionProcessingService
|
||||
- Spawns `MaxConcurrentRegions` worker tasks with staggered startup (100–500ms random delay)
|
||||
- Each worker loops: dequeue → `ProcessRegionAsync` → repeat
|
||||
- Graceful shutdown on cancellation
|
||||
|
||||
### RegionRequestQueue
|
||||
- Uses `System.Threading.Channels.Channel<T>.CreateBounded` with `BoundedChannelFullMode.Wait`
|
||||
- Tracks `_totalEnqueued` and `_totalDequeued` counters
|
||||
|
||||
## Dependencies
|
||||
- `ITileService`, `IRegionRepository`, `IRegionRequestQueue`
|
||||
- `StorageConfig`, `ProcessingConfig`
|
||||
- `SixLabors.ImageSharp` — tile stitching
|
||||
- `SatelliteProvider.Common.Utils.GeoUtils` — coordinate conversion for stitching
|
||||
|
||||
## Consumers
|
||||
- `Program.cs` API endpoints — `RequestRegionAsync`, `GetRegionStatusAsync`
|
||||
- `RouteService` — `RequestRegionAsync` (for geofence regions)
|
||||
- `RouteProcessingService` — `RequestRegionAsync` (for route-point regions)
|
||||
|
||||
## Data Models
|
||||
- Input: `RegionRequest` (queue message)
|
||||
- Output: `RegionStatus` (API response), CSV files, summary files, stitched images
|
||||
- Persistence: `RegionEntity`
|
||||
|
||||
## Configuration
|
||||
- `StorageConfig.ReadyDirectory` — output file location
|
||||
- `ProcessingConfig.MaxConcurrentRegions` — worker count
|
||||
- `ProcessingConfig.QueueCapacity` — bounded channel size
|
||||
|
||||
## External Integrations
|
||||
- PostgreSQL (via repositories)
|
||||
- File system (CSV, summary, stitched images in `./ready/`)
|
||||
- Google Maps (indirectly via TileService → GoogleMapsDownloaderV2)
|
||||
|
||||
## Security
|
||||
None.
|
||||
|
||||
## Tests
|
||||
Integration tests in `RegionTests.cs` cover the request → poll → complete flow.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Module: Services/RouteService + RouteProcessingService
|
||||
|
||||
## Purpose
|
||||
Route management and asynchronous map generation. `RouteService` handles route creation with intermediate point interpolation and geofencing. `RouteProcessingService` is a background service that polls for routes needing map generation and produces stitched images, CSVs, summaries, and ZIP archives.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### RouteService (implements IRouteService)
|
||||
- `CreateRouteAsync(CreateRouteRequest) → Task<RouteResponse>`: validates input, computes intermediate points, persists route + points, creates geofence regions if specified
|
||||
- `GetRouteAsync(Guid id) → Task<RouteResponse?>`: retrieves route with all points
|
||||
|
||||
### RouteProcessingService (BackgroundService)
|
||||
- `ExecuteAsync(CancellationToken)`: polls every 5 seconds for routes with `request_maps = true AND maps_ready = false`, then processes each sequentially
|
||||
|
||||
## Internal Logic
|
||||
|
||||
### RouteService.CreateRouteAsync
|
||||
1. **Validation**: minimum 2 points, region size 100–10000m, name required
|
||||
2. **Point interpolation**: for each segment between consecutive input points:
|
||||
- First point typed as "start", last as "end", middle as "action"
|
||||
- Calls `GeoUtils.CalculateIntermediatePoints(start, end, 200m)` to generate intermediate points
|
||||
- Each intermediate point gets `PointType = "intermediate"`
|
||||
- Distance from previous point is computed via `GeoUtils.CalculateDistance`
|
||||
3. **Persistence**: inserts `RouteEntity` + bulk inserts `RoutePointEntity` via repository
|
||||
4. **Geofencing** (if `Geofences.Polygons` provided):
|
||||
- Validates each polygon: non-null corners, non-zero coordinates, valid lat/lon ranges, NW lat > SE lat
|
||||
- Calls `CreateGeofenceRegionGrid` to divide the polygon bounding box into a grid of region centers
|
||||
- For each grid point, calls `RegionService.RequestRegionAsync` and links to route as geofence region
|
||||
5. Returns `RouteResponse` with all computed points
|
||||
|
||||
### CreateGeofenceRegionGrid
|
||||
Divides a bounding box (NW → SE) into a regular grid where each cell is `regionSizeMeters` wide. Uses lat/lon step sizes derived from physical distance calculations. Returns a list of center points.
|
||||
|
||||
### RouteProcessingService.ProcessRouteSequentiallyAsync
|
||||
1. Checks route needs processing (`RequestMaps && !MapsReady`)
|
||||
2. Loads route points and linked region IDs (both regular and geofence)
|
||||
3. If no regions linked yet: creates region requests for each route point
|
||||
4. Checks completion status of all linked regions
|
||||
5. When enough regions complete: generates consolidated outputs
|
||||
6. Retries failed regions by creating new region requests
|
||||
|
||||
### GenerateRouteMapsAsync
|
||||
1. Collects all tile data from completed region CSVs, deduplicating by coordinates
|
||||
2. Generates consolidated route CSV
|
||||
3. If `RequestMaps`: stitches all tiles into a single image with:
|
||||
- Geofence polygon borders (yellow rectangles)
|
||||
- Route point markers (red crosses, 50px arms, 10px thickness)
|
||||
4. If `CreateTilesZip`: creates ZIP archive of all tile files with directory structure preserved
|
||||
5. Generates route summary text file
|
||||
6. Updates route record (`MapsReady = true`, file paths)
|
||||
7. Cleans up individual region CSV/summary/stitched files
|
||||
|
||||
### Tile Stitching (route-level)
|
||||
- Extracts tile X/Y from filenames (`tile_{z}_{x}_{y}_{ts}.jpg`)
|
||||
- Creates grid-sized image, places tiles, draws geofence borders and route points
|
||||
- Background color: black (for missing tiles)
|
||||
|
||||
### TileInfo (helper class)
|
||||
Simple data holder: `Latitude`, `Longitude`, `FilePath`.
|
||||
|
||||
## Dependencies
|
||||
- `IRouteRepository`, `IRegionRepository`, `IRegionService`
|
||||
- `SatelliteProvider.Common.DTO` — GeoPoint, RoutePointDto, CreateRouteRequest, RouteResponse, GeofencePolygon
|
||||
- `SatelliteProvider.Common.Utils.GeoUtils`
|
||||
- `SatelliteProvider.DataAccess.Models` — RouteEntity, RoutePointEntity, RegionEntity
|
||||
- `SixLabors.ImageSharp` — tile stitching
|
||||
- `System.IO.Compression` — ZIP creation
|
||||
- `IServiceProvider` — creates scoped `IRegionService` instances
|
||||
|
||||
## Consumers
|
||||
- `Program.cs` API endpoints — `CreateRouteAsync`, `GetRouteAsync`
|
||||
|
||||
## Data Models
|
||||
- Input: `CreateRouteRequest`, `RoutePoint`, `GeofencePolygon`
|
||||
- Output: `RouteResponse`, `RoutePointDto`, CSV/summary/stitched/zip files
|
||||
- Persistence: `RouteEntity`, `RoutePointEntity`, `route_regions` junction
|
||||
|
||||
## Configuration
|
||||
- `StorageConfig.ReadyDirectory` — output directory
|
||||
- `StorageConfig.TilesDirectory` — used for ZIP relative paths
|
||||
|
||||
## External Integrations
|
||||
- PostgreSQL (via repositories)
|
||||
- File system (CSV, summary, stitched image, ZIP in `./ready/`)
|
||||
- Region processing pipeline (for tile downloading)
|
||||
|
||||
## Security
|
||||
None.
|
||||
|
||||
## Tests
|
||||
Integration tests in `BasicRouteTests.cs`, `ComplexRouteTests.cs`, `ExtendedRouteTests.cs`.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Module: Services/TileService
|
||||
|
||||
## Purpose
|
||||
Orchestrates tile downloading and persistence. Bridges the downloader (Google Maps) with the tile repository (PostgreSQL), handling cache checks, entity creation, and metadata mapping.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### TileService (implements ITileService)
|
||||
- `DownloadAndStoreTilesAsync(double lat, double lon, double sizeMeters, int zoomLevel, CancellationToken) → Task<List<TileMetadata>>`:
|
||||
1. Queries existing tiles in the region from the repository (filtered to current year's version)
|
||||
2. Calls `GoogleMapsDownloaderV2.GetTilesWithMetadataAsync` with existing tiles to skip
|
||||
3. Creates `TileEntity` for each newly downloaded tile and inserts via repository (upsert)
|
||||
4. Returns combined list of existing + new tile metadata
|
||||
- `GetTileAsync(Guid id) → Task<TileMetadata?>`: single tile lookup
|
||||
- `GetTilesByRegionAsync(double lat, double lon, double sizeMeters, int zoomLevel) → Task<IEnumerable<TileMetadata>>`: query tiles in a region
|
||||
|
||||
## Internal Logic
|
||||
- Version is `DateTime.UtcNow.Year` — tiles are considered fresh for the current calendar year
|
||||
- `MapToMetadata(TileEntity) → TileMetadata`: entity-to-DTO mapping (static helper)
|
||||
- Tile size hardcoded to 256 pixels, image type "jpg"
|
||||
- `MapsVersion` formatted as `"downloaded_{date}"`
|
||||
|
||||
## Dependencies
|
||||
- `GoogleMapsDownloaderV2` (concrete class, not interface)
|
||||
- `ITileRepository`
|
||||
- `SatelliteProvider.Common.DTO` — GeoPoint, TileMetadata
|
||||
- `SatelliteProvider.DataAccess.Models` — TileEntity
|
||||
|
||||
## Consumers
|
||||
- `RegionService.ProcessRegionAsync` — downloads and retrieves tiles for a region
|
||||
|
||||
## Data Models
|
||||
Transforms between `TileEntity` (persistence) and `TileMetadata` (DTO).
|
||||
|
||||
## Configuration
|
||||
None directly; relies on `GoogleMapsDownloaderV2`'s configuration.
|
||||
|
||||
## External Integrations
|
||||
Indirect: Google Maps (via downloader), PostgreSQL (via repository).
|
||||
|
||||
## Security
|
||||
None.
|
||||
|
||||
## Tests
|
||||
No dedicated tests.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Module: Tests/SatelliteProvider.IntegrationTests
|
||||
|
||||
## Purpose
|
||||
Console application that runs end-to-end integration tests against a live API instance. Designed to run in Docker alongside the API and PostgreSQL containers.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### Test Classes
|
||||
- `TileTests` — tile download via lat/lon endpoint
|
||||
- `RegionTests` — region request → polling → completion flow
|
||||
- `BasicRouteTests` — route creation with intermediate points
|
||||
- `ComplexRouteTests` — routes with geofencing
|
||||
- `ExtendedRouteTests` — routes with `requestMaps: true` and tile ZIP creation
|
||||
|
||||
### Supporting Classes
|
||||
- `Models.cs` — HTTP response DTOs for deserialization
|
||||
- `RouteTestHelpers.cs` — shared utilities (wait-for-completion polling, geofence polygon builders, test data)
|
||||
- `Program.cs` — test runner entry point
|
||||
|
||||
## Internal Logic
|
||||
- Makes HTTP calls to the API at `API_URL` environment variable (default: `http://api:8080`)
|
||||
- Tests are methods called sequentially from `Program.cs` (not xUnit — plain console app)
|
||||
- Poll-based waiting for async operations (region/route completion)
|
||||
- Validates response structure, status transitions, file creation
|
||||
|
||||
## Dependencies
|
||||
- No project references (standalone console app)
|
||||
- Communicates with the API exclusively via HTTP
|
||||
- NuGet: implicit .NET 8 runtime
|
||||
|
||||
## Consumers
|
||||
- `docker-compose.tests.yml` — runs as a container that depends on the API service
|
||||
|
||||
## Configuration
|
||||
- `API_URL` environment variable (set in docker-compose.tests.yml to `http://api:8080`)
|
||||
|
||||
## External Integrations
|
||||
- HTTP to the SatelliteProvider API
|
||||
- Reads output files from mounted `./ready/` and `./tiles/` volumes
|
||||
|
||||
## Security
|
||||
None.
|
||||
|
||||
## Tests
|
||||
This IS the integration test suite.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Module: Tests/SatelliteProvider.Tests
|
||||
|
||||
## Purpose
|
||||
Unit test project. Currently contains only a single dummy test as a placeholder.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### DummyTests
|
||||
- `Dummy_ShouldWork()`: asserts `1 == 1`
|
||||
|
||||
## Internal Logic
|
||||
No meaningful test logic.
|
||||
|
||||
## Dependencies
|
||||
- Project references: `SatelliteProvider.Services`, `SatelliteProvider.Common`
|
||||
- NuGet: xUnit (2.5.3), Moq (4.20.72), FluentAssertions (8.8.0), coverlet.collector (6.0.0), Microsoft.NET.Test.Sdk (17.8.0), Microsoft.Extensions.* (Configuration, DI, Logging, Options, Http)
|
||||
- Has `appsettings.json` copied to output (empty config for potential future test setups)
|
||||
|
||||
## Consumers
|
||||
- CI pipeline (`01-test.yml`) runs `dotnet test` against this project
|
||||
|
||||
## Tests
|
||||
This IS the test module. Coverage: effectively zero (only a dummy test).
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"current_step": "component-assembly",
|
||||
"completed_steps": ["discovery", "module-analysis"],
|
||||
"focus_dir": null,
|
||||
"modules_total": 12,
|
||||
"modules_documented": [
|
||||
"common_configs",
|
||||
"common_dtos",
|
||||
"common_interfaces",
|
||||
"common_geoutil",
|
||||
"dataaccess_models",
|
||||
"dataaccess_migrator",
|
||||
"dataaccess_tile_repository",
|
||||
"dataaccess_region_repository",
|
||||
"dataaccess_route_repository",
|
||||
"services_google_maps_downloader",
|
||||
"services_tile_service",
|
||||
"services_region",
|
||||
"services_route",
|
||||
"api_program",
|
||||
"tests_unit",
|
||||
"tests_integration"
|
||||
],
|
||||
"modules_remaining": [],
|
||||
"module_batch": 4,
|
||||
"components_written": [],
|
||||
"step_4_5_glossary_vision": "not_started",
|
||||
"last_updated": "2026-05-10T00:30:00Z"
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
# Satellite Provider — System Flows
|
||||
|
||||
## Flow Inventory
|
||||
|
||||
| # | Flow Name | Trigger | Primary Components | Criticality |
|
||||
|---|-----------|---------|-------------------|-------------|
|
||||
| F1 | Single Tile Download | HTTP GET /api/satellite/tiles/latlon | WebApi, TileDownloader, DataAccess | High |
|
||||
| F2 | Region Request | HTTP POST /api/satellite/request | WebApi, RegionProcessing, TileDownloader, DataAccess | High |
|
||||
| F3 | Region Processing | Queue dequeue (background) | RegionProcessing, TileDownloader, DataAccess | High |
|
||||
| F4 | Route Creation | HTTP POST /api/satellite/route | WebApi, RouteManagement, DataAccess | High |
|
||||
| F5 | Route Map Processing | Queue dequeue (background) | RouteManagement, RegionProcessing, TileDownloader, DataAccess | Medium |
|
||||
| F6 | Status Query | HTTP GET /api/satellite/region/{id} or /route/{id} | WebApi, DataAccess | Low |
|
||||
|
||||
## Flow Dependencies
|
||||
|
||||
| Flow | Depends On | Shares Data With |
|
||||
|------|-----------|-----------------|
|
||||
| F1 | — | F3 (tile cache) |
|
||||
| F2 | — | F3 (triggers it) |
|
||||
| F3 | F2 enqueues work | F1 (shares tile cache), F5 |
|
||||
| F4 | — | F5 (triggers it) |
|
||||
| F5 | F4 must create route first | F3 (submits region requests) |
|
||||
| F6 | F2/F4 must exist | — |
|
||||
|
||||
---
|
||||
|
||||
## Flow F1: Single Tile Download
|
||||
|
||||
### Description
|
||||
|
||||
Client requests a single satellite tile by geographic coordinates and zoom level. The service checks the cache (DB), downloads from Google Maps if not cached, stores it, and returns metadata.
|
||||
|
||||
### Preconditions
|
||||
|
||||
- Valid latitude, longitude, and zoom level provided
|
||||
- Google Maps session token configured
|
||||
|
||||
### Sequence Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant WebApi
|
||||
participant TileService
|
||||
participant TileRepo
|
||||
participant GoogleMaps
|
||||
participant FileSystem
|
||||
|
||||
Client->>WebApi: GET /api/satellite/tiles/latlon?lat&lon&zoom
|
||||
WebApi->>TileService: DownloadTileAsync(lat, lon, zoom)
|
||||
TileService->>TileRepo: FindByCoordinates(lat, lon, zoom)
|
||||
alt Tile exists in cache
|
||||
TileRepo-->>TileService: TileEntity
|
||||
TileService-->>WebApi: TileMetadata (cached)
|
||||
else Not cached
|
||||
TileService->>GoogleMaps: Download tile image
|
||||
GoogleMaps-->>TileService: JPEG bytes
|
||||
TileService->>FileSystem: Save to ./tiles/{zoom}/{x}/{y}.jpg
|
||||
TileService->>TileRepo: Insert(TileEntity)
|
||||
TileService-->>WebApi: TileMetadata (new)
|
||||
end
|
||||
WebApi-->>Client: JSON response
|
||||
```
|
||||
|
||||
### Error Scenarios
|
||||
|
||||
| Error | Where | Detection | Recovery |
|
||||
|-------|-------|-----------|----------|
|
||||
| Google Maps timeout | Download step | HttpClient timeout | Return error to caller |
|
||||
| Duplicate download race | Concurrent requests | ConcurrentDictionary check | Await existing download |
|
||||
| Disk full | File save | IOException | Exception propagated, region fails |
|
||||
|
||||
---
|
||||
|
||||
## Flow F2: Region Request
|
||||
|
||||
### Description
|
||||
|
||||
Client submits a region definition (center point, size, zoom). The request is persisted and queued for asynchronous processing.
|
||||
|
||||
### Preconditions
|
||||
|
||||
- Valid region parameters (lat, lon, size_meters, zoom_level)
|
||||
|
||||
### Sequence Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant WebApi
|
||||
participant RegionService
|
||||
participant RegionRepo
|
||||
participant Queue
|
||||
|
||||
Client->>WebApi: POST /api/satellite/request {lat, lon, size, zoom}
|
||||
WebApi->>RegionService: CreateRegionRequest(dto)
|
||||
RegionService->>RegionRepo: Insert(RegionEntity status=pending)
|
||||
RegionRepo-->>RegionService: region_id
|
||||
RegionService->>Queue: Enqueue(region_id)
|
||||
RegionService-->>WebApi: region_id
|
||||
WebApi-->>Client: 200 OK {region_id}
|
||||
```
|
||||
|
||||
### Error Scenarios
|
||||
|
||||
| Error | Where | Detection | Recovery |
|
||||
|-------|-------|-----------|----------|
|
||||
| Queue full | Enqueue step | Channel at capacity | Return 503 / reject request |
|
||||
| DB insert failure | Persist step | Exception | Return 500 |
|
||||
|
||||
---
|
||||
|
||||
## Flow F3: Region Processing (Background)
|
||||
|
||||
### Description
|
||||
|
||||
Background service dequeues region IDs, calculates tile grid, downloads all tiles (with concurrency control), optionally stitches them, and produces output files (CSV, summary, stitched image).
|
||||
|
||||
### Preconditions
|
||||
|
||||
- Region exists in DB with status "pending"
|
||||
- Google Maps session token configured
|
||||
|
||||
### Sequence Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Queue
|
||||
participant RegionProcessor
|
||||
participant RegionService
|
||||
participant TileService
|
||||
participant GoogleMaps
|
||||
participant RegionRepo
|
||||
participant FileSystem
|
||||
|
||||
Queue->>RegionProcessor: Dequeue region_id
|
||||
RegionProcessor->>RegionRepo: GetById(region_id)
|
||||
RegionProcessor->>RegionRepo: UpdateStatus(processing)
|
||||
loop For each tile in grid
|
||||
RegionProcessor->>TileService: DownloadTileAsync(lat, lon, zoom)
|
||||
TileService->>GoogleMaps: Download (if not cached)
|
||||
end
|
||||
RegionProcessor->>FileSystem: Write CSV (tile manifest)
|
||||
RegionProcessor->>FileSystem: Write summary file
|
||||
opt stitch_tiles = true
|
||||
RegionProcessor->>FileSystem: Stitch tiles into composite image
|
||||
end
|
||||
RegionProcessor->>RegionRepo: UpdateStatus(completed, file paths)
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
| Step | From | To | Data | Format |
|
||||
|------|------|----|------|--------|
|
||||
| 1 | Queue | RegionProcessor | region_id | int |
|
||||
| 2 | RegionProcessor | TileService | lat, lon, zoom per tile | method call |
|
||||
| 3 | TileService | FileSystem | JPEG image | file |
|
||||
| 4 | RegionProcessor | FileSystem | tile manifest | CSV |
|
||||
| 5 | RegionProcessor | FileSystem | region summary | TXT |
|
||||
| 6 | RegionProcessor | FileSystem | composite image | JPEG |
|
||||
|
||||
### Error Scenarios
|
||||
|
||||
| Error | Where | Detection | Recovery |
|
||||
|-------|-------|-----------|----------|
|
||||
| Tile download failure | Per-tile loop | Exception from TileService | Log, continue with remaining tiles |
|
||||
| All tiles fail | After loop | Zero tiles downloaded | Mark region as "failed" |
|
||||
| Stitch failure | Image processing | ImageSharp exception | Mark region failed, tiles still available |
|
||||
|
||||
---
|
||||
|
||||
## Flow F4: Route Creation
|
||||
|
||||
### Description
|
||||
|
||||
Client submits a route (ordered waypoints + optional geofence polygons). The service interpolates intermediate points every ~200m and persists the full point set.
|
||||
|
||||
### Preconditions
|
||||
|
||||
- At least 2 waypoints provided
|
||||
- Valid geofence polygons (if provided)
|
||||
|
||||
### Sequence Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant WebApi
|
||||
participant RouteService
|
||||
participant RouteRepo
|
||||
participant GeoUtils
|
||||
|
||||
Client->>WebApi: POST /api/satellite/route {points, geofences, options}
|
||||
WebApi->>RouteService: CreateRoute(request)
|
||||
RouteService->>GeoUtils: Interpolate points between waypoints
|
||||
GeoUtils-->>RouteService: All points (original + intermediate)
|
||||
RouteService->>RouteRepo: InsertRoute(RouteEntity)
|
||||
RouteService->>RouteRepo: InsertPoints(RoutePointEntities)
|
||||
RouteService-->>WebApi: RouteResponse
|
||||
WebApi-->>Client: 200 OK {route_id, total_points, total_distance}
|
||||
```
|
||||
|
||||
### Error Scenarios
|
||||
|
||||
| Error | Where | Detection | Recovery |
|
||||
|-------|-------|-----------|----------|
|
||||
| Invalid points (< 2) | Validation | Count check | Return 400 |
|
||||
| DB insert failure | Persist step | Exception | Return 500 |
|
||||
|
||||
---
|
||||
|
||||
## Flow F5: Route Map Processing (Background)
|
||||
|
||||
### Description
|
||||
|
||||
When a route requests map tiles (`request_maps = true`), a background service creates region requests for each route point, optionally filtered by geofence, then waits for all regions to complete and produces a ZIP archive.
|
||||
|
||||
### Preconditions
|
||||
|
||||
- Route exists with `request_maps = true`
|
||||
- Route points already interpolated and persisted
|
||||
|
||||
### Sequence Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant RouteProcessor
|
||||
participant RouteRepo
|
||||
participant RegionService
|
||||
participant Queue
|
||||
participant RegionProcessor
|
||||
participant FileSystem
|
||||
|
||||
RouteProcessor->>RouteRepo: GetRouteWithPoints(route_id)
|
||||
loop For each route point
|
||||
RouteProcessor->>RouteProcessor: Check geofence (point-in-polygon)
|
||||
opt Point inside geofence (or no geofence)
|
||||
RouteProcessor->>RegionService: CreateRegionRequest(point)
|
||||
RegionService->>Queue: Enqueue(region_id)
|
||||
end
|
||||
end
|
||||
RouteProcessor->>RouteProcessor: Wait for all regions to complete
|
||||
opt create_tiles_zip = true
|
||||
RouteProcessor->>FileSystem: Create ZIP of all tiles (max 50MB)
|
||||
RouteProcessor->>RouteRepo: Update tiles_zip_path
|
||||
end
|
||||
```
|
||||
|
||||
### Error Scenarios
|
||||
|
||||
| Error | Where | Detection | Recovery |
|
||||
|-------|-------|-----------|----------|
|
||||
| Region processing timeout | Wait loop | Polling timeout | Mark route partially complete |
|
||||
| ZIP exceeds 50MB | ZIP creation | Size check during write | Truncate or skip |
|
||||
| Geofence calculation error | Point-in-polygon | Exception | Include point (fail-open) |
|
||||
|
||||
---
|
||||
|
||||
## Flow F6: Status Query
|
||||
|
||||
### Description
|
||||
|
||||
Client polls for the status of a region or route by ID. Returns current processing state and output file paths when complete.
|
||||
|
||||
### Sequence Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant WebApi
|
||||
participant DataAccess
|
||||
|
||||
Client->>WebApi: GET /api/satellite/region/{id}
|
||||
WebApi->>DataAccess: GetRegionById(id)
|
||||
DataAccess-->>WebApi: RegionEntity (status, file paths)
|
||||
WebApi-->>Client: JSON {status, files}
|
||||
```
|
||||
@@ -0,0 +1,115 @@
|
||||
# Blackbox Test Scenarios
|
||||
|
||||
## BT-01: Single Tile Download
|
||||
|
||||
**Trigger**: GET /api/satellite/tiles/latlon?Latitude=47.461747&Longitude=37.647063&ZoomLevel=18
|
||||
**Precondition**: Tile not in cache
|
||||
**Expected**: HTTP 200; JSON with zoomLevel=18, tileSizePixels=256, imageType="jpg", filePath matching pattern `tiles/18/*/...`
|
||||
**Pass criterion**: All fields present and correct values
|
||||
|
||||
## BT-02: Tile Cache Reuse
|
||||
|
||||
**Trigger**: Same GET as BT-01 repeated
|
||||
**Precondition**: BT-01 completed (tile now cached)
|
||||
**Expected**: HTTP 200; same tile ID returned; no new file created
|
||||
**Pass criterion**: tile.Id matches first request's tile.Id
|
||||
|
||||
## BT-03: Region Request (200m, zoom 18, no stitch)
|
||||
|
||||
**Trigger**: POST /api/satellite/request with lat=47.461747, lon=37.647063, sizeMeters=200, zoomLevel=18, stitchTiles=false
|
||||
**Expected**: HTTP 200 immediately; status transitions: pending → processing → completed
|
||||
**Pass criterion**: Final status="completed"; csvFilePath non-empty; summaryFilePath non-empty; tilesDownloaded + tilesReused > 0
|
||||
**Timeout**: 240s
|
||||
|
||||
## BT-04: Region Request (400m, zoom 17, no stitch)
|
||||
|
||||
**Trigger**: POST /api/satellite/request with lat=47.461747, lon=37.647063, sizeMeters=400, zoomLevel=17, stitchTiles=false
|
||||
**Expected**: Same as BT-03
|
||||
**Pass criterion**: Same as BT-03
|
||||
**Timeout**: 240s
|
||||
|
||||
## BT-05: Region with Stitching (500m, zoom 18)
|
||||
|
||||
**Trigger**: POST /api/satellite/request with lat=47.461747, lon=37.647063, sizeMeters=500, zoomLevel=18, stitchTiles=true
|
||||
**Expected**: Completes with stitched image generated
|
||||
**Pass criterion**: status="completed"; stitched image file exists and size > 1024 bytes
|
||||
**Timeout**: 240s
|
||||
|
||||
## BT-06: Simple Route Creation (2 points)
|
||||
|
||||
**Trigger**: POST /api/satellite/route with 2 waypoints (48.276067,37.384458) → (48.270740,37.374029), regionSize=500, zoom=18
|
||||
**Expected**: Route created with interpolated intermediate points
|
||||
**Pass criterion**: totalPoints > 2; every point spacing ≤ 200m; first point type="original"; last point type="original"; intermediates type="intermediate"
|
||||
|
||||
## BT-07: Route Retrieval by ID
|
||||
|
||||
**Trigger**: GET /api/satellite/route/{id} after BT-06
|
||||
**Expected**: Same route returned with all points
|
||||
**Pass criterion**: route.Id matches; points count matches creation response
|
||||
|
||||
## BT-08: Route with Map Processing
|
||||
|
||||
**Trigger**: POST /api/satellite/route with requestMaps=true, 2 points, regionSize=300
|
||||
**Expected**: Route maps processed, stitched image and CSV created
|
||||
**Pass criterion**: mapsReady=true; stitchedImagePath non-empty; csvFilePath non-empty; stitched image > 1024 bytes
|
||||
**Timeout**: 180s
|
||||
|
||||
## BT-09: Route with Tiles ZIP
|
||||
|
||||
**Trigger**: POST /api/satellite/route with requestMaps=true, createTilesZip=true, 2 points
|
||||
**Expected**: ZIP file created with tiles
|
||||
**Pass criterion**: tilesZipPath non-empty; ZIP > 1024 bytes; ZIP entry count = unique tiles in CSV; entries start with "tiles/"; path has ≥5 parts (directory structure preserved)
|
||||
**Timeout**: 180s
|
||||
|
||||
## BT-10: Complex Route (10 points, maps)
|
||||
|
||||
**Trigger**: POST /api/satellite/route with 10 waypoints, requestMaps=true, regionSize=300
|
||||
**Expected**: All points interpolated; map tiles processed
|
||||
**Pass criterion**: mapsReady=true; uniqueTileCount ≥ 10; stitched image > 1024 bytes
|
||||
**Timeout**: 240s
|
||||
|
||||
## BT-11: Route with Geofences (10 points + 2 rectangles)
|
||||
|
||||
**Trigger**: POST /api/satellite/route with 10 waypoints + 2 geofence polygons, requestMaps=true
|
||||
**Expected**: Geofence regions created and processed
|
||||
**Pass criterion**: mapsReady=true; uniqueTileCount ≥ 10; stitched image > 1024 bytes; geofence regions linked to route
|
||||
**Timeout**: 240s
|
||||
|
||||
## BT-12: Extended Route (20 points, maps)
|
||||
|
||||
**Trigger**: POST /api/satellite/route with 20 waypoints in separate geographic area, requestMaps=true
|
||||
**Expected**: Large route processed completely
|
||||
**Pass criterion**: mapsReady=true; uniqueTileCount ≥ 20; stitched image > 1024 bytes
|
||||
**Timeout**: 360s
|
||||
|
||||
## Negative Scenarios
|
||||
|
||||
## BT-N01: Invalid Coordinates (out of range)
|
||||
|
||||
**Trigger**: GET /api/satellite/tiles/latlon?Latitude=91&Longitude=181&ZoomLevel=18
|
||||
**Expected**: Error response
|
||||
**Pass criterion**: HTTP 4xx or error in response body
|
||||
|
||||
## BT-N02: Invalid Zoom Level
|
||||
|
||||
**Trigger**: GET /api/satellite/tiles/latlon?Latitude=47.46&Longitude=37.64&ZoomLevel=25
|
||||
**Expected**: Error response
|
||||
**Pass criterion**: HTTP 4xx or error indicating invalid zoom
|
||||
|
||||
## BT-N03: Route with < 2 Points
|
||||
|
||||
**Trigger**: POST /api/satellite/route with only 1 point
|
||||
**Expected**: Validation error
|
||||
**Pass criterion**: HTTP 400 or validation error message
|
||||
|
||||
## BT-N04: Geofence with Invalid Coordinates (0,0)
|
||||
|
||||
**Trigger**: POST /api/satellite/route with geofence NW=(0,0) SE=(0,0)
|
||||
**Expected**: Validation error
|
||||
**Pass criterion**: Error message mentioning coordinates cannot be (0,0)
|
||||
|
||||
## BT-N05: Geofence with Inverted Corners
|
||||
|
||||
**Trigger**: POST /api/satellite/route with geofence NW.lat < SE.lat
|
||||
**Expected**: Validation error
|
||||
**Pass criterion**: Error message about northWest latitude > southEast latitude
|
||||
@@ -0,0 +1,49 @@
|
||||
# Test Environment
|
||||
|
||||
## Infrastructure
|
||||
|
||||
| Component | Technology | Configuration |
|
||||
|-----------|-----------|---------------|
|
||||
| System Under Test | SatelliteProvider.Api (Docker container) | ASPNETCORE_ENVIRONMENT=Development |
|
||||
| Database | PostgreSQL 16 (Docker container) | Fresh DB per test run (migrations auto-applied) |
|
||||
| Test Runner | Custom console app (SatelliteProvider.IntegrationTests) | Docker container on same network |
|
||||
| Orchestration | docker-compose.tests.yml | Waits for API health before starting tests |
|
||||
|
||||
## Network Topology
|
||||
|
||||
```
|
||||
[Test Runner] --HTTP--> [API :8080] --TCP--> [PostgreSQL :5432]
|
||||
|
|
||||
+--HTTPS--> [Google Maps] (external, real)
|
||||
```
|
||||
|
||||
## External Dependencies
|
||||
|
||||
| Dependency | Strategy | Notes |
|
||||
|------------|----------|-------|
|
||||
| Google Maps tile server | Real (live) | Integration tests use real downloads; requires GOOGLE_MAPS_API_KEY |
|
||||
| PostgreSQL | Real (containerized) | Fresh database each run via migrations |
|
||||
| File system | Real (Docker volume) | ./tiles, ./ready, ./logs mounted |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Value | Purpose |
|
||||
|----------|-------|---------|
|
||||
| API_URL | http://api:8080 | Test runner → API connection |
|
||||
| ASPNETCORE_ENVIRONMENT | Development | API config mode |
|
||||
| ConnectionStrings__DefaultConnection | Host=postgres;Port=5432;... | DB connection |
|
||||
| MapConfig__ApiKey | (from host env) | Google Maps auth |
|
||||
|
||||
## Test Execution
|
||||
|
||||
**Decision**: Docker (no hardware dependencies detected)
|
||||
**Hardware dependencies found**: None
|
||||
**Execution method**: `docker-compose -f docker-compose.yml -f docker-compose.tests.yml up --build --abort-on-container-exit`
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Execution mode | Sequential (one test at a time) |
|
||||
| Timeout per test | 15 minutes (HttpClient timeout) |
|
||||
| Polling interval | 2–3 seconds |
|
||||
| Max poll attempts | 120–360 (depends on test) |
|
||||
| Startup wait | 30 retries × 2s = 60s max |
|
||||
@@ -0,0 +1,43 @@
|
||||
# Performance Test Scenarios
|
||||
|
||||
## PT-01: Single Tile Download Latency
|
||||
|
||||
**Trigger**: GET /api/satellite/tiles/latlon (uncached tile)
|
||||
**Load**: 1 request
|
||||
**Expected**: Response within 30s (includes Google Maps round-trip)
|
||||
**Pass criterion**: Response time < 30000ms; HTTP 200
|
||||
|
||||
## PT-02: Cached Tile Retrieval Latency
|
||||
|
||||
**Trigger**: GET /api/satellite/tiles/latlon (cached tile)
|
||||
**Load**: 1 request
|
||||
**Expected**: Response within 500ms (DB lookup + response)
|
||||
**Pass criterion**: Response time < 500ms; HTTP 200
|
||||
|
||||
## PT-03: Region Processing Throughput (200m)
|
||||
|
||||
**Trigger**: POST /api/satellite/request with 200m region
|
||||
**Load**: 1 region
|
||||
**Expected**: Complete processing within 60s
|
||||
**Pass criterion**: status="completed" within 60s; tiles downloaded > 0
|
||||
|
||||
## PT-04: Region Processing Throughput (500m with stitch)
|
||||
|
||||
**Trigger**: POST /api/satellite/request with 500m region + stitch
|
||||
**Load**: 1 region
|
||||
**Expected**: Complete processing within 120s (more tiles + stitching)
|
||||
**Pass criterion**: status="completed" within 120s; stitched image exists
|
||||
|
||||
## PT-05: Concurrent Region Requests
|
||||
|
||||
**Trigger**: 5 simultaneous POST /api/satellite/request (different coordinates)
|
||||
**Load**: 5 concurrent requests
|
||||
**Expected**: All queued immediately; all complete within 5 minutes
|
||||
**Pass criterion**: All 5 regions reach status="completed"; queue does not reject
|
||||
|
||||
## PT-06: Route Point Interpolation Speed
|
||||
|
||||
**Trigger**: POST /api/satellite/route with 20 points
|
||||
**Load**: 1 request
|
||||
**Expected**: Route created (with interpolation) within 5s
|
||||
**Pass criterion**: HTTP 200 response within 5000ms; totalPoints > 20
|
||||
@@ -0,0 +1,37 @@
|
||||
# Resilience Test Scenarios
|
||||
|
||||
## RS-01: API Startup with Database Ready
|
||||
|
||||
**Trigger**: Start API container after PostgreSQL is healthy
|
||||
**Observable**: API responds to HTTP requests
|
||||
**Pass criterion**: API returns non-5xx response within 60s of container start
|
||||
|
||||
## RS-02: Database Migrations on Fresh Start
|
||||
|
||||
**Trigger**: Start API against empty database
|
||||
**Observable**: All 11 migrations execute successfully
|
||||
**Pass criterion**: API starts without error; all tables exist; schemaversions table has 11 entries
|
||||
|
||||
## RS-03: Region Processing Survives Tile Download Failure
|
||||
|
||||
**Trigger**: Submit region request where some tiles may fail (rate limit / timeout)
|
||||
**Observable**: Region either completes (with partial tiles) or is marked "failed"
|
||||
**Pass criterion**: Status is either "completed" or "failed" (never stuck in "processing" indefinitely); max processing time < 300s
|
||||
|
||||
## RS-04: Queue Capacity Limit
|
||||
|
||||
**Trigger**: Submit 1001+ region requests rapidly (exceeds capacity 1000)
|
||||
**Observable**: Queue rejects overflow requests
|
||||
**Pass criterion**: First 1000 accepted; subsequent requests return error or are dropped; no crash
|
||||
|
||||
## RS-05: Concurrent Download Limit Respected
|
||||
|
||||
**Trigger**: Submit large region (many tiles) and observe download concurrency
|
||||
**Observable**: At most MaxConcurrentDownloads (4) HTTP requests to Google Maps simultaneously
|
||||
**Pass criterion**: No more than 4 concurrent outbound tile requests at any point (behavioral; requires observation or logging)
|
||||
|
||||
## RS-06: Route Processing with All Regions Completing
|
||||
|
||||
**Trigger**: Create route with requestMaps=true, wait for completion
|
||||
**Observable**: Route transitions from processing to ready
|
||||
**Pass criterion**: mapsReady=true; no regions stuck in "processing"
|
||||
@@ -0,0 +1,25 @@
|
||||
# Resource Limit Test Scenarios
|
||||
|
||||
## RL-01: ZIP File Size Limit (50 MB)
|
||||
|
||||
**Trigger**: Create route with enough tiles to approach 50 MB ZIP limit
|
||||
**Observable**: ZIP file size
|
||||
**Pass criterion**: ZIP file ≤ 50 MB; tiles included up to limit; no crash on boundary
|
||||
|
||||
## RL-02: Queue Capacity (1000)
|
||||
|
||||
**Trigger**: Submit 1000 region requests
|
||||
**Observable**: Queue accepts all 1000
|
||||
**Pass criterion**: All 1000 requests accepted and queued; no rejection until capacity reached
|
||||
|
||||
## RL-03: Concurrent Download Semaphore (4)
|
||||
|
||||
**Trigger**: Process region with many tiles
|
||||
**Observable**: Concurrent outbound HTTP connections
|
||||
**Pass criterion**: Never exceeds 4 simultaneous tile downloads (configurable via ProcessingConfig.MaxConcurrentDownloads)
|
||||
|
||||
## RL-04: Concurrent Region Processing (20)
|
||||
|
||||
**Trigger**: Queue 25 region requests
|
||||
**Observable**: Processing parallelism
|
||||
**Pass criterion**: At most 20 regions processing simultaneously (configurable via ProcessingConfig.MaxConcurrentRegions); remaining wait in queue
|
||||
@@ -0,0 +1,25 @@
|
||||
# Security Test Scenarios
|
||||
|
||||
## SEC-01: SQL Injection via Coordinate Parameters
|
||||
|
||||
**Trigger**: GET /api/satellite/tiles/latlon?Latitude=1;DROP TABLE tiles--&Longitude=1&ZoomLevel=18
|
||||
**Expected**: Request rejected or treated as invalid parameter
|
||||
**Pass criterion**: HTTP 400 or parameter parsing error; no database damage; tiles table intact
|
||||
|
||||
## SEC-02: Path Traversal in Tile Serving
|
||||
|
||||
**Trigger**: GET /tiles/18/../../../etc/passwd
|
||||
**Expected**: Request rejected; no file outside tiles directory served
|
||||
**Pass criterion**: HTTP 404 or 400; response body does not contain system file content
|
||||
|
||||
## SEC-03: Oversized Region Request
|
||||
|
||||
**Trigger**: POST /api/satellite/request with sizeMeters=999999999
|
||||
**Expected**: Either rejected or handled without resource exhaustion
|
||||
**Pass criterion**: No OOM; no infinite processing; either error response or bounded processing
|
||||
|
||||
## SEC-04: Malformed JSON in Route Request
|
||||
|
||||
**Trigger**: POST /api/satellite/route with invalid JSON body
|
||||
**Expected**: Parse error returned
|
||||
**Pass criterion**: HTTP 400; error message indicates parsing failure; no crash
|
||||
@@ -0,0 +1,30 @@
|
||||
# Test Data Management
|
||||
|
||||
## Data Sources
|
||||
|
||||
| Source | Location | Type |
|
||||
|--------|----------|------|
|
||||
| Test coordinates | `_docs/00_problem/input_data/test_coordinates.md` | Static reference data |
|
||||
| Expected results | `_docs/00_problem/input_data/expected_results/results_report.md` | Pass/fail criteria |
|
||||
| Generated tiles | ./tiles/ (Docker volume) | Runtime artifacts |
|
||||
| Output files | ./ready/ (Docker volume) | Runtime artifacts |
|
||||
|
||||
## Test Data Lifecycle
|
||||
|
||||
1. **Before test run**: Fresh PostgreSQL database (empty, migrations applied on API startup)
|
||||
2. **During test run**: Each test creates its own data (unique GUIDs for routes/regions)
|
||||
3. **After test run**: Data persists in volumes for inspection; DB data disposable
|
||||
|
||||
## Data Isolation
|
||||
|
||||
- Each test uses `Guid.NewGuid()` for region/route IDs — no conflicts between tests
|
||||
- Tests run sequentially — no concurrency conflicts
|
||||
- Tile cache is shared across tests (by design — tests tile reuse)
|
||||
|
||||
## Reference Coordinates
|
||||
|
||||
| Label | Latitude | Longitude | Use |
|
||||
|-------|----------|-----------|-----|
|
||||
| Tile/Region test point | 47.461747 | 37.647063 | Tile download, region processing |
|
||||
| Route area (start) | 48.276067 | 37.384458 | Route creation, map processing |
|
||||
| Route area (east) | 48.276067 | 37.519458 | Extended route (non-overlapping) |
|
||||
@@ -0,0 +1,53 @@
|
||||
# Traceability Matrix
|
||||
|
||||
## Acceptance Criteria → Test Mapping
|
||||
|
||||
| AC | Description | Tests | Coverage |
|
||||
|----|-------------|-------|----------|
|
||||
| T1 | Tiles cached, not re-downloaded | BT-02 | ✓ |
|
||||
| T2 | Concurrent download limit | RS-05, RL-03 | ✓ |
|
||||
| T3 | Tile stored with correct path | BT-01 | ✓ |
|
||||
| T4 | Tile metadata persisted | BT-01 | ✓ |
|
||||
| R1 | Region state transitions | BT-03, BT-04, BT-05 | ✓ |
|
||||
| R2 | CSV manifest generated | BT-03, BT-04, BT-05 | ✓ |
|
||||
| R3 | Summary file generated | BT-03, BT-04, BT-05 | ✓ |
|
||||
| R4 | Stitched image when requested | BT-05 | ✓ |
|
||||
| R5 | Stitched image valid content | BT-05 | ✓ |
|
||||
| R6 | Region processing bounded | RL-04 | ✓ |
|
||||
| RT1 | Points interpolated at ~200m | BT-06 | ✓ |
|
||||
| RT2 | Point types correctly assigned | BT-06 | ✓ |
|
||||
| RT3 | Total distance calculated | BT-06 | ✓ |
|
||||
| RT4 | Geofence filtering applied | BT-11 | ✓ |
|
||||
| RT5 | ZIP ≤ 50 MB | BT-09, RL-01 | ✓ |
|
||||
| RT6 | Route map stitched | BT-08, BT-10, BT-12 | ✓ |
|
||||
| A1 | Region request returns immediately | BT-03 | ✓ |
|
||||
| A2 | Status endpoint reflects state | BT-03, BT-07 | ✓ |
|
||||
| A3 | Route returns computed metadata | BT-06 | ✓ |
|
||||
| S1 | Migrations run on startup | RS-02 | ✓ |
|
||||
| S2 | Queue rejects when full | RS-04, RL-02 | ✓ |
|
||||
| S3 | Failed regions marked failed | RS-03 | ✓ |
|
||||
|
||||
## Restrictions → Test Mapping
|
||||
|
||||
| Restriction | Tests | Coverage |
|
||||
|-------------|-------|----------|
|
||||
| .NET 8.0 runtime | All (via Docker image) | ✓ |
|
||||
| PostgreSQL 16 | All (via docker-compose) | ✓ |
|
||||
| Single instance | PT-05 (concurrent regions on one instance) | ✓ |
|
||||
| Max 4 concurrent downloads | RS-05, RL-03 | ✓ |
|
||||
| Max 20 concurrent regions | RL-04 | ✓ |
|
||||
| Queue capacity 1000 | RS-04, RL-02 | ✓ |
|
||||
| Max ZIP 50 MB | RL-01 | ✓ |
|
||||
| No authentication | SEC-01 through SEC-04 (all requests accepted without auth) | ✓ |
|
||||
|
||||
## Coverage Summary
|
||||
|
||||
| Category | Total Tests | ACs Covered | Restrictions Covered |
|
||||
|----------|-------------|-------------|---------------------|
|
||||
| Blackbox (positive) | 12 | 19/22 | — |
|
||||
| Blackbox (negative) | 5 | — | — |
|
||||
| Performance | 6 | 2 | 1 |
|
||||
| Resilience | 6 | 4 | 3 |
|
||||
| Security | 4 | — | 1 |
|
||||
| Resource Limits | 4 | 3 | 4 |
|
||||
| **Total** | **37** | **22/22 (100%)** | **8/8 (100%)** |
|
||||
@@ -0,0 +1,34 @@
|
||||
# Task Dependencies
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
| Task | Depends On | Points | Status |
|
||||
|------|-----------|--------|--------|
|
||||
| AZ-285 Test Infrastructure | — | 3 | To Do |
|
||||
| AZ-286 TileService Tests | AZ-285 | 3 | To Do |
|
||||
| AZ-287 RegionService Tests | AZ-285 | 3 | To Do |
|
||||
| AZ-288 RouteService Tests | AZ-285 | 3 | To Do |
|
||||
| AZ-289 Integration Route Maps | AZ-285 | 2 | To Do |
|
||||
| AZ-290 Non-Functional Tests | AZ-285 | 3 | To Do |
|
||||
|
||||
## Execution Order
|
||||
|
||||
1. AZ-285 (test infrastructure — all others depend on this)
|
||||
2. AZ-286, AZ-287, AZ-288 (unit tests — can run in parallel)
|
||||
3. AZ-289 (integration tests — depends on infra only)
|
||||
4. AZ-290 (non-functional tests — depends on infra only)
|
||||
|
||||
## Total Effort
|
||||
|
||||
6 tasks, 17 story points total
|
||||
|
||||
## Coverage Verification
|
||||
|
||||
| Test Spec Category | Covered By |
|
||||
|-------------------|------------|
|
||||
| blackbox-tests.md (BT-01..BT-12, BT-N01..BT-N05) | AZ-286, AZ-287, AZ-288, AZ-289 |
|
||||
| performance-tests.md (PT-01..PT-06) | AZ-290 |
|
||||
| resilience-tests.md (RS-01..RS-06) | AZ-290 |
|
||||
| security-tests.md (SEC-01..SEC-04) | AZ-290 |
|
||||
| resource-limit-tests.md (RL-01..RL-04) | AZ-290 |
|
||||
| traceability-matrix.md (100% AC coverage) | All tasks combined |
|
||||
@@ -0,0 +1,30 @@
|
||||
# Test Infrastructure
|
||||
|
||||
**Task**: AZ-285_test_infrastructure
|
||||
**Name**: Test Infrastructure: scaffold unit test project with mocks
|
||||
**Description**: Scaffold the unit test project with proper mocking infrastructure for ISatelliteDownloader and all repository interfaces
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: None
|
||||
**Component**: Blackbox Tests
|
||||
**Tracker**: AZ-285
|
||||
**Epic**: AZ-284
|
||||
|
||||
## Scope
|
||||
|
||||
- Replace DummyTest with actual test infrastructure in `SatelliteProvider.Tests`
|
||||
- Add Moq package for interface mocking (ISatelliteDownloader, ITileRepository, IRegionRepository, IRouteRepository, IRegionRequestQueue)
|
||||
- Add shared test fixtures with standard test coordinates (47.461747, 37.647063 for tiles; 48.276067, 37.384458 for routes)
|
||||
- Verify existing docker-compose.tests.yml works as integration test environment
|
||||
- Ensure FluentAssertions is available (already in project)
|
||||
|
||||
## Test Project Layout
|
||||
|
||||
Existing structure is sufficient — enhance `SatelliteProvider.Tests/`:
|
||||
- Add `Fixtures/` for shared test data
|
||||
- Add test classes per service (TileServiceTests, RegionServiceTests, RouteServiceTests)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1**: Unit test project builds and all mock interfaces resolve
|
||||
**AC-2**: `docker-compose -f docker-compose.yml -f docker-compose.tests.yml up --build --abort-on-container-exit` succeeds
|
||||
**AC-3**: Test runner discovers and executes test classes
|
||||
@@ -0,0 +1,30 @@
|
||||
# Unit Tests: TileService
|
||||
|
||||
**Task**: AZ-286_tile_service_tests
|
||||
**Name**: Unit tests: TileService (download, cache, dedup)
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: AZ-285
|
||||
**Component**: Blackbox Tests
|
||||
**Tracker**: AZ-286
|
||||
**Epic**: AZ-284
|
||||
|
||||
## Scenarios
|
||||
|
||||
- BT-01: Tile download — mock ISatelliteDownloader.DownloadSingleTileAsync returns tile info; verify TileService stores it via ITileRepository.InsertAsync
|
||||
- BT-02: Cache reuse — mock ITileRepository.GetTilesByRegionAsync returns existing tile; verify ISatelliteDownloader receives existing tiles to skip
|
||||
- BT-N01: Invalid coordinates — verify appropriate error handling for out-of-range lat/lon
|
||||
- BT-N02: Invalid zoom level — verify GoogleMapsDownloaderV2 rejects zoom levels outside allowed range
|
||||
|
||||
## Test Data
|
||||
|
||||
Coordinates from `input_data/test_coordinates.md`: lat=47.461747, lon=37.647063, zoom=18
|
||||
|
||||
## Expected Results
|
||||
|
||||
Per `input_data/expected_results/results_report.md`: tile with zoomLevel=18, tileSizePixels=256, imageType="jpg", non-empty filePath
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
AC-1: All 4 scenarios have passing tests
|
||||
AC-2: ISatelliteDownloader is mocked (no real Google Maps calls)
|
||||
AC-3: Tests verify both happy path and error paths
|
||||
@@ -0,0 +1,30 @@
|
||||
# Unit Tests: RegionService
|
||||
|
||||
**Task**: AZ-287_region_service_tests
|
||||
**Name**: Unit tests: RegionService (request, process, stitch)
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: AZ-285
|
||||
**Component**: Blackbox Tests
|
||||
**Tracker**: AZ-287
|
||||
**Epic**: AZ-284
|
||||
|
||||
## Scenarios
|
||||
|
||||
- BT-03: Region 200m zoom 18 — verify request queuing and status="completed" after processing
|
||||
- BT-04: Region 400m zoom 17 — same flow, different parameters
|
||||
- BT-05: Region 500m zoom 18 with stitching — verify stitched image path is set
|
||||
|
||||
## Test Data
|
||||
|
||||
Coordinates: lat=47.461747, lon=37.647063. Sizes: 200m, 400m, 500m.
|
||||
|
||||
## Expected Results
|
||||
|
||||
Per results_report.md: status="completed", csvFilePath non-empty, summaryFilePath non-empty
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
AC-1: RegionService.RequestRegionAsync creates entity and queues request
|
||||
AC-2: RegionService.ProcessRegionAsync transitions status pending→processing→completed
|
||||
AC-3: CSV and summary files are generated
|
||||
AC-4: Stitch path is set when stitchTiles=true
|
||||
@@ -0,0 +1,30 @@
|
||||
# Unit Tests: RouteService
|
||||
|
||||
**Task**: AZ-288_route_service_tests
|
||||
**Name**: Unit tests: RouteService (interpolation, geofence, distance)
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: AZ-285
|
||||
**Component**: Blackbox Tests
|
||||
**Tracker**: AZ-288
|
||||
**Epic**: AZ-284
|
||||
|
||||
## Scenarios
|
||||
|
||||
- BT-06: Simple 2-point route — verify intermediate points at ≤200m spacing, point types correct
|
||||
- BT-07: Route retrieval — verify GET returns same route with all points
|
||||
- BT-10: Complex 10-point route — verify point distribution (1 first-original, 1 last-original, 8 intermediate)
|
||||
- BT-11: Geofenced route — verify geofence region creation
|
||||
- BT-12: Extended 20-point route — verify point distribution (1 first-original, 1 last-original, 18 intermediate)
|
||||
- BT-N03: Route with <2 points — verify validation error
|
||||
- BT-N04/N05: Invalid geofences — verify validation errors
|
||||
|
||||
## Test Data
|
||||
|
||||
Route points from test_coordinates.md (ROUTE-01 through ROUTE-06)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
AC-1: Point interpolation produces spacing ≤200m between all consecutive points
|
||||
AC-2: Point type assignment is correct (original/intermediate)
|
||||
AC-3: Total distance is calculated via Haversine
|
||||
AC-4: Negative cases return appropriate errors
|
||||
@@ -0,0 +1,31 @@
|
||||
# Integration Tests: Route Map Processing + ZIP
|
||||
|
||||
**Task**: AZ-289_integration_route_maps
|
||||
**Name**: Integration tests: route map processing + ZIP
|
||||
**Complexity**: 2 points
|
||||
**Dependencies**: AZ-285
|
||||
**Component**: Blackbox Tests
|
||||
**Tracker**: AZ-289
|
||||
**Epic**: AZ-284
|
||||
|
||||
## Scenarios
|
||||
|
||||
- BT-08: Route with requestMaps=true — verify mapsReady=true, stitchedImagePath, csvFilePath
|
||||
- BT-09: Route with createTilesZip=true — verify ZIP contents match CSV tile count, directory structure preserved
|
||||
|
||||
## System Under Test Boundary
|
||||
|
||||
Tests drive the API via HTTP endpoints. No internal module stubs — all services run in Docker as production.
|
||||
|
||||
## Test Data
|
||||
|
||||
Route points from test_coordinates.md (ROUTE-02, ROUTE-03)
|
||||
|
||||
## Notes
|
||||
|
||||
BT-10/11/12 are already covered by existing integration tests (ComplexRouteTests, ExtendedRouteTests).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
AC-1: Route map processing completes within 180s
|
||||
AC-2: ZIP file structure is validated (entry count matches CSV, path prefix "tiles/")
|
||||
@@ -0,0 +1,50 @@
|
||||
# Non-Functional Tests
|
||||
|
||||
**Task**: AZ-290_nonfunctional_tests
|
||||
**Name**: Non-functional tests: perf, resilience, security, limits
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: AZ-285
|
||||
**Component**: Blackbox Tests
|
||||
**Tracker**: AZ-290
|
||||
**Epic**: AZ-284
|
||||
|
||||
## Performance Scenarios (PT-01 through PT-06)
|
||||
|
||||
- PT-01: Tile download latency <30s
|
||||
- PT-02: Cached tile retrieval <500ms
|
||||
- PT-03/04: Region processing throughput
|
||||
- PT-05: 5 concurrent regions all complete
|
||||
- PT-06: Route interpolation <5s
|
||||
|
||||
## Resilience Scenarios (RS-01 through RS-06)
|
||||
|
||||
- RS-01: API starts with database ready
|
||||
- RS-02: Migrations run on fresh DB
|
||||
- RS-03: Region processing handles tile failures
|
||||
- RS-04: Queue rejects overflow (capacity 1000)
|
||||
- RS-05: Max 4 concurrent downloads
|
||||
- RS-06: Route processing completes all regions
|
||||
|
||||
## Security Scenarios (SEC-01 through SEC-04)
|
||||
|
||||
- SEC-01: SQL injection via coordinates
|
||||
- SEC-02: Path traversal in tile serving
|
||||
- SEC-03: Oversized region request
|
||||
- SEC-04: Malformed JSON
|
||||
|
||||
## Resource Limit Scenarios (RL-01 through RL-04)
|
||||
|
||||
- RL-01: ZIP ≤ 50MB
|
||||
- RL-02: Queue capacity 1000
|
||||
- RL-03: Concurrent download semaphore (4)
|
||||
- RL-04: Concurrent region processing (20)
|
||||
|
||||
## System Under Test Boundary
|
||||
|
||||
All tests drive the system via HTTP API or observe Docker container behavior.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
AC-1: Performance scripts (scripts/run-performance-tests.sh) pass thresholds
|
||||
AC-2: Resilience tests verify state transitions and resource limits
|
||||
AC-3: Security tests confirm no injection or traversal vulnerabilities
|
||||
@@ -0,0 +1,50 @@
|
||||
# Testability Refactoring — List of Changes
|
||||
|
||||
Mode: guided
|
||||
Source: autodev-testability-analysis + architecture-compliance-baseline F1/F2
|
||||
|
||||
## TC-01: Move DownloadedTileInfoV2 to Common
|
||||
|
||||
**Files**: `SatelliteProvider.Services/GoogleMapsDownloaderV2.cs` → `SatelliteProvider.Common/DTO/DownloadedTileInfoV2.cs`
|
||||
**Problem**: `DownloadedTileInfoV2` is defined in Services alongside the concrete downloader. The interface in Common cannot reference it, blocking the interface fix.
|
||||
**Change**: Move the record to `SatelliteProvider.Common.DTO`. Update namespace references in GoogleMapsDownloaderV2 and TileService.
|
||||
**Risk**: Low — record has no dependencies on Services-specific types.
|
||||
|
||||
## TC-02: Update ISatelliteDownloader interface
|
||||
|
||||
**Files**: `SatelliteProvider.Common/Interfaces/ISatelliteDownloader.cs`
|
||||
**Problem**: Current interface has a single method `GetTiles` with a signature that doesn't match any usage in the codebase (dead code, per baseline F2).
|
||||
**Change**: Replace with the two methods actually needed by callers:
|
||||
- `DownloadSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken token = default)`
|
||||
- `GetTilesWithMetadataAsync(GeoPoint centerGeoPoint, double radiusM, int zoomLevel, IEnumerable<ExistingTileInfo> existingTiles, CancellationToken token = default)`
|
||||
Introduce `ExistingTileInfo` record in Common to avoid referencing DataAccess.TileEntity from the interface.
|
||||
**Risk**: Low — the old interface was unused.
|
||||
|
||||
## TC-03: GoogleMapsDownloaderV2 implements ISatelliteDownloader
|
||||
|
||||
**Files**: `SatelliteProvider.Services/GoogleMapsDownloaderV2.cs`
|
||||
**Problem**: Concrete class does not implement the interface, so it cannot be swapped in tests (baseline F1).
|
||||
**Change**: Add `: ISatelliteDownloader` to class declaration. Adjust `GetTilesWithMetadataAsync` parameter from `IEnumerable<TileEntity>` to `IEnumerable<ExistingTileInfo>`.
|
||||
**Risk**: Medium — method signature change requires updating callers.
|
||||
**Dependencies**: TC-01, TC-02
|
||||
|
||||
## TC-04: TileService depends on ISatelliteDownloader
|
||||
|
||||
**Files**: `SatelliteProvider.Services/TileService.cs`
|
||||
**Problem**: Constructor takes `GoogleMapsDownloaderV2` (concrete). Cannot mock in unit tests.
|
||||
**Change**: Change constructor parameter and field type from `GoogleMapsDownloaderV2` to `ISatelliteDownloader`. Map `TileEntity` to `ExistingTileInfo` before calling the interface.
|
||||
**Risk**: Low — no public API change.
|
||||
**Dependencies**: TC-02, TC-03
|
||||
|
||||
## TC-05: Update DI registration and API endpoint
|
||||
|
||||
**Files**: `SatelliteProvider.Api/Program.cs`
|
||||
**Problem**: `GoogleMapsDownloaderV2` registered as concrete singleton. `ServeTile` endpoint injects concrete class directly.
|
||||
**Change**: Register as `ISatelliteDownloader` → `GoogleMapsDownloaderV2`. Change `ServeTile` to inject `ISatelliteDownloader`.
|
||||
**Risk**: Low — behavior unchanged, just the injection type.
|
||||
**Dependencies**: TC-03
|
||||
|
||||
## Deferred to Step 8 Refactor
|
||||
|
||||
- **Medium finding: ServeTile bypasses service layer** — The endpoint directly uses `ITileRepository` and the downloader instead of `ITileService`. Moving this logic into the service layer would be cleaner but changes module boundaries (not allowed in testability step).
|
||||
- **Medium finding: No physical boundaries in Services project** — All services share one project. Splitting into separate projects is a structural change beyond testability scope.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Testability Changes Summary
|
||||
|
||||
## Applied Changes
|
||||
|
||||
### TC-01: Moved DownloadedTileInfoV2 to Common/DTO
|
||||
- **Created**: `SatelliteProvider.Common/DTO/DownloadedTileInfoV2.cs`
|
||||
- **Created**: `SatelliteProvider.Common/DTO/ExistingTileInfo.cs`
|
||||
- **Modified**: `SatelliteProvider.Services/GoogleMapsDownloaderV2.cs` — removed inline record definition, added import
|
||||
- **Why**: The return type of the downloader must be accessible from the interface in Common
|
||||
|
||||
### TC-02: Updated ISatelliteDownloader interface
|
||||
- **Modified**: `SatelliteProvider.Common/Interfaces/ISatelliteDownloader.cs`
|
||||
- **Old API**: `Task GetTiles(GeoPoint, double, int, CancellationToken)` — never implemented, dead code
|
||||
- **New API**: `DownloadSingleTileAsync(...)` + `GetTilesWithMetadataAsync(...)` — matches actual usage
|
||||
- **Why**: Previous interface was dead code; new interface enables mocking the downloader in tests
|
||||
|
||||
### TC-03: GoogleMapsDownloaderV2 implements ISatelliteDownloader
|
||||
- **Modified**: `SatelliteProvider.Services/GoogleMapsDownloaderV2.cs`
|
||||
- **Change**: Class declaration now includes `: ISatelliteDownloader`; `GetTilesWithMetadataAsync` parameter changed from `IEnumerable<TileEntity>` to `IEnumerable<ExistingTileInfo>`
|
||||
- **Why**: Concrete class must implement the interface for DI and mocking to work
|
||||
|
||||
### TC-04: TileService depends on ISatelliteDownloader
|
||||
- **Modified**: `SatelliteProvider.Services/TileService.cs`
|
||||
- **Change**: Constructor parameter and field type changed from `GoogleMapsDownloaderV2` to `ISatelliteDownloader`; added mapping from `TileEntity` to `ExistingTileInfo` before calling the interface
|
||||
- **Why**: Unit tests can now mock the downloader via the interface
|
||||
|
||||
### TC-05: Updated DI registration and API endpoints
|
||||
- **Modified**: `SatelliteProvider.Api/Program.cs`
|
||||
- **Changes**:
|
||||
- DI: `AddSingleton<GoogleMapsDownloaderV2>()` → `AddSingleton<ISatelliteDownloader, GoogleMapsDownloaderV2>()`
|
||||
- `ServeTile` endpoint: parameter `GoogleMapsDownloaderV2 downloader` → `ISatelliteDownloader downloader`
|
||||
- `GetTileByLatLon` endpoint: parameter `GoogleMapsDownloaderV2 downloader` → `ISatelliteDownloader downloader`
|
||||
- **Why**: Endpoints should inject the interface, not the concrete class
|
||||
|
||||
## Deferred to Step 8
|
||||
|
||||
- ServeTile and GetTileByLatLon endpoints bypass the service layer (inject repository + downloader directly)
|
||||
- No physical project boundaries between logical components in the Services project
|
||||
@@ -0,0 +1,13 @@
|
||||
# Autodev State
|
||||
|
||||
## Current Step
|
||||
flow: existing-code
|
||||
step: 6
|
||||
name: Implement Tests
|
||||
status: in_progress
|
||||
sub_step:
|
||||
phase: 0
|
||||
name: awaiting-invocation
|
||||
detail: ""
|
||||
retry_count: 0
|
||||
cycle: 1
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
API_URL="${API_URL:-http://localhost:18980}"
|
||||
|
||||
cleanup() {
|
||||
echo "Cleaning up..."
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "=== Satellite Provider Performance Tests ==="
|
||||
echo "API URL: $API_URL"
|
||||
echo ""
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
check_threshold() {
|
||||
local test_name="$1"
|
||||
local actual_ms="$2"
|
||||
local threshold_ms="$3"
|
||||
|
||||
if (( actual_ms <= threshold_ms )); then
|
||||
echo " ✓ $test_name: ${actual_ms}ms (threshold: ${threshold_ms}ms)"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo " ✗ $test_name: ${actual_ms}ms EXCEEDS threshold ${threshold_ms}ms"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "PT-02: Cached Tile Retrieval Latency (threshold: 500ms)"
|
||||
START=$(date +%s%N)
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$API_URL/api/satellite/tiles/latlon?Latitude=47.461747&Longitude=37.647063&ZoomLevel=18")
|
||||
END=$(date +%s%N)
|
||||
ELAPSED_MS=$(( (END - START) / 1000000 ))
|
||||
|
||||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||||
check_threshold "Cached tile retrieval" "$ELAPSED_MS" 500
|
||||
else
|
||||
echo " ✗ PT-02: HTTP $HTTP_CODE (expected 200) - tile may not be cached yet"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "PT-06: Route Point Interpolation Speed (threshold: 5000ms)"
|
||||
ROUTE_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
|
||||
BODY="{\"id\":\"$ROUTE_ID\",\"name\":\"Perf Test\",\"regionSizeMeters\":300,\"zoomLevel\":18,\"points\":[{\"lat\":48.276067,\"lon\":37.384458},{\"lat\":48.270740,\"lon\":37.374029}]}"
|
||||
|
||||
START=$(date +%s%N)
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" -d "$BODY" "$API_URL/api/satellite/route")
|
||||
END=$(date +%s%N)
|
||||
ELAPSED_MS=$(( (END - START) / 1000000 ))
|
||||
|
||||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||||
check_threshold "Route creation (2 points)" "$ELAPSED_MS" 5000
|
||||
else
|
||||
echo " ✗ PT-06: HTTP $HTTP_CODE (expected 200)"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Performance Test Summary ==="
|
||||
echo " Passed: $PASS"
|
||||
echo " Failed: $FAIL"
|
||||
echo ""
|
||||
|
||||
if [[ $FAIL -gt 0 ]]; then
|
||||
echo "FAILED: $FAIL performance threshold(s) exceeded"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "ALL PERFORMANCE TESTS PASSED"
|
||||
exit 0
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
cleanup() {
|
||||
docker compose -f "$PROJECT_ROOT/docker-compose.yml" -f "$PROJECT_ROOT/docker-compose.tests.yml" down --remove-orphans 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "=== Satellite Provider Test Suite ==="
|
||||
echo ""
|
||||
|
||||
if [[ "${1:-}" == "--unit-only" ]]; then
|
||||
echo "Running unit tests only..."
|
||||
docker run --rm -v "$PROJECT_ROOT:/src" -w /src mcr.microsoft.com/dotnet/sdk:8.0 \
|
||||
dotnet test SatelliteProvider.Tests/SatelliteProvider.Tests.csproj \
|
||||
--no-restore --configuration Release \
|
||||
--logger "console;verbosity=normal"
|
||||
echo ""
|
||||
echo "=== Unit tests complete ==="
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Running full test suite (unit + integration)..."
|
||||
echo ""
|
||||
|
||||
echo "Step 1: Unit tests"
|
||||
docker run --rm -v "$PROJECT_ROOT:/src" -w /src mcr.microsoft.com/dotnet/sdk:8.0 \
|
||||
sh -c "dotnet restore SatelliteProvider.sln && dotnet test SatelliteProvider.Tests/SatelliteProvider.Tests.csproj --no-restore --configuration Release --logger 'console;verbosity=normal'"
|
||||
|
||||
echo ""
|
||||
echo "Step 2: Integration tests (Docker Compose)"
|
||||
docker compose -f "$PROJECT_ROOT/docker-compose.yml" -f "$PROJECT_ROOT/docker-compose.tests.yml" \
|
||||
up --build --abort-on-container-exit --exit-code-from integration-tests
|
||||
|
||||
echo ""
|
||||
echo "=== All tests passed ==="
|
||||
Reference in New Issue
Block a user