"""WGS84 geodesic helpers — Vincenty distance + bearing for accuracy assertions. Wraps `pyproj.Geod` (WGS84 ellipsoid) for the few operations the blackbox tests need. Kept deliberately small — broader geo math (UTM, MGRS, datum conversions) is NOT in scope for the e2e harness. All inputs are degrees lat / lon (WGS84); all distances are meters. """ from __future__ import annotations from dataclasses import dataclass from pyproj import Geod _WGS84 = Geod(ellps="WGS84") @dataclass(frozen=True) class GeodeticDelta: """Bearing + distance + back-bearing between two WGS84 points.""" distance_m: float forward_bearing_deg: float reverse_bearing_deg: float def distance_m(lat1: float, lon1: float, lat2: float, lon2: float) -> float: """Vincenty distance in meters between two WGS84 points. Raises ValueError on NaN inputs (defensive — silent NaN propagation in a test assertion is the kind of bug this helper exists to prevent). """ for name, value in (("lat1", lat1), ("lon1", lon1), ("lat2", lat2), ("lon2", lon2)): if value != value: # NaN check raise ValueError(f"distance_m: {name} is NaN") _, _, d = _WGS84.inv(lon1, lat1, lon2, lat2) return float(d) def delta(lat1: float, lon1: float, lat2: float, lon2: float) -> GeodeticDelta: """Full geodetic delta: distance + forward/reverse bearings.""" fwd_az, rev_az, d = _WGS84.inv(lon1, lat1, lon2, lat2) return GeodeticDelta( distance_m=float(d), forward_bearing_deg=float(fwd_az), reverse_bearing_deg=float(rev_az), ) def offset(lat: float, lon: float, bearing_deg: float, distance_m: float) -> tuple[float, float]: """Project ``(lat, lon)`` by ``distance_m`` along ``bearing_deg`` (degrees CW from north).""" new_lon, new_lat, _ = _WGS84.fwd(lon, lat, bearing_deg, distance_m) return float(new_lat), float(new_lon)