# Story 2.3: Home Marker & Airspace Outlines

Status: done

## Story

As a user glancing at the display,
I want to see my home location marked on the map and published airspace boundaries shown as outlines,
So that I have immediate spatial context for all aircraft positions.

## Acceptance Criteria

AC1: **Given** a loaded base map image and home lat/lon **When** the home marker is drawn **Then** a distinct `COLOUR_HOME_MARKER` (red) cross/circle marker is drawn at the projected pixel position of the home location

AC2: **Given** a valid `airspace.geojson` at `AIRSPACE_PATH` **When** airspace outlines are drawn **Then** each feature's boundary is drawn as an outline in `COLOUR_AIRSPACE` (blue) on the image **And** GeoJSON `[lon, lat]` coordinates are reversed to `(lat, lon)` at the parse boundary before any projection

AC3: **Given** `airspace.geojson` does not exist at `AIRSPACE_PATH` **When** airspace draw is called **Then** no exception is raised — the map renders without airspace outlines and a WARNING is logged

## Tasks / Subtasks

- [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(...)`

- [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

- [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

- [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

- [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

### Critical Context

**Module locations:**
- `src/planemapper/renderer/overlay.py` — NEW file; implement `draw_home_marker()`
- `src/planemapper/renderer/airspace.py` — currently `# stub`; replace with full `draw_airspace()` implementation

**Constants already defined in `src/planemapper/constants.py`:**
```python
COLOUR_HOME_MARKER = COLOUR_RED   # (255, 0, 0)
COLOUR_AIRSPACE = COLOUR_BLUE     # (0, 0, 255)
AIRSPACE_PATH = Path("/etc/planemapper/airspace.geojson")
```

**Projection:** `project(lat, lon, bounds)` from `planemapper.renderer.projection` — lat first, lon second. `MapBounds` is the bounds object.

**GeoJSON coordinate convention:** GeoJSON uses `[lon, lat]` — MUST reverse at parse boundary. This is critical and easy to get wrong:
```python
# CORRECT — unpack GeoJSON [lon, lat] order, then pass as (lat, lon) to project()
points = [project(lat, lon, bounds) for lon, lat in coords]
```

**Airspace GeoJSON structure from OpenAIP:**
FeatureCollection with features having `geometry.type = "Polygon"`. `coordinates[0]` is the exterior ring as a list of `[lon, lat]` pairs. For MVP, only handle `Polygon` features — skip all others silently.

**Home marker drawing detail:**
The cross is drawn as two separate `line()` calls (or one call with both segments). The centre pixel at `(400, 240)` must be red after drawing when home is at the projected centre — tests assert this directly.

**Existing airspace fixture** at `tests/fixtures/airspace_sample.geojson` is currently `{"type": "FeatureCollection", "features": []}` — must be updated with a real polygon so AC2 test exercises the drawing path.

**Sample polygon for fixture:** Use 5 points forming a closed square ring near lat=53, lon=-6 (e.g. corners at ±0.1 degrees). Coordinates must be in GeoJSON `[lon, lat]` order with the first and last point identical to close the ring.

**Test file:** `tests/test_airspace.py` is a new file — create from scratch.

**Monkeypatching `AIRSPACE_PATH`:** The path constant is imported into `planemapper.renderer.airspace`, so patch it there:
```python
monkeypatch.setattr("planemapper.renderer.airspace.AIRSPACE_PATH", Path("tests/fixtures/airspace_sample.geojson"))
```
