# 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 | | F7 | Leaflet Tile Serving | HTTP GET /tiles/{z}/{x}/{y} | WebApi, TileService, DataAccess, FileSystem | High | | F8 | Tile Inventory Bulk Lookup | HTTP POST /api/satellite/tiles/inventory | WebApi, TileService, DataAccess | High | ## 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 | — | | F7 | F1 or F3 must have populated the tile (else 404) | F1, F3, F8 (shares `tiles.location_hash`) | | F8 | — | F1, F3, F7 (shares `tiles.location_hash`) | --- ## 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} ``` --- ## Flow F7: Leaflet Tile Serving (added AZ-310, rewired AZ-505) ### Description Leaflet (or any HTTP/2-capable client) requests a single tile body by slippy `(z, x, y)`. The handler resolves the most-recent variant across sources/flights by `location_hash` and streams the JPEG body back. AZ-505 rewired the lookup predicate from `(tile_zoom, tile_x, tile_y)` to `location_hash = Uuidv5(TileNamespace, "{z}/{x}/{y}")` so the read hits the `tiles_leaflet_path` covering index as an `Index Only Scan`; the selection rule (`captured_at DESC, updated_at DESC, id DESC LIMIT 1`) is unchanged from AZ-484 / AZ-503-foundation. Kestrel runs `Http1AndHttp2` over TLS (`https://+:8080` in dev) so ALPN multiplexes many concurrent leaflet requests on a single TLS connection. ### Sequence Diagram ```mermaid sequenceDiagram participant Client participant Kestrel participant ServeTile participant TileService participant TileRepo participant FileSystem Client->>Kestrel: GET /tiles/{z}/{x}/{y} (HTTP/2 via TLS+ALPN) Kestrel->>ServeTile: route match ServeTile->>TileService: GetOrDownloadTileAsync(z, x, y) TileService->>TileRepo: GetByTileCoordinatesAsync(z, x, y) Note over TileRepo: WHERE location_hash = $1
ORDER BY captured_at DESC, updated_at DESC, id DESC
LIMIT 1
Index Only Scan: tiles_leaflet_path alt Cached TileRepo-->>TileService: TileEntity TileService->>FileSystem: read file_path FileSystem-->>TileService: JPEG bytes TileService-->>ServeTile: TileBytes (file_path, ETag, Cache-Control) ServeTile-->>Client: 200 OK, JPEG body else Miss TileService->>TileService: download via GoogleMapsDownloaderV2 Note over TileService: persists row + on-disk path, falls through to ServeTile TileService-->>ServeTile: TileBytes ServeTile-->>Client: 200 OK, JPEG body end ``` ### Error Handling | Failure | Detection | Handling | |---------|-----------|----------| | Tile not present and downloader rejects (404 from Google Maps) | `TileService.GetOrDownloadTileAsync` propagates the downloader's `HttpRequestException` | Returns 500; `ServeTile` does NOT translate this to 404 because the predicate matched (path-traversal cases below 404 earlier in routing) | | Path traversal in `/tiles/...` segment | ASP.NET Core route binding | `400`/`404` before the handler runs (covered by SEC-02) | --- ## Flow F8: Tile Inventory Bulk Lookup (added AZ-505) ### Description Programmatic clients (httpx `http2=True`, .NET `HttpClient`, onboard cross-repo callers) post a batch of up to 5000 `(z, x, y)` triples (Form A) or up to 5000 pre-computed `location_hash` UUIDs (Form B) and get one inventory entry per input slot, in the same order. Each entry says whether the cell is present and — when present — the most-recent row's `id`, `capturedAt`, `source`, `flightId`, and `resolutionMPerPx`. No tile bodies are returned; the caller subsequently fetches bodies via F7. This is the read-half of the bulk-list contract that the onboard `gps-denied-onboard` workspace consumes to decide which Google-Maps cells it needs and which UAV variants are already on the server. ### Sequence Diagram ```mermaid sequenceDiagram participant Client participant Kestrel participant GetTilesInventory participant TileService participant TileRepo Client->>Kestrel: POST /api/satellite/tiles/inventory (JWT, Form A or B) Kestrel->>GetTilesInventory: route match GetTilesInventory->>GetTilesInventory: XOR check (both/neither populated → 400) GetTilesInventory->>GetTilesInventory: cap check (count > 5000 → 400) GetTilesInventory->>TileService: GetInventoryAsync(request) Note over TileService: Form A: compute location_hash per coord
via Uuidv5.LocationHashForTile
Form B: echo caller-supplied hashes TileService->>TileRepo: GetTilesByLocationHashesAsync(hashes) Note over TileRepo: NpgsqlCommand:
SELECT DISTINCT ON (location_hash) ...
WHERE location_hash = ANY($1::uuid[])
ORDER BY location_hash, captured_at DESC, updated_at DESC, id DESC
(bypasses Dapper IEnumerable expansion) TileRepo-->>TileService: IReadOnlyDictionary TileService->>TileService: shape into TileInventoryEntry[] in request order TileService-->>GetTilesInventory: TileInventoryResponse GetTilesInventory-->>Client: 200 OK, JSON (results in input order) ``` ### Validation Surface | Input | Detection | Response | |-------|-----------|----------| | Both `tiles` and `locationHashes` populated | Handler XOR check | 400 + ProblemDetails (`tile-inventory.md` Inv-1) | | Neither populated | Handler XOR check | 400 + ProblemDetails | | `count > 5000` (`TileInventoryLimits.MaxEntriesPerRequest`) | Handler cap check | 400 + ProblemDetails (Inv-7) | | No `Authorization: Bearer …` header | `.RequireAuthorization()` | 401 before handler runs | ### Performance p95 ≤ 1000 ms for 2500-coord batches (AZ-505 AC-4). Cycle-6 measured: p95=66ms — well under budget. The covering index (`tiles_leaflet_path`) supplies the leading `location_hash` lookup; the projection's columns beyond the INCLUDE list (`id`, `captured_at`, `flight_id`, ...) trigger a bounded heap fetch which is documented and accepted per the AZ-505 NFR.