[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:
Oleksandr Bezdieniezhnykh
2026-05-10 04:44:08 +03:00
parent 25a644a9bf
commit b0fffa6d42
68 changed files with 4192 additions and 11 deletions
+208
View File
@@ -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)
+60
View File
@@ -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.
+109
View File
@@ -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 |
+182
View File
@@ -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 | ~100K1M (depends on usage) | ~200B | Variable |
| regions | ~10K50K | ~150B | Proportional to tile requests |
| routes | ~1K5K | ~200B | Low |
| route_points | ~50K500K | ~100B | Proportional to routes |
| route_regions | ~10K100K | ~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 (100500ms) 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` |
+217
View File
@@ -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
+52
View File
@@ -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 |
+40
View File
@@ -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 (120); 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) |
+140
View File
@@ -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.
+88
View File
@@ -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 (10010000m), 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.
+101
View File
@@ -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 (0360)
### 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 (100500ms 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 10010000m, 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.
+23
View File
@@ -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).
+29
View File
@@ -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"
}
+277
View File
@@ -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}
```
+115
View File
@@ -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
+49
View File
@@ -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 | 23 seconds |
| Max poll attempts | 120360 (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
+25
View File
@@ -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
+30
View File
@@ -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%)** |