Implement story 2.2: coordinate projection and base map loading

Add MapBounds dataclass and equirectangular project() function in
projection.py, basemap.load() forcing pixels into memory via .copy(),
and full test coverage for both modules (4 new tests). All quality
gates pass: 67 tests, ruff clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Edholm
2026-04-22 23:10:38 -04:00
parent f8e763d734
commit 037ce3e193
6 changed files with 107 additions and 37 deletions
+9 -1
View File
@@ -1 +1,9 @@
# stub
from __future__ import annotations
from PIL import Image
from planemapper.constants import BACKGROUND_PATH
def load() -> Image.Image:
return Image.open(BACKGROUND_PATH).copy()
+25 -1
View File
@@ -1 +1,25 @@
# stub
from __future__ import annotations
import math
from dataclasses import dataclass, field
from planemapper.constants import DISPLAY_HEIGHT, DISPLAY_WIDTH
@dataclass
class MapBounds:
home_lat: float
home_lon: float
radius_nm: float
width: int = field(default=DISPLAY_WIDTH)
height: int = field(default=DISPLAY_HEIGHT)
def project(lat: float, lon: float, bounds: MapBounds) -> tuple[int, int]:
deg_per_nm_lat = 1 / 60
deg_per_nm_lon = 1 / (60 * math.cos(math.radians(bounds.home_lat)))
px_per_nm_x = (bounds.width / 2) / bounds.radius_nm
px_per_nm_y = (bounds.height / 2) / bounds.radius_nm
x = bounds.width // 2 + int((lon - bounds.home_lon) / deg_per_nm_lon * px_per_nm_x)
y = bounds.height // 2 - int((lat - bounds.home_lat) / deg_per_nm_lat * px_per_nm_y)
return (x, y)