"""Small WGS84 geodesy helpers used by the RTK--IMU calibration path.""" from __future__ import annotations import numpy as np WGS84_A_M = 6378137.0 WGS84_F = 1.0 / 298.257223563 WGS84_E2 = WGS84_F * (2.0 - WGS84_F) def geodetic_to_ecef( latitude_deg: np.ndarray, longitude_deg: np.ndarray, altitude_m: np.ndarray, ) -> np.ndarray: """Convert WGS84 latitude/longitude/ellipsoidal height to ECEF metres.""" latitude = np.deg2rad(np.asarray(latitude_deg, dtype=float)) longitude = np.deg2rad(np.asarray(longitude_deg, dtype=float)) altitude = np.asarray(altitude_m, dtype=float) latitude, longitude, altitude = np.broadcast_arrays(latitude, longitude, altitude) sin_lat = np.sin(latitude) cos_lat = np.cos(latitude) radius = WGS84_A_M / np.sqrt(1.0 - WGS84_E2 * sin_lat**2) x = (radius + altitude) * cos_lat * np.cos(longitude) y = (radius + altitude) * cos_lat * np.sin(longitude) z = (radius * (1.0 - WGS84_E2) + altitude) * sin_lat return np.stack([x, y, z], axis=-1) def geodetic_to_enu( latitude_deg: np.ndarray, longitude_deg: np.ndarray, altitude_m: np.ndarray, *, origin_latitude_deg: float | None = None, origin_longitude_deg: float | None = None, origin_altitude_m: float | None = None, ) -> tuple[np.ndarray, tuple[float, float, float]]: """Convert WGS84 samples to a local east/north/up frame. When no origin is supplied, the first finite sample is used. The returned origin tuple is ``(latitude_deg, longitude_deg, altitude_m)``. """ lat = np.asarray(latitude_deg, dtype=float).reshape(-1) lon = np.asarray(longitude_deg, dtype=float).reshape(-1) alt = np.asarray(altitude_m, dtype=float).reshape(-1) if not (lat.size == lon.size == alt.size): raise ValueError("latitude, longitude and altitude must have equal length") finite = np.isfinite(lat) & np.isfinite(lon) & np.isfinite(alt) if not np.any(finite): raise ValueError("no finite geodetic sample") first = int(np.flatnonzero(finite)[0]) lat0 = float(lat[first] if origin_latitude_deg is None else origin_latitude_deg) lon0 = float(lon[first] if origin_longitude_deg is None else origin_longitude_deg) alt0 = float(alt[first] if origin_altitude_m is None else origin_altitude_m) ecef = geodetic_to_ecef(lat, lon, alt) ecef0 = geodetic_to_ecef(np.array(lat0), np.array(lon0), np.array(alt0)).reshape(3) delta = ecef - ecef0 phi = np.deg2rad(lat0) lam = np.deg2rad(lon0) rotation = np.array( [ [-np.sin(lam), np.cos(lam), 0.0], [-np.sin(phi) * np.cos(lam), -np.sin(phi) * np.sin(lam), np.cos(phi)], [np.cos(phi) * np.cos(lam), np.cos(phi) * np.sin(lam), np.sin(phi)], ], dtype=float, ) return delta @ rotation.T, (lat0, lon0, alt0)