# 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 (0–360) ### 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) - `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/Lon` and added `[JsonPropertyName("lat")]` / `[JsonPropertyName("lon")]` to make the wire format unambiguous. With `JsonSerializerOptions.UnmappedMemberHandling.Disallow` active (AZ-795), the old `latitude`/`longitude` wire 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\), `Geofences` (Geofences?) - `RequestMaps` (bool), `CreateTilesZip` (bool) ### RouteResponse API response for route queries. - All fields from the route entity plus `Points` (List\) - `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\) ### 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-item `tiles.id` becomes `Uuidv5(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. When `null`, the per-item id uses the zero-UUID `00000000-0000-0000-0000-000000000000` placeholder and the on-disk path uses the literal `none` segment (`./tiles/uav/none/{z}/{x}/{y}.jpg`). The placeholder UUID is purely a key-space marker — it never lands in the `flight_id` column (which stays `NULL`); the UPSERT uses `COALESCE(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\) ### 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 (added AZ-488) Per-item result inside `UavTileBatchUploadResponse`. - `Index` (int): zero-based index into the request batch. - `Status` (string): one of `UavTileUploadStatus.Accepted` / `UavTileUploadStatus.Rejected`. - `TileId` (Guid?): set on accept (matches the new/updated `tiles.id`); null on reject. - `RejectReason` (string?): closed-enum reason code from `UavTileRejectReasons`; 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 than `MaxAgeDays`). - `ImageTooUniform = "IMAGE_TOO_UNIFORM"` — Rule 5 (luminance variance below `MinLuminanceVariance`). - `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`. Matches `tile-inventory.md` v2.0.0 Shape (the rename from `tileZoom/tileX/tileY` shipped in AZ-794; the `[JsonRequired]` markers + the global `UnmappedMemberHandling.Disallow` mean missing axes and the legacy field names both surface as HTTP 400 with `ValidationProblemDetails` per `error-shape.md` v1.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?`) — Form A: coords-by-value. The server computes `location_hash = Uuidv5(TileNamespace, "{z}/{x}/{y}")` per entry. - `LocationHashes` (`IReadOnlyList?`) — 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` / `LocationHashes` must be populated and non-empty; both-populated or neither → HTTP 400 (`tile-inventory.md` v2.0.0 Inv-1, enforced by `InventoryRequestValidator` via `ValidationEndpointFilter` 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}"` from `Uuidv5.LocationHashForTile` (Form A) or echoed from request (Form B). - `Present` (bool) — `true` iff a row exists in `tiles` with this `location_hash` (Inv-4). - `Id` (Guid?) — `tiles.id` of the most-recent row across sources/flights (`captured_at DESC, updated_at DESC, id DESC`, Inv-5); null when `Present=false` (Inv-6). - `CapturedAt` (DateTime?), `Source` (string?), `FlightId` (Guid?), `ResolutionMPerPx` (double?) — populated on the most-recent row; all null when `Present=false`. ### TileInventoryResponse (added AZ-505) API response body for `POST /api/satellite/tiles/inventory`. - `Results` (`IReadOnlyList`) — one entry per request entry; `Results.Count` always equals the request entry count (Inv-2). ### TileInventoryLimits (added AZ-505, static constants) - `MaxEntriesPerRequest = 5000` — request-body cap enforced by `InventoryRequestValidator` (per-array cap; `tile-inventory.md` v2.0.0 Inv-7). ## 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.