Files
Oleksandr Bezdieniezhnykh 5d84d2839e
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
[AZ-505] Test-spec sync + task-mode doc updates for cycle 6
Step 12 (Test-Spec Sync, cycle-update mode):
- blackbox-tests.md: append BT-23..BT-26 for AZ-505's new
  observable behaviors (inventory order/shape; leaflet
  most-recent via location_hash; HTTP/2 multiplex over TLS+ALPN;
  request validation).
- performance-tests.md: append PT-09 (inventory p95 ≤ 1000ms /
  2500 tiles); records cycle-6 measured p95=66ms; documents
  promotion path to scripts/run-performance-tests.sh if budget
  ever tightens.
- traceability-matrix.md: resolve the 5 AZ-503 deferrals
  (AC-5/6/9/10/12) by pointing at AZ-505 test names + add 7
  AZ-505 AC rows (AC-1..AC-7) + bump totals (90 -> 94 tests,
  56/56 -> 63/63 in-scope) + add cycle-6 coverage shape notes
  (budget relaxation rationale, voting-filter deferral note,
  TLS+ALPN pivot, NFR propagation).

Step 13 (Update Docs, task mode):
- common_dtos.md: add 5 new TileInventory DTOs.
- common_interfaces.md: add ITileService.GetInventoryAsync.
- services_tile_service.md: document TileService.GetInventoryAsync
  steps + the XOR-validation-in-handler note.
- dataaccess_migrator.md: bump migration count 14 -> 15;
  describe migration 015 (AZ-505 leaflet covering index, lock
  window, INCLUDE-list trade-off).
- system-flows.md: add F7 (Leaflet Tile Serving, AZ-310 +
  AZ-505 location_hash rewire + TLS+ALPN) and F8 (Tile
  Inventory Bulk Lookup) with sequence diagrams, validation
  surface, and AC-4 perf evidence. Update Flow Inventory +
  Dependencies tables accordingly.
- glossary.md: add "Tile Inventory" entry pointing at the
  v1.0.0 contract.
- ripple_log_cycle6.md: new file — exhaustive reverse-dependency
  analysis confirms zero stale downstream module docs.

Advance autodev state from step 11 -> 14 (skipping 12+13 as
completed in this commit; auto-chain through Step 14 = Security
Audit optional gate).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 22:29:22 +03:00

5.8 KiB

Module: Services/TileService

Purpose

Orchestrates tile downloading and persistence. Bridges the downloader (Google Maps) with the tile repository (PostgreSQL), handling in-memory caching, entity creation, and metadata mapping. Single ownership point for all tile read/write business logic — both region-batch and single-tile API endpoints route through this service.

csproj: SatelliteProvider.Services.TileDownloader/TileService.cs

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 — most-recent across sources per (latitude, longitude, tile_zoom, tile_size_meters) (AZ-484 selection rule applied by TileRepository.GetTilesByRegionAsync via DISTINCT ON)
    2. Calls ISatelliteDownloader.GetTilesWithMetadataAsync with existing tiles to skip
    3. Creates TileEntity for each newly downloaded tile and inserts via repository (per-source UPSERT keyed on (latitude, longitude, tile_zoom, tile_size_meters, source))
    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
  • GetOrDownloadTileAsync(int z, int x, int y, CancellationToken) → Task<TileBytes> (AZ-310): cache → repository → downloader fallback for single Z/X/Y serving
  • DownloadAndStoreSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken) → Task<TileMetadata> (AZ-311): download one tile by lat/lon, persist, return metadata
  • GetInventoryAsync(TileInventoryRequest request, CancellationToken) → Task<TileInventoryResponse> (AZ-505): bulk per-cell metadata read for POST /api/satellite/tiles/inventory. Steps:
    1. Project the request to an ordered Guid[] of location_hash values — either by computing Uuidv5.LocationHashForTile(z, x, y) per entry (Form A request.Tiles) or by echoing the caller-supplied hashes (Form B request.LocationHashes). The request-order vector is retained so step 3 can shape the response in input order.
    2. Call ITileRepository.GetTilesByLocationHashesAsync(hashes, CancellationToken) once. The repository returns the most-recent row per hash ((captured_at DESC, updated_at DESC, id DESC) LIMIT 1 per location_hash); this is the AZ-484 / AZ-503-foundation selection rule preserved at the bulk layer.
    3. Build a TileInventoryEntry[] of the same length as the input vector. For each request slot: emit Present=false (only LocationHash populated) when no row was returned for that hash; otherwise emit Present=true with Id / CapturedAt / Source / FlightId / ResolutionMPerPx populated from the row. Order matches the request — duplicate hashes in the request produce duplicate entries pointing at the same row (tile-inventory.md v1.0.0 Inv-2 / Inv-3).
    4. XOR validation (both populated / neither populated) and the 5000-entry cap are NOT enforced here — they live in the API handler (GetTilesInventory in Program.cs) so the HTTP-layer error contract is single-sourced and ITileService.GetInventoryAsync can be called from non-HTTP contexts without re-implementing the gate.

Internal Logic

  • New rows write Version = null and MapsVersion = null (post-AZ-357 / AZ-373); the version and maps_version columns are retained for backward compatibility with pre-existing rows
  • AZ-484: BuildTileEntity stamps every newly downloaded row with Source = TileSourceConverter.ToWireValue(TileSource.GoogleMaps) (wire value "google_maps") and CapturedAt = DateTime.UtcNow. The Google Maps download path is the only producer of 'google_maps' rows; UAV ingestion (separate task) is the only producer of 'uav' rows.
  • AZ-503: BuildTileEntity computes deterministic identity fields — Id = Uuidv5.Create(Uuidv5.TileNamespace, "{z}/{x}/{y}/google_maps/00000000-0000-0000-0000-000000000000"), LocationHash = Uuidv5.Create(Uuidv5.TileNamespace, "{z}/{x}/{y}"), ContentSha256 = SHA256.HashData(<jpeg bytes from disk>). FlightId is always null for Google Maps tiles. No Guid.NewGuid() remains on this path.
  • MapToMetadata(TileEntity) → TileMetadata: entity-to-DTO mapping (static helper); MapsVersion is no longer projected onto TileMetadata / DownloadTileResponse. Source, CapturedAt, FlightId, LocationHash, ContentSha256 are not currently projected to the public DTO (no API contract change observable for AZ-484 or AZ-503).
  • TileSizePixels sourced from MapConfig.TileSizePixels (default 256, post-AZ-371); image type fixed at "jpg"
  • IMemoryCache keyed by (z, x, y) with 1h absolute / 30min sliding expiration; populated on first hit and on downloader fallback

Dependencies

  • ISatelliteDownloader (resolved via DI; concrete is GoogleMapsDownloaderV2)
  • ITileRepository
  • IMemoryCache (registered by AddTileDownloader())
  • SatelliteProvider.Common.DTO — GeoPoint, TileMetadata, TileBytes
  • SatelliteProvider.Common.EnumsTileSource, TileSourceConverter (AZ-484)
  • SatelliteProvider.Common.Utils.Uuidv5 (AZ-503) — deterministic UUIDv5 generator + TileNamespace constant
  • System.Security.Cryptography.SHA256 (AZ-503) — content digest
  • 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.