Step 12 (Test-Spec Sync) - cycle-update mode
- traceability-matrix: 8 AZ-500 AC rows + .NET 10 runtime
restriction supersession + Cycle-4 coverage shape note
(no new tests; ACs verified by re-running existing 78-test
suite + build pipeline + manifest grep)
Step 13 (Update Docs) - task mode
- FINAL_report, 00_discovery, architecture, module-layout,
api_program, tests_unit: .NET 8 -> .NET 10 / C# 12 -> 14 /
Swashbuckle 6.6.2 -> 10.1.7 + Microsoft.OpenApi 2.x
refactor note in api_program; Serilog.AspNetCore 8.0.3
fallback documented inline per AZ-500 Risk #4
- deployment/{containerization, ci_cd_pipeline}: Docker
aspnet/sdk:8.0 -> :10.0
- ripple_log_cycle4: empty import-graph ripple recorded
(Program.cs is entry point; ParameterDescriptionFilter only
consumed by Program.cs; csproj/global.json/Dockerfile have
no import edges)
Step 14 (Security Audit) - resume mode
- dependency_scan_cycle4: AZ-500 19-package delta scanned;
cycle-3 D1+D3 (CVE-2026-26130) closed by major-version
bump; cycle-3 D2 (Test.Sdk 17.8.0 NuGet.Frameworks flag)
carried over - explicitly out of AZ-500 scope
- security_report_cycle4: PASS_WITH_WARNINGS (only carry-over
Medium open; AZ-500 introduced 0 new Critical/High); cycle-3
static_analysis/owasp_review/infrastructure_review carried
forward unchanged (AZ-500 made no source-level edits to
those surfaces)
Step 15 (Performance Test) - perf mode, full default-param run
- perf_2026-05-12_cycle4: 7 Pass + 1 Unverified (PT-08 hit
pre-existing scripts/run-performance-tests.sh:417 grep-
pipefail bug, NOT a .NET 10 regression)
- PT-07 warm p95 = 301ms (7.7x improvement vs cycle-3 short
variant - .NET 10 pipeline + N=20 dilution); cold p95 =
2782ms (-14%); PT-06 90ms (-49%)
- AZ-500 NFR (Performance) MET for 7/8 scenarios
- Cycle-3 perf-harness leftover updated with replay #3
results; STAYS OPEN per AZ-500 Constraint (deletes only on
fully clean run)
Recommended follow-up PBIs (out of cycle-4 scope, surfaced for
the backlog):
- 1 SP fix scripts/run-performance-tests.sh:416-417 grep-
pipefail (replace grep -o ... | wc -l with grep -c ... ||
true) - unblocks PT-08 + closes the cycle-3 perf leftover
- 3 SP migrate WithOpenApi(...) callsites to ASP.NET Core 10
minimal-API metadata extensions (clears 8 ASPDEPR002
warnings; recorded in batch_01_cycle4_review.md)
- 1 SP Microsoft.OpenApi 2.x nullable cleanup (CS8604 in
ParameterDescriptionFilter.cs:25)
- 1 SP bump Microsoft.NET.Test.Sdk 17.8.0 -> 17.13.0+
(closes cycle-3 D2 NuGet.Frameworks transitive flag)
Co-authored-by: Cursor <cursoragent@cursor.com>
Satellite Provider API
A .NET 8.0 microservice for downloading, managing, and serving satellite imagery tiles from Google Maps. Supports region-based tile requests, route planning with automatic intermediate point generation, and geofencing capabilities.
Features
- Tile Management: Download and cache satellite imagery tiles from Google Maps
- Region Requests: Request tiles for specific geographic areas (100m - 10km squares)
- Route Planning: Create routes with automatic intermediate point interpolation (~200m intervals)
- Geofencing: Define polygon boundaries to filter regions inside/outside zones
- Image Stitching: Combine tiles into single images for regions
- Tile Packaging: Create ZIP archives of route tiles (max 50MB)
- Async Processing: Queue-based background processing for large requests
- Caching: Reuse previously downloaded tiles to minimize redundant downloads
- REST API: OpenAPI/Swagger documented endpoints
Prerequisites
- Docker and Docker Compose
- .NET 8.0 SDK (for local development)
- PostgreSQL (provided via Docker)
Quick Start
Run the Service
docker-compose up --build
The API will be available at http://localhost:5100
Swagger documentation: http://localhost:5100/swagger
Run with Tests
docker-compose -f docker-compose.yml -f docker-compose.tests.yml up --build --abort-on-container-exit
This command:
- Builds and starts all services
- Runs integration tests
- Exits when tests complete
Architecture
The service follows a layered architecture:
┌─────────────────────────────────────┐
│ API Layer (ASP.NET Core) │ HTTP endpoints
├─────────────────────────────────────┤
│ Services (Business Logic) │ Processing, validation
├─────────────────────────────────────┤
│ Data Access (Dapper + Repos) │ Database operations
├─────────────────────────────────────┤
│ PostgreSQL Database │ Persistence
└─────────────────────────────────────┘
Projects
- SatelliteProvider.Api: Web API and endpoints
- SatelliteProvider.Services: Business logic and processing
- SatelliteProvider.DataAccess: Database access and migrations
- SatelliteProvider.Common: Shared DTOs, interfaces, utilities
- SatelliteProvider.IntegrationTests: Integration test suite
- SatelliteProvider.Tests: Unit tests
API Endpoints
Download Single Tile
GET /api/satellite/tiles/latlon?Latitude={lat}&Longitude={lon}&ZoomLevel={zoom}
Downloads a single tile at specified coordinates and zoom level.
Parameters:
Latitude(double): Center latitudeLongitude(double): Center longitudeZoomLevel(int): Zoom level (higher = more detail, max ~20)
Response:
{
"id": "uuid",
"zoomLevel": 18,
"latitude": 37.7749,
"longitude": -122.4194,
"tileSizeMeters": 38.2,
"tileSizePixels": 256,
"imageType": "jpg",
"mapsVersion": "downloaded_2024-11-20",
"version": 2024,
"filePath": "18/158/90.jpg",
"createdAt": "2024-11-20T10:30:00Z",
"updatedAt": "2024-11-20T10:30:00Z"
}
Request Region Tiles
POST /api/satellite/request
Content-Type: application/json
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"latitude": 37.7749,
"longitude": -122.4194,
"sizeMeters": 500,
"zoomLevel": 18,
"stitchTiles": false
}
Requests tiles for a square region. Processing happens asynchronously.
Parameters:
id(guid): Unique identifier for the regionlatitude(double): Center latitudelongitude(double): Center longitudesizeMeters(double): Square side length (100-10000 meters)zoomLevel(int): Zoom level (default: 18)stitchTiles(bool): Create single stitched image (default: false)
Response:
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "pending",
"csvFilePath": null,
"summaryFilePath": null,
"tilesDownloaded": 0,
"tilesReused": 0,
"createdAt": "2024-11-20T10:30:00Z",
"updatedAt": "2024-11-20T10:30:00Z"
}
Get Region Status
GET /api/satellite/region/{id}
Check processing status and get file paths when complete.
Status Values:
pending: Queued for processingprocessing: Currently downloading tilescompleted: All tiles ready, files createdfailed: Processing failed
Response (completed):
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "completed",
"csvFilePath": "./ready/region_3fa85f64-5717-4562-b3fc-2c963f66afa6_ready.csv",
"summaryFilePath": "./ready/region_3fa85f64-5717-4562-b3fc-2c963f66afa6_summary.txt",
"tilesDownloaded": 12,
"tilesReused": 4,
"createdAt": "2024-11-20T10:30:00Z",
"updatedAt": "2024-11-20T10:32:15Z"
}
Create Route
POST /api/satellite/route
Content-Type: application/json
{
"id": "7fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "City Tour Route",
"description": "Downtown to waterfront",
"regionSizeMeters": 200,
"zoomLevel": 18,
"points": [
{ "latitude": 37.7749, "longitude": -122.4194, "name": "Start" },
{ "latitude": 37.7849, "longitude": -122.4094, "name": "Middle" },
{ "latitude": 37.7949, "longitude": -122.3994, "name": "End" }
],
"geofences": {
"polygons": [
{
"name": "Restricted Area",
"points": [
{ "latitude": 37.775, "longitude": -122.420 },
{ "latitude": 37.776, "longitude": -122.420 },
{ "latitude": 37.776, "longitude": -122.418 },
{ "latitude": 37.775, "longitude": -122.418 }
]
}
],
"direction": "inside"
},
"requestMaps": true,
"createTilesZip": true
}
Creates a route with intermediate points calculated automatically.
Parameters:
id(guid): Route identifiername(string): Route namedescription(string): Optional descriptionregionSizeMeters(double): Size of region at each pointzoomLevel(int): Zoom level for tilespoints(array): Original route pointsgeofences(object): Optional polygon boundariespolygons: Array of polygon definitionsdirection: "inside" or "outside" (filter regions)
requestMaps(bool): Automatically request tiles for route regionscreateTilesZip(bool): Create ZIP file of all tiles
Response:
{
"id": "7fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "City Tour Route",
"description": "Downtown to waterfront",
"regionSizeMeters": 200,
"zoomLevel": 18,
"totalDistanceMeters": 2486.5,
"totalPoints": 15,
"points": [
{
"sequenceNumber": 0,
"latitude": 37.7749,
"longitude": -122.4194,
"pointType": "original",
"segmentIndex": 0,
"distanceFromPrevious": 0
},
{
"sequenceNumber": 1,
"latitude": 37.7765,
"longitude": -122.4177,
"pointType": "intermediate",
"segmentIndex": 0,
"distanceFromPrevious": 198.3
}
],
"regions": [
"region-id-1",
"region-id-2"
],
"tilesZipPath": "./ready/route_7fa85f64-5717-4562-b3fc-2c963f66afa6_tiles.zip",
"createdAt": "2024-11-20T10:30:00Z"
}
Get Route
GET /api/satellite/route/{id}
Retrieve route details including all interpolated points.
Configuration
Configuration is managed through appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=satelliteprovider;Username=postgres;Password=postgres"
},
"MapConfig": {
"Service": "GoogleMaps",
"ApiKey": "your-google-maps-api-key"
},
"StorageConfig": {
"TilesDirectory": "./tiles",
"ReadyDirectory": "./ready"
},
"ProcessingConfig": {
"MaxConcurrentDownloads": 4,
"MaxConcurrentRegions": 20,
"DefaultZoomLevel": 20,
"QueueCapacity": 1000,
"DelayBetweenRequestsMs": 50,
"SessionTokenReuseCount": 100
}
}
Environment Variables (Docker)
Override settings via environment variables in docker-compose.yml:
environment:
- ConnectionStrings__DefaultConnection=Host=db;Database=satelliteprovider;Username=postgres;Password=postgres
- MapConfig__ApiKey=your-api-key-here
File Storage
Tiles
Downloaded tiles are stored in:
./tiles/{zoomLevel}/{x}/{y}.jpg
Output Files
CSV File (region_{id}_ready.csv):
Comma-separated list of tiles covering the region, ordered top-left to bottom-right:
latitude,longitude,filepath
37.7750,-122.4195,18/158/90.jpg
37.7750,-122.4193,18/158/91.jpg
Summary File (region_{id}_summary.txt):
Processing statistics:
Region: 3fa85f64-5717-4562-b3fc-2c963f66afa6
Size: 500m x 500m
Center: 37.7749, -122.4194
Zoom Level: 18
Tiles Downloaded: 12
Tiles Reused: 4
Total Tiles: 16
Processing Time: 3.5s
Stitched Image (region_{id}_stitched.jpg):
Single combined image when stitchTiles: true
Route ZIP (route_{id}_tiles.zip):
ZIP archive of all tiles for route when createTilesZip: true (max 50MB)
Database Schema
Tables
tiles: Cached satellite tiles
- Unique per (latitude, longitude, tile_size_meters, zoom_level)
- Tracks maps version for cache invalidation
regions: Region requests and processing status
- Status tracking: pending → processing → completed/failed
- Links to output files when complete
routes: Route definitions
- Stores original configuration
- Links to calculated points and regions
route_points: All points (original + interpolated)
- Sequenced in route order
- Type: "original" or "intermediate"
- Includes distance calculations
route_regions: Junction table
- Links routes to regions for tile requests
- Tracks geofence status (inside/outside)
Development
Building Locally
cd SatelliteProvider.Api
dotnet build
dotnet run
Running Tests
Unit Tests:
cd SatelliteProvider.Tests
dotnet test
Integration Tests:
docker-compose -f docker-compose.yml -f docker-compose.tests.yml up --build --abort-on-container-exit
Database Migrations
Migrations run automatically on startup. SQL files are located in:
SatelliteProvider.DataAccess/Migrations/
To add a new migration:
- Create
NNN_DescriptiveName.sql(increment number) - Set Build Action to
EmbeddedResource - Restart application
Logging
Logs are written to:
- Console (stdout)
./logs/satellite-provider-YYYYMMDD.log
Log level can be adjusted in appsettings.json under Serilog:MinimumLevel.
Performance Considerations
Tile Caching
- Tiles are cached indefinitely (no expiration)
- Reused across multiple region/route requests
- Reduces redundant downloads significantly
Concurrent Processing
- Configurable concurrent downloads (default: 4)
- Configurable concurrent regions (default: 20)
- Queue capacity: 1000 requests
Rate Limiting
- Delay between requests: 50ms default
- Session token reuse: 100 tiles per token
- Helps avoid Google Maps rate limits
Geofencing
- Uses point-in-polygon algorithm
- Reduces unnecessary tile downloads
- Spatial index on polygon data
Troubleshooting
Service won't start
- Check Docker is running
- Verify ports 5100 and 5432 are available
- Check logs:
docker-compose logs api
Tiles not downloading
- Verify Google Maps API key is configured
- Check internet connectivity
- Review logs for HTTP errors
- Check rate limiting settings
Region stuck in "processing"
- Check background service is running:
docker-compose logs api - Verify database connection
- Check queue capacity not exceeded
- Review logs for exceptions
Tests failing
- Ensure all containers are healthy before tests run
- Check test container logs:
docker-compose logs tests - Verify database migrations completed
- Check network connectivity between containers
Out of disk space
- Tiles accumulate in
./tiles/directory - Implement cleanup strategy for old tiles
- Monitor disk usage
License
This project is proprietary software.
Support
For issues and questions, please contact the development team.