mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-22 21:21:15 +00:00
Compare commits
2 Commits
45f7852fb2
...
1ca8c80d7b
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ca8c80d7b | |||
| 7c37636fdf |
@@ -179,7 +179,6 @@ async Task<IResult> GetTileByLatLon([FromQuery] double Latitude, [FromQuery] dou
|
|||||||
TileSizeMeters = tile.TileSizeMeters,
|
TileSizeMeters = tile.TileSizeMeters,
|
||||||
TileSizePixels = tile.TileSizePixels,
|
TileSizePixels = tile.TileSizePixels,
|
||||||
ImageType = tile.ImageType,
|
ImageType = tile.ImageType,
|
||||||
MapsVersion = tile.MapsVersion,
|
|
||||||
Version = tile.Version,
|
Version = tile.Version,
|
||||||
FilePath = tile.FilePath,
|
FilePath = tile.FilePath,
|
||||||
CreatedAt = tile.CreatedAt,
|
CreatedAt = tile.CreatedAt,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ public record DownloadTileResponse
|
|||||||
public double TileSizeMeters { get; set; }
|
public double TileSizeMeters { get; set; }
|
||||||
public int TileSizePixels { get; set; }
|
public int TileSizePixels { get; set; }
|
||||||
public string ImageType { get; set; } = string.Empty;
|
public string ImageType { get; set; } = string.Empty;
|
||||||
public string? MapsVersion { get; set; }
|
|
||||||
public int? Version { get; set; }
|
public int? Version { get; set; }
|
||||||
public string FilePath { get; set; } = string.Empty;
|
public string FilePath { get; set; } = string.Empty;
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ public class TileMetadata
|
|||||||
public double TileSizeMeters { get; set; }
|
public double TileSizeMeters { get; set; }
|
||||||
public int TileSizePixels { get; set; }
|
public int TileSizePixels { get; set; }
|
||||||
public string ImageType { get; set; } = string.Empty;
|
public string ImageType { get; set; } = string.Empty;
|
||||||
public string? MapsVersion { get; set; }
|
|
||||||
public int? Version { get; set; }
|
public int? Version { get; set; }
|
||||||
public string FilePath { get; set; } = string.Empty;
|
public string FilePath { get; set; } = string.Empty;
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ public record DownloadTileResponse
|
|||||||
public double TileSizeMeters { get; set; }
|
public double TileSizeMeters { get; set; }
|
||||||
public int TileSizePixels { get; set; }
|
public int TileSizePixels { get; set; }
|
||||||
public string ImageType { get; set; } = string.Empty;
|
public string ImageType { get; set; } = string.Empty;
|
||||||
public string? MapsVersion { get; set; }
|
|
||||||
public string FilePath { get; set; } = string.Empty;
|
public string FilePath { get; set; } = string.Empty;
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public DateTime UpdatedAt { get; set; }
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ public static class TileTests
|
|||||||
Console.WriteLine($" Tile Size (meters): {tile.TileSizeMeters:F2}");
|
Console.WriteLine($" Tile Size (meters): {tile.TileSizeMeters:F2}");
|
||||||
Console.WriteLine($" Tile Size (pixels): {tile.TileSizePixels}");
|
Console.WriteLine($" Tile Size (pixels): {tile.TileSizePixels}");
|
||||||
Console.WriteLine($" Image Type: {tile.ImageType}");
|
Console.WriteLine($" Image Type: {tile.ImageType}");
|
||||||
Console.WriteLine($" Maps Version: {tile.MapsVersion}");
|
|
||||||
Console.WriteLine($" File Path: {tile.FilePath}");
|
Console.WriteLine($" File Path: {tile.FilePath}");
|
||||||
Console.WriteLine($" Created At: {tile.CreatedAt:yyyy-MM-dd HH:mm:ss}");
|
Console.WriteLine($" Created At: {tile.CreatedAt:yyyy-MM-dd HH:mm:ss}");
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ public class TileService : ITileService
|
|||||||
TileSizeMeters = downloaded.TileSizeMeters,
|
TileSizeMeters = downloaded.TileSizeMeters,
|
||||||
TileSizePixels = _mapConfig.TileSizePixels,
|
TileSizePixels = _mapConfig.TileSizePixels,
|
||||||
ImageType = "jpg",
|
ImageType = "jpg",
|
||||||
MapsVersion = $"downloaded_{now:yyyy-MM-dd}",
|
MapsVersion = null,
|
||||||
Version = null,
|
Version = null,
|
||||||
FilePath = downloaded.FilePath,
|
FilePath = downloaded.FilePath,
|
||||||
CreatedAt = now,
|
CreatedAt = now,
|
||||||
@@ -175,7 +175,6 @@ public class TileService : ITileService
|
|||||||
TileSizeMeters = entity.TileSizeMeters,
|
TileSizeMeters = entity.TileSizeMeters,
|
||||||
TileSizePixels = entity.TileSizePixels,
|
TileSizePixels = entity.TileSizePixels,
|
||||||
ImageType = entity.ImageType,
|
ImageType = entity.ImageType,
|
||||||
MapsVersion = entity.MapsVersion,
|
|
||||||
Version = entity.Version,
|
Version = entity.Version,
|
||||||
FilePath = entity.FilePath,
|
FilePath = entity.FilePath,
|
||||||
CreatedAt = entity.CreatedAt,
|
CreatedAt = entity.CreatedAt,
|
||||||
|
|||||||
@@ -207,6 +207,54 @@ public class TileServiceTests
|
|||||||
captured!.Version.Should().BeNull("AZ-357: new code never writes the deprecated year-based version");
|
captured!.Version.Should().BeNull("AZ-357: new code never writes the deprecated year-based version");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildTileEntity_DoesNotPopulateMapsVersion_AZ373_AC1()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var downloader = new Mock<ISatelliteDownloader>();
|
||||||
|
var tileRepo = new Mock<ITileRepository>();
|
||||||
|
TileEntity? captured = null;
|
||||||
|
tileRepo
|
||||||
|
.Setup(r => r.InsertAsync(It.IsAny<TileEntity>()))
|
||||||
|
.Callback<TileEntity>(e => captured = e)
|
||||||
|
.ReturnsAsync(Guid.NewGuid());
|
||||||
|
downloader
|
||||||
|
.Setup(d => d.DownloadSingleTileAsync(It.IsAny<double>(), It.IsAny<double>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new DownloadedTileInfoV2(1, 2, 18, 47.46, 37.65, "tiles/18/1/2.jpg", 100.0));
|
||||||
|
|
||||||
|
var service = BuildService(downloader, tileRepo);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_ = service.DownloadAndStoreSingleTileAsync(47.46, 37.65, 18).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
captured.Should().NotBeNull();
|
||||||
|
captured!.MapsVersion.Should().BeNull(
|
||||||
|
"AZ-373 option (a): new code never writes a MapsVersion value; the column is retained for forensics on pre-existing rows only");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DownloadTileResponse_DoesNotExposeMapsVersion_AZ373_AC2()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var property = typeof(DownloadTileResponse).GetProperty("MapsVersion");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
property.Should().BeNull(
|
||||||
|
"AZ-373 AC-2 option (a): MapsVersion is removed from the HTTP response shape");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TileMetadata_DoesNotExposeMapsVersion_AZ373_AC1()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var property = typeof(TileMetadata).GetProperty("MapsVersion");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
property.Should().BeNull(
|
||||||
|
"AZ-373 AC-1: TileMetadata DTO no longer carries MapsVersion (consistent with the HTTP response removal)");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetTileAsync_KnownId_ReturnsMappedMetadata()
|
public async Task GetTileAsync_KnownId_ReturnsMappedMetadata()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ Stores metadata for downloaded satellite imagery tiles. Each tile is a single im
|
|||||||
| tile_size_meters | DOUBLE PRECISION | NOT NULL | Ground coverage in meters |
|
| tile_size_meters | DOUBLE PRECISION | NOT NULL | Ground coverage in meters |
|
||||||
| tile_size_pixels | INT | NOT NULL | Image dimension in pixels |
|
| tile_size_pixels | INT | NOT NULL | Image dimension in pixels |
|
||||||
| image_type | VARCHAR(10) | NOT NULL | Image format (e.g., "jpg") |
|
| image_type | VARCHAR(10) | NOT NULL | Image format (e.g., "jpg") |
|
||||||
| maps_version | VARCHAR(50) | | Google Maps version string |
|
| maps_version | VARCHAR(50) | | Legacy free-form provider tag; post-AZ-373 new rows write NULL (column retained for forensics on pre-existing rows) |
|
||||||
| version | INT | NOT NULL, DEFAULT 2025 | Year-based versioning for cache invalidation |
|
| version | INT | NOT NULL, DEFAULT 2025 | Year-based versioning for cache invalidation |
|
||||||
| file_path | VARCHAR(500) | NOT NULL | Relative path to stored image |
|
| file_path | VARCHAR(500) | NOT NULL | Relative path to stored image |
|
||||||
| tile_x | INT | NOT NULL | Tile X coordinate (Slippy Map) |
|
| tile_x | INT | NOT NULL | Tile X coordinate (Slippy Map) |
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Represents a single map tile with its spatial bounds.
|
|||||||
Metadata about a stored tile (mirrors `TileEntity` but without DB-specific concerns).
|
Metadata about a stored tile (mirrors `TileEntity` but without DB-specific concerns).
|
||||||
- `Id` (Guid), `TileZoom`, `TileX`, `TileY` (int), `Latitude`, `Longitude` (double)
|
- `Id` (Guid), `TileZoom`, `TileX`, `TileY` (int), `Latitude`, `Longitude` (double)
|
||||||
- `TileSizeMeters` (double), `TileSizePixels` (int), `ImageType` (string)
|
- `TileSizeMeters` (double), `TileSizePixels` (int), `ImageType` (string)
|
||||||
- `MapsVersion` (string?), `Version` (int), `FilePath` (string)
|
- `Version` (int?), `FilePath` (string)
|
||||||
- `CreatedAt`, `UpdatedAt` (DateTime)
|
- `CreatedAt`, `UpdatedAt` (DateTime)
|
||||||
|
|
||||||
### RegionRequest
|
### RegionRequest
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Orchestrates tile downloading and persistence. Bridges the downloader (Google Ma
|
|||||||
|
|
||||||
### TileService (implements ITileService)
|
### TileService (implements ITileService)
|
||||||
- `DownloadAndStoreTilesAsync(double lat, double lon, double sizeMeters, int zoomLevel, CancellationToken) → Task<List<TileMetadata>>`:
|
- `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)
|
1. Queries existing tiles in the region from the repository (latest per `(latitude, longitude, zoom_level, tile_size_meters)` post-AZ-357)
|
||||||
2. Calls `ISatelliteDownloader.GetTilesWithMetadataAsync` with existing tiles to skip
|
2. Calls `ISatelliteDownloader.GetTilesWithMetadataAsync` with existing tiles to skip
|
||||||
3. Creates `TileEntity` for each newly downloaded tile and inserts via repository (upsert)
|
3. Creates `TileEntity` for each newly downloaded tile and inserts via repository (upsert)
|
||||||
4. Returns combined list of existing + new tile metadata
|
4. Returns combined list of existing + new tile metadata
|
||||||
@@ -19,10 +19,9 @@ Orchestrates tile downloading and persistence. Bridges the downloader (Google Ma
|
|||||||
- `DownloadAndStoreSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken) → Task<TileMetadata>` (AZ-311): download one tile by lat/lon, persist, return metadata
|
- `DownloadAndStoreSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken) → Task<TileMetadata>` (AZ-311): download one tile by lat/lon, persist, return metadata
|
||||||
|
|
||||||
## Internal Logic
|
## Internal Logic
|
||||||
- Version is `DateTime.UtcNow.Year` — tiles are considered fresh for the current calendar year
|
- New rows write `Version = null` and `MapsVersion = null` (post-AZ-357 / AZ-373); the `version` and `maps_version` columns are retained for backward compatibility with pre-existing rows
|
||||||
- `MapToMetadata(TileEntity) → TileMetadata`: entity-to-DTO mapping (static helper)
|
- `MapToMetadata(TileEntity) → TileMetadata`: entity-to-DTO mapping (static helper); `MapsVersion` is no longer projected onto `TileMetadata` / `DownloadTileResponse`
|
||||||
- Tile size hardcoded to 256 pixels, image type "jpg"
|
- `TileSizePixels` sourced from `MapConfig.TileSizePixels` (default 256, post-AZ-371); image type fixed at `"jpg"`
|
||||||
- `MapsVersion` formatted as `"downloaded_{date}"`
|
|
||||||
- `IMemoryCache` keyed by `(z, x, y)` with 1h absolute / 30min sliding expiration; populated on first hit and on downloader fallback
|
- `IMemoryCache` keyed by `(z, x, y)` with 1h absolute / 30min sliding expiration; populated on first hit and on downloader fallback
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Batch Report
|
||||||
|
|
||||||
|
**Batch**: 20
|
||||||
|
**Tasks**: AZ-373 (C20 — clarify or drop MapsVersion field; option (a) selected)
|
||||||
|
**Date**: 2026-05-11
|
||||||
|
**Run**: `03-code-quality-refactoring`
|
||||||
|
**Cycle**: 1
|
||||||
|
|
||||||
|
## Task Results
|
||||||
|
|
||||||
|
| Task | Status | Files Modified | Tests | AC Coverage | Issues |
|
||||||
|
|------|--------|----------------|-------|-------------|--------|
|
||||||
|
| AZ-373_refactor_clarify_mapsversion | Done | 7 src/test + 3 doc + 1 task = 11 files | 174 unit + 5 smoke pass | 3/3 covered | None |
|
||||||
|
|
||||||
|
## Decision Recorded
|
||||||
|
|
||||||
|
Option (a) — **drop `MapsVersion` from new writes**. `tiles.maps_version` column retained (`coderule.mdc` forbids unprompted column drops). Pre-existing rows keep their `"downloaded_YYYY-MM-DD"` labels for forensic lookup; new rows write `NULL`. The field is removed from the API contract (`DownloadTileResponse`) and the internal DTO (`TileMetadata`) so it no longer appears in JSON responses.
|
||||||
|
|
||||||
|
Rationale for picking (a) over (b): the recommended default in the task spec; `MapsVersion` carries no operational signal — the value was a creation-date label, which is already redundant with `CreatedAt`. Removing it eliminates the dual-source confusion called out by C20.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### Modified production code
|
||||||
|
|
||||||
|
- `SatelliteProvider.Services.TileDownloader/TileService.cs`
|
||||||
|
- `BuildTileEntity`: replaced `MapsVersion = $"downloaded_{now:yyyy-MM-dd}"` with `MapsVersion = null`.
|
||||||
|
- `MapToMetadata`: removed `MapsVersion = entity.MapsVersion` projection (the DTO no longer carries that property).
|
||||||
|
- `SatelliteProvider.Common/DTO/DownloadTileResponse.cs` — `MapsVersion` property removed.
|
||||||
|
- `SatelliteProvider.Common/DTO/TileMetadata.cs` — `MapsVersion` property removed.
|
||||||
|
- `SatelliteProvider.Api/Program.cs` — removed the `MapsVersion = tile.MapsVersion` line in the `DownloadTileResponse` constructor inside `GetTileByLatLon`.
|
||||||
|
|
||||||
|
### Unchanged (deliberate)
|
||||||
|
|
||||||
|
- `SatelliteProvider.DataAccess/Models/TileEntity.cs` — `MapsVersion` property retained because the column still exists; Dapper maps both directions.
|
||||||
|
- `SatelliteProvider.DataAccess/Repositories/TileRepository.cs` — `maps_version as MapsVersion` in all four SELECTs and `@MapsVersion` in INSERT/UPDATE kept; binds to `null` for new rows.
|
||||||
|
- `SatelliteProvider.DataAccess/Migrations/001_CreateTilesTable.sql` — `maps_version VARCHAR(50)` (nullable) untouched.
|
||||||
|
|
||||||
|
### Modified tests
|
||||||
|
|
||||||
|
- `SatelliteProvider.Tests/TileServiceTests.cs` — 3 new tests:
|
||||||
|
- `BuildTileEntity_DoesNotPopulateMapsVersion_AZ373_AC1` — captures the persisted `TileEntity` and asserts `MapsVersion == null`.
|
||||||
|
- `DownloadTileResponse_DoesNotExposeMapsVersion_AZ373_AC2` — reflection check that the property is absent.
|
||||||
|
- `TileMetadata_DoesNotExposeMapsVersion_AZ373_AC1` — reflection check.
|
||||||
|
- `SatelliteProvider.IntegrationTests/Models.cs` — `MapsVersion` removed from the local wire-shape record.
|
||||||
|
- `SatelliteProvider.IntegrationTests/TileTests.cs` — removed the `Console.WriteLine($" Maps Version: …")` line.
|
||||||
|
|
||||||
|
### Modified docs
|
||||||
|
|
||||||
|
- `_docs/02_document/modules/common_dtos.md` — `TileMetadata` bullet now lists `Version (int?), FilePath (string)` (no `MapsVersion`).
|
||||||
|
- `_docs/02_document/modules/services_tile_service.md` — Internal Logic and `DownloadAndStoreTilesAsync` description refreshed for AZ-357 + AZ-371 + AZ-373 (adjacent hygiene within the same file).
|
||||||
|
- `_docs/02_document/data_model.md` — `maps_version` column comment now reads "Legacy free-form provider tag; post-AZ-373 new rows write NULL".
|
||||||
|
|
||||||
|
## Wire-shape impact
|
||||||
|
|
||||||
|
- JSON for `/api/satellite/tiles/latlon` no longer contains the `mapsVersion` property. Tolerant JSON consumers are unaffected. Strict consumers were already aware of the upcoming change via AZ-357's release notes; the integration test that previously printed the field has been updated.
|
||||||
|
- OpenAPI schema regenerates automatically from the DTO (Swashbuckle) — no manual spec edit required.
|
||||||
|
|
||||||
|
## AC Test Coverage
|
||||||
|
|
||||||
|
| AC | Covered by |
|
||||||
|
|----|------------|
|
||||||
|
| AC-1 (semantics explicit — option a: no new writes) | `BuildTileEntity_DoesNotPopulateMapsVersion_AZ373_AC1`, `TileMetadata_DoesNotExposeMapsVersion_AZ373_AC1` |
|
||||||
|
| AC-2 (HTTP shape decision recorded — field removed) | `DownloadTileResponse_DoesNotExposeMapsVersion_AZ373_AC2` |
|
||||||
|
| AC-3 (tests stay green) | `scripts/run-tests.sh --smoke` reports 174 unit + 5 smoke. |
|
||||||
|
|
||||||
|
## Test Run
|
||||||
|
|
||||||
|
| Suite | Result | Count |
|
||||||
|
|-------|--------|-------|
|
||||||
|
| Unit (`SatelliteProvider.Tests`) | All passed | 174 (was 171; +3 new tests for AC-1 / AC-2) |
|
||||||
|
| Smoke integration (Docker) | All passed | 5 scenarios |
|
||||||
|
|
||||||
|
## Code Review Verdict: PASS
|
||||||
|
|
||||||
|
Inline review covered:
|
||||||
|
|
||||||
|
- **Architecture**: No layer violations. DTO change confined to `Common/DTO`; persistence layer untouched aside from the implicit `null` write; `WebApi` updated as the direct consumer. Module-layout ownership respected.
|
||||||
|
- **Behavior preservation**: DB column kept and nullable; existing rows untouched; SQL binds `@MapsVersion` to `null` for new rows. Single observable JSON wire change: `mapsVersion` field is gone from `/api/satellite/tiles/latlon` responses (intentional per AC-2).
|
||||||
|
- **AC coverage**: 3/3, all wired to dedicated tests.
|
||||||
|
- **Code quality**: SRP preserved; no narrative comments; A/A/A test pattern; no suppressed errors; argument validation unaffected.
|
||||||
|
- **Cross-batch consistency**: extends the AZ-357 (drop `Version` concept) theme; AZ-370 (enums, batch 19), AZ-371 (config, batch 18), AZ-364/AZ-360 (RouteProcessingService decomposition, batch 17), and AZ-367 (TileGridStitcher, batch 16) are untouched and still healthy.
|
||||||
|
|
||||||
|
**Findings**: None.
|
||||||
|
|
||||||
|
## Auto-Fix Attempts: 0
|
||||||
|
## Stuck Agents: None
|
||||||
|
|
||||||
|
## Next Batch
|
||||||
|
|
||||||
|
Auto-chain to next batch per `/implement` Step 14. Cumulative K=3 review fires after batch 21 (covering batches 19–21). Phase 4 continues with the remaining 8 tasks in `todo/`:
|
||||||
|
|
||||||
|
- `AZ-374` — C21 typed HttpClient for Google Maps (2 SP)
|
||||||
|
- `AZ-375` — C22 O(N) existing-tile lookup (2 SP, needs AZ-371 ✓)
|
||||||
|
- `AZ-376` — C23 delete unused FindExistingTileAsync (1 SP)
|
||||||
|
- `AZ-377` — C24 consolidate Earth-geometry constants (2 SP, needs AZ-371 ✓)
|
||||||
|
- `AZ-378` — C25 repo `_logger` fields (1 SP)
|
||||||
|
- `AZ-379` — C26 repo SELECT column-list constants (2 SP)
|
||||||
|
- `AZ-380` — C27 delete `CalculatePolygonDiagonalDistance` (1 SP)
|
||||||
|
- `AZ-372` — C19 dotnet format + analyzers + Coverlet (3 SP)
|
||||||
@@ -8,7 +8,7 @@ status: in_progress
|
|||||||
sub_step:
|
sub_step:
|
||||||
phase: 4
|
phase: 4
|
||||||
name: execution
|
name: execution
|
||||||
detail: "batch 19 (AZ-370) complete; K=3 review fires after batch 21"
|
detail: "batch 20 (AZ-373) complete; K=3 cumulative review fires after batch 21"
|
||||||
retry_count: 0
|
retry_count: 0
|
||||||
cycle: 1
|
cycle: 1
|
||||||
tracker: jira
|
tracker: jira
|
||||||
|
|||||||
Reference in New Issue
Block a user