Mirror of AZ-794 (inventory z/x/y rename). RequestRegionRequest.cs renames C#
props Latitude→Lat / Longitude→Lon and adds [JsonPropertyName("lat"/"lon")] so
the wire format is unambiguous under the AZ-795 strict-parsing stack
(UnmappedMemberHandling.Disallow → legacy {"latitude":..,"longitude":..} now
returns HTTP 400 instead of silently coercing).
Updates all in-repo consumers: API handler (Program.cs), integration tests
(Models.cs, RegionTests.cs, IdempotentPostTests.cs, SecurityTests.cs), the
performance harness (run-performance-tests.sh PT-03/04/05/07), and module
docs (common_dtos.md, api_program.md; system-flows.md F2 already used
lat/lon). New RegionFieldRenameTests.cs covers AC-4 both directions (new
format → 200, legacy format → 400). Smoke green; no regressions.
region-request.md contract doc not bumped here — AZ-808 publishes v1.0.0
directly with the post-rename names per AZ-812 coordination clause.
Batch 01 of cycle 8. PASS_WITH_WARNINGS (one Low DRY finding for follow-up
test-helper consolidation; details in
_docs/03_implementation/reviews/batch_01_cycle8_review.md).
Co-authored-by: Cursor <cursoragent@cursor.com>
11 KiB
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 metersAzimuth(double): bearing in degrees (0–360)
SatTile
Represents a single map tile with its spatial bounds.
X,Y(int): tile coordinates in the slippy map schemeZoom(int): zoom levelLeftTop,BottomRight(GeoPoint): computed bounding box corners (viaGeoUtils.TileToWorldPos)Url(string): download URLFileName → 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)Version(int?),FilePath(string)CreatedAt,UpdatedAt(DateTime)
RequestRegionRequest (renamed by AZ-812 cycle 8 — OSM convention)
API request body for POST /api/satellite/request (region enqueue). Defined in SatelliteProvider.Common/DTO/RequestRegionRequest.cs. Moved out of Program.cs by AZ-369.
Id(Guid),Lat(double, JSON:"lat"),Lon(double, JSON:"lon"),SizeMeters(double)ZoomLevel(int, default 18),StitchTiles(bool, default false)- AZ-812 renamed C# props
Latitude/Longitude→Lat/Lonand added[JsonPropertyName("lat")]/[JsonPropertyName("lon")]to make the wire format unambiguous. WithJsonSerializerOptions.UnmappedMemberHandling.Disallowactive (AZ-795), the oldlatitude/longitudewire shape now returns HTTP 400.
RegionRequest
Internal queue message for async region processing (not a wire-format DTO — exchanged between the API handler and RegionProcessingService background worker via IRegionRequestQueue). Distinct from RequestRegionRequest above; intentionally kept on Latitude/Longitude because the queue is in-process only.
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>)
UavTileMetadata (added AZ-488, extended AZ-503)
Per-tile metadata payload inside a UAV batch upload (POST /api/satellite/upload). Indexed-correlated with the multipart IFormFileCollection.
Latitude,Longitude(double)TileZoom(int)TileSizeMeters(double)CapturedAt(DateTime, UTC; subject to AZ-488 Rule 4 future-skew / age checks)FlightId(Guid?, JSON:"flightId") — AZ-503 optional flight identifier. When set, the per-itemtiles.idbecomesUuidv5(TileNamespace, "{z}/{x}/{y}/uav/{flightId}"), the on-disk path is./tiles/uav/{flightId}/{z}/{x}/{y}.jpg, and the UPSERT conflict key separates this row from rows belonging to other flights at the same cell. Whennull, the per-item id uses the zero-UUID00000000-0000-0000-0000-000000000000placeholder and the on-disk path uses the literalnonesegment (./tiles/uav/none/{z}/{x}/{y}.jpg). The placeholder UUID is purely a key-space marker — it never lands in theflight_idcolumn (which staysNULL); the UPSERT usesCOALESCE(flight_id, '00000000-...')for the conflict check.
UavTileBatchMetadataPayload (added AZ-488)
JSON envelope deserialized from the metadata form field of a UAV batch upload.
Items(IReadOnlyList<UavTileMetadata>)
UavTileBatchUploadResponse (added AZ-488)
Wire response for POST /api/satellite/upload. Returned with HTTP 200 regardless of per-item outcomes; envelope-level failures (auth, oversize, deserialization) bypass this shape.
Items(List<UavTileUploadResultItem>)
UavTileUploadResultItem (added AZ-488)
Per-item result inside UavTileBatchUploadResponse.
Index(int): zero-based index into the request batch.Status(string): one ofUavTileUploadStatus.Accepted/UavTileUploadStatus.Rejected.TileId(Guid?): set on accept (matches the new/updatedtiles.id); null on reject.RejectReason(string?): closed-enum reason code fromUavTileRejectReasons; null on accept.RejectDetails(string?): short human-readable note. MUST NOT leak server-internal paths / exception types / hostnames (AZ-488 Security NFR; covered by SEC-11).
UavTileUploadStatus (added AZ-488, static string constants)
Accepted = "accepted"Rejected = "rejected"
UavTileRejectReasons (added AZ-488, static string constants — closed enumeration v1.0.0)
Authoritative reject-reason codes for the UAV upload quality gate. Adding a new code requires a minor-version bump of _docs/02_document/contracts/api/uav-tile-upload.md.
InvalidFormat = "INVALID_FORMAT"— Rule 1 (content-type or JPEG magic bytes).SizeOutOfBand = "SIZE_OUT_OF_BAND"— Rule 2 (bytes outside[MinBytes, MaxBytes]).WrongDimensions = "WRONG_DIMENSIONS"— Rule 3 (image width/height ≠MapConfig.TileSizePixels).CapturedAtFuture = "CAPTURED_AT_FUTURE"— Rule 4 (timestamp ahead of now +CapturedAtFutureSkewSeconds).CapturedAtTooOld = "CAPTURED_AT_TOO_OLD"— Rule 4 (timestamp older thanMaxAgeDays).ImageTooUniform = "IMAGE_TOO_UNIFORM"— Rule 5 (luminance variance belowMinLuminanceVariance).StorageFailure = "STORAGE_FAILURE"— reserved for the orphan-row-recovery path when the on-disk write succeeds but the DB UPSERT fails; surfaced per-item without failing the envelope (AZ-488 Reliability NFR).
TileCoord (added AZ-505, renamed AZ-794 cycle 7)
Single tile coordinate triple used by the inventory endpoint Form A request shape and as the per-entry input echo on the response.
Z(int)[JsonRequired]— slippy zoom level. Wire name"z".X(int)[JsonRequired]— slippy x at that zoom. Wire name"x".Y(int)[JsonRequired]— slippy y at that zoom. Wire name"y".- Defined in
SatelliteProvider.Common/DTO/TileInventory.cs. Matchestile-inventory.mdv2.0.0 Shape (the rename fromtileZoom/tileX/tileYshipped in AZ-794; the[JsonRequired]markers + the globalUnmappedMemberHandling.Disallowmean missing axes and the legacy field names both surface as HTTP 400 withValidationProblemDetailspererror-shape.mdv1.0.0).
TileInventoryRequest (added AZ-505)
API request body for POST /api/satellite/tiles/inventory. Carries one of two XOR-exclusive batch shapes.
Tiles(IReadOnlyList<TileCoord>?) — Form A: coords-by-value. The server computeslocation_hash = Uuidv5(TileNamespace, "{z}/{x}/{y}")per entry.LocationHashes(IReadOnlyList<Guid>?) — Form B: hashes-by-reference. Used when the caller already has UUIDv5 location hashes (typical for the onboard cross-repo path).- Exactly one of
Tiles/LocationHashesmust be populated and non-empty; both-populated or neither → HTTP 400 (tile-inventory.mdv2.0.0 Inv-1, enforced byInventoryRequestValidatorviaValidationEndpointFilter<TileInventoryRequest>in cycle 7). - Total entries (in either field) ≤
TileInventoryLimits.MaxEntriesPerRequest(5000); over-cap → HTTP 400 (Inv-7).
TileInventoryEntry (added AZ-505, coord fields renamed AZ-794 cycle 7)
Per-entry result inside TileInventoryResponse. One entry per request entry, in the SAME order as the request (tile-inventory.md Inv-2).
Z,X,Y(int) — echoed coord triple matching the request entry; wire names"z","x","y"(renamed from"tileZoom"/"tileX"/"tileY"by AZ-794). Always populated; when Form B was used, these are 0 (the caller already knows the hash).LocationHash(Guid) — always populated; UUIDv5 of"{z}/{x}/{y}"fromUuidv5.LocationHashForTile(Form A) or echoed from request (Form B).Present(bool) —trueiff a row exists intileswith thislocation_hash(Inv-4).Id(Guid?) —tiles.idof the most-recent row across sources/flights (captured_at DESC, updated_at DESC, id DESC, Inv-5); null whenPresent=false(Inv-6).CapturedAt(DateTime?),Source(string?),FlightId(Guid?),ResolutionMPerPx(double?) — populated on the most-recent row; all null whenPresent=false.
TileInventoryResponse (added AZ-505)
API response body for POST /api/satellite/tiles/inventory.
Results(IReadOnlyList<TileInventoryEntry>) — one entry per request entry;Results.Countalways equals the request entry count (Inv-2).
TileInventoryLimits (added AZ-505, static constants)
MaxEntriesPerRequest = 5000— request-body cap enforced byInventoryRequestValidator(per-array cap;tile-inventory.mdv2.0.0 Inv-7).
Internal Logic
GeoPointuses a precision tolerance of0.00005degrees (~5.5 meters) for equality comparison.SatTileeagerly computes its bounding box corners on construction by callingGeoUtils.TileToWorldPos.
Dependencies
GeoPoint,Direction— no importsSatTile→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
RegionRequestis the message type forIRegionRequestQueue
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.