feat(story-2.3): implement home marker and airspace outline rendering

Adds draw_home_marker() in overlay.py (red cross at projected home
position) and draw_airspace() in airspace.py (GeoJSON Polygon
boundaries in blue, graceful FileNotFoundError handling). Includes
airspace fixture and three tests covering all acceptance criteria.
All 70 tests pass; ruff check and format clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Edholm
2026-04-22 23:16:03 -04:00
parent 5a18d0867a
commit e8ce0602a4
6 changed files with 144 additions and 29 deletions
@@ -1,6 +1,6 @@
# Story 2.3: Home Marker & Airspace Outlines
Status: ready-for-dev
Status: review
## Story
@@ -18,35 +18,35 @@ AC3: **Given** `airspace.geojson` does not exist at `AIRSPACE_PATH` **When** air
## Tasks / Subtasks
- [ ] Task 1: Implement `draw_home_marker(image, bounds)` in `src/planemapper/renderer/overlay.py` (AC: #1)
- [ ] 1.1 New file; imports: `from PIL import Image, ImageDraw`; `from planemapper.constants import COLOUR_HOME_MARKER`; `from planemapper.renderer.projection import MapBounds, project`
- [ ] 1.2 Signature: `def draw_home_marker(image: Image.Image, bounds: MapBounds) -> None:`
- [ ] 1.3 Project `(bounds.home_lat, bounds.home_lon)` to `(cx, cy)`
- [ ] 1.4 Draw cross: horizontal line `(cx-10, cy)` to `(cx+10, cy)`, vertical line `(cx, cy-10)` to `(cx, cy+10)`, fill `COLOUR_HOME_MARKER`, width 3
- [ ] 1.5 Use `ImageDraw.Draw(image).line(...)`
- [x] Task 1: Implement `draw_home_marker(image, bounds)` in `src/planemapper/renderer/overlay.py` (AC: #1)
- [x] 1.1 New file; imports: `from PIL import Image, ImageDraw`; `from planemapper.constants import COLOUR_HOME_MARKER`; `from planemapper.renderer.projection import MapBounds, project`
- [x] 1.2 Signature: `def draw_home_marker(image: Image.Image, bounds: MapBounds) -> None:`
- [x] 1.3 Project `(bounds.home_lat, bounds.home_lon)` to `(cx, cy)`
- [x] 1.4 Draw cross: horizontal line `(cx-10, cy)` to `(cx+10, cy)`, vertical line `(cx, cy-10)` to `(cx, cy+10)`, fill `COLOUR_HOME_MARKER`, width 3
- [x] 1.5 Use `ImageDraw.Draw(image).line(...)`
- [ ] Task 2: Implement `draw_airspace(image, bounds)` in `src/planemapper/renderer/airspace.py` (AC: #2, #3)
- [ ] 2.1 Replace `# stub` with full implementation
- [ ] 2.2 Imports: `import json`, `import logging`; `from PIL import Image, ImageDraw`; `from planemapper.constants import AIRSPACE_PATH, COLOUR_AIRSPACE`; `from planemapper.renderer.projection import MapBounds, project`
- [ ] 2.3 `log = logging.getLogger(__name__)`
- [ ] 2.4 Try to open `AIRSPACE_PATH`; on `FileNotFoundError`: `log.warning("airspace.geojson not found")` and return
- [ ] 2.5 Parse JSON, iterate `data["features"]`
- [ ] 2.6 For each feature with `geometry["type"] == "Polygon"`: get `coords = feature["geometry"]["coordinates"][0]`
- [ ] 2.7 Reverse GeoJSON `[lon, lat]``(lat, lon)` at parse boundary: `points = [project(lat, lon, bounds) for lon, lat in coords]`
- [ ] 2.8 Draw with `ImageDraw.Draw(image).line(points, fill=COLOUR_AIRSPACE, width=2)`; skip features with fewer than 2 points
- [x] Task 2: Implement `draw_airspace(image, bounds)` in `src/planemapper/renderer/airspace.py` (AC: #2, #3)
- [x] 2.1 Replace `# stub` with full implementation
- [x] 2.2 Imports: `import json`, `import logging`; `from PIL import Image, ImageDraw`; `from planemapper.constants import AIRSPACE_PATH, COLOUR_AIRSPACE`; `from planemapper.renderer.projection import MapBounds, project`
- [x] 2.3 `log = logging.getLogger(__name__)`
- [x] 2.4 Try to open `AIRSPACE_PATH`; on `FileNotFoundError`: `log.warning("airspace.geojson not found")` and return
- [x] 2.5 Parse JSON, iterate `data["features"]`
- [x] 2.6 For each feature with `geometry["type"] == "Polygon"`: get `coords = feature["geometry"]["coordinates"][0]`
- [x] 2.7 Reverse GeoJSON `[lon, lat]``(lat, lon)` at parse boundary: `points = [project(lat, lon, bounds) for lon, lat in coords]`
- [x] 2.8 Draw with `ImageDraw.Draw(image).line(points, fill=COLOUR_AIRSPACE, width=2)`; skip features with fewer than 2 points
- [ ] Task 3: Update `tests/fixtures/airspace_sample.geojson` with a sample polygon (AC: #2)
- [ ] 3.1 Replace empty features list with one `Polygon` feature having 5 `[lon, lat]` coordinate pairs forming a closed ring near lat=53, lon=-6
- [x] Task 3: Update `tests/fixtures/airspace_sample.geojson` with a sample polygon (AC: #2)
- [x] 3.1 Replace empty features list with one `Polygon` feature having 5 `[lon, lat]` coordinate pairs forming a closed ring near lat=53, lon=-6
- [ ] Task 4: Write tests in `tests/test_airspace.py` (AC: #1, #2, #3)
- [ ] 4.1 Test AC1 (home marker): Create 800×480 white RGB image, create `MapBounds(53.0, -6.0, 100.0)`, call `draw_home_marker(image, bounds)`, assert pixel at (400, 240) is red
- [ ] 4.2 Test AC2 (airspace drawn): Monkeypatch `AIRSPACE_PATH` to `tests/fixtures/airspace_sample.geojson` path, create image, call `draw_airspace(image, bounds)`, assert no exception raised and function returns normally (drawing occurred without crash)
- [ ] 4.3 Test AC3 (missing geojson): Monkeypatch `AIRSPACE_PATH` to a nonexistent path, call `draw_airspace(image, bounds)`, assert no exception raised
- [x] Task 4: Write tests in `tests/test_airspace.py` (AC: #1, #2, #3)
- [x] 4.1 Test AC1 (home marker): Create 800×480 white RGB image, create `MapBounds(53.0, -6.0, 100.0)`, call `draw_home_marker(image, bounds)`, assert pixel at (400, 240) is red
- [x] 4.2 Test AC2 (airspace drawn): Monkeypatch `AIRSPACE_PATH` to `tests/fixtures/airspace_sample.geojson` path, create image, call `draw_airspace(image, bounds)`, assert no exception raised and function returns normally (drawing occurred without crash)
- [x] 4.3 Test AC3 (missing geojson): Monkeypatch `AIRSPACE_PATH` to a nonexistent path, call `draw_airspace(image, bounds)`, assert no exception raised
- [ ] Task 5: Run quality gates
- [ ] 5.1 `python -m pytest tests/` — all tests pass
- [ ] 5.2 `python -m ruff check .` — zero violations
- [ ] 5.3 `python -m ruff format --check .` — no formatting issues
- [x] Task 5: Run quality gates
- [x] 5.1 `python -m pytest tests/` — all tests pass
- [x] 5.2 `python -m ruff check .` — zero violations
- [x] 5.3 `python -m ruff format --check .` — no formatting issues
## Dev Notes
@@ -55,7 +55,7 @@ development_status:
epic-2: in-progress
2-1-aircraft-data-model-and-fetcher: done
2-2-coordinate-projection-and-base-map-loading: done
2-3-home-marker-and-airspace-outlines: ready-for-dev
2-3-home-marker-and-airspace-outlines: review
2-4-altitude-colour-bands-and-aircraft-type-icons: backlog
2-5-per-aircraft-drawing: backlog
2-6-stateful-renderer-and-display-interface: backlog