Files
satellite-provider/_docs/02_document/components/03_tile_downloader/description.md
T
Oleksandr Bezdieniezhnykh 1802d32107 [AZ-488] UAV tile batch upload + 5-rule quality gate
Replaces the 501 stub at POST /api/satellite/upload with a multipart
batch endpoint that ingests UAV-captured tiles, runs each item through
a 5-rule quality gate, and persists accepted tiles via the AZ-484
multi-source storage path with source='uav'.

Quality gate (in fixed order, first failure wins): JPEG format
(content-type + magic), size band 5 KiB-5 MiB, exact 256x256
dimensions, captured-at age (no future >30 s skew, no older than
7 days), luminance variance on 32x32 downsample. Closed reject-reason
enumeration in v1.0.0 contract.

Authorization: custom PermissionsRequirement / PermissionsAuthorization
Handler that reads the JWT `permissions` claim (tolerates both
repeated-string and JSON-array shapes). Endpoint protected by
RequiresGpsPermission policy; 401 without token, 403 without GPS perm.

Persistence: file-first to ./tiles/uav/{z}/{x}/{y}.jpg, then
ITileRepository.InsertAsync UPSERT (per-source UPSERT contract from
AZ-484). Per-item failures reported in response without aborting the
batch. Kestrel MaxRequestBodySize and FormOptions limits set to
MaxBatchSize x MaxBytes (default 100 x 5 MiB = 500 MiB).

New frozen contract: _docs/02_document/contracts/api/uav-tile-upload.md
v1.0.0. PT-08 NFR added to performance-tests.md as Deferred (harness
work tracked in PT-07 leftover, per AZ-488 § Risk 4).

Tests: 11 quality-gate unit tests, 5 handler unit tests, 3 file-path
unit tests, 12 permission-handler unit tests, 7 integration tests
(AC-1..AC-6, AC-8). All 253 unit tests + smoke integration suite
green.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 23:50:49 +03:00

6.6 KiB
Raw Blame History

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. Since AZ-488 it also hosts the UAV upload pipeline: the UavTileQualityGate 5-rule validator and the UavTileUploadHandler that persists source='uav' rows via ITileRepository.InsertAsync.

Architectural Pattern: Service + Gateway (wraps external API with retry/throttling) + per-source quality gate (UAV upload path)

csproj: SatelliteProvider.Services.TileDownloader/SatelliteProvider.Services.TileDownloader.csproj (split out of the monolithic SatelliteProvider.Services project in epic AZ-309)

Upstream dependencies: Common (DTOs, Enums — TileSource + TileSourceConverter since AZ-484, plus UavTileMetadata / UavTileBatchUploadResponse / UavTileRejectReasons since AZ-488; GeoUtils; configs MapConfig, StorageConfig, ProcessingConfig, UavQualityConfig; RateLimitException), DataAccess (TileEntity, ITileRepository), SixLabors.ImageSharp 3.1.11 (UAV decode + variance check).

Downstream consumers: RegionProcessing and WebApi — both via ITileService from Common; WebApi also resolves IUavTileQualityGate and IUavTileUploadHandler from this component (DI only — no compile-time ProjectReference from any consumer to this project's concrete types).

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
GetOrDownloadTileAsync (AZ-310) z, x, y, CancellationToken TileBytes Yes propagated from downloader
DownloadAndStoreSingleTileAsync (AZ-311) lat, lon, zoom, CancellationToken TileMetadata Yes propagated from downloader

Service: UavTileQualityGate (implements IUavTileQualityGate, AZ-488)

Method Input Output Async Error Types
Validate imageBytes, contentType, UavTileMetadata UavTileQualityResult (accept + reason code) No none (decode exceptions caught and translated to INVALID_FORMAT)

Rules run in fixed order (Format → Size band → Dimensions → Captured-at age → Blank/uniform); first failure short-circuits. Thresholds come from UavQualityConfig. Time comes from injected TimeProvider (defaults to TimeProvider.System) for deterministic tests.

Service: UavTileUploadHandler (implements IUavTileUploadHandler, AZ-488)

Method Input Output Async Error Types
HandleAsync metadataJson, IReadOnlyList<UavUploadFile>, CancellationToken UavTileUploadHandlerResult (envelope error OR per-item response) Yes propagated IOException/UnauthorizedAccessException per item, translated to per-item STORAGE_FAILURE

Per-item flow: parse metadata JSON → reject envelope (mismatch, oversize, malformed JSON) OR run each item through IUavTileQualityGate → for accepted items, write JPEG to {StorageConfig.TilesDirectory}/uav/{zoom}/{x}/{y}.jpg then call ITileRepository.InsertAsync (per-source UPSERT) with source='uav' and the request-supplied capturedAt. File-before-row ordering keeps an orphan file (rather than a row pointing at nothing) when persistence fails.

4. Data Access Patterns

Caching Strategy

Data Cache Type TTL Invalidation
Tile bytes In-memory (IMemoryCache, owned by TileService since AZ-310) 1h absolute, 30min sliding None (manual restart)
Tile metadata Database Append-by-source per cell (AZ-484); reads return most-recent across sources Per-source UPSERT keyed on (latitude, longitude, tile_zoom, tile_size_meters, source) overwrites the existing same-source row and refreshes captured_at
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 behind ISatelliteDownloader (resolved by AZ-310 cleanup); the previous concrete-type coupling is gone.
  • 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)