haptic_skin.geo
Geographic helpers for navigation: distances, bearings, directional buckets.
1"""Geographic helpers for navigation: distances, bearings, directional buckets.""" 2 3from __future__ import annotations 4 5import math 6from typing import Tuple 7 8EARTH_RADIUS_M = 6_371_000.0 9LatLon = Tuple[float, float] 10 11 12def haversine_m(a: LatLon, b: LatLon) -> float: 13 """Great-circle distance between two (lat, lon) points, in metres.""" 14 lat1, lon1 = math.radians(a[0]), math.radians(a[1]) 15 lat2, lon2 = math.radians(b[0]), math.radians(b[1]) 16 dlat, dlon = lat2 - lat1, lon2 - lon1 17 h = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2 18 return 2 * EARTH_RADIUS_M * math.asin(min(1.0, math.sqrt(h))) 19 20 21def bearing_deg(a: LatLon, b: LatLon) -> float: 22 """Initial compass bearing from a to b, degrees in [0, 360).""" 23 lat1, lat2 = math.radians(a[0]), math.radians(b[0]) 24 dlon = math.radians(b[1] - a[1]) 25 x = math.sin(dlon) * math.cos(lat2) 26 y = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon) 27 return (math.degrees(math.atan2(x, y)) + 360.0) % 360.0 28 29 30def relative_bearing(target_abs: float, heading: float) -> float: 31 """Signed angle (deg) of ``target_abs`` relative to ``heading``, in (-180, 180]. 32 33 0 = straight ahead, +90 = to the right, -90 = to the left, 180 = behind. 34 """ 35 diff = (target_abs - heading + 180.0) % 360.0 - 180.0 36 return diff if diff != -180.0 else 180.0 37 38 39def bucket_from_relative(relative: float) -> int: 40 """Map a relative bearing to one of 8 motor indices (0 = front, clockwise).""" 41 return round(relative / 45.0) % 8
EARTH_RADIUS_M =
6371000.0
LatLon =
typing.Tuple[float, float]
def
haversine_m(a: Tuple[float, float], b: Tuple[float, float]) -> float:
13def haversine_m(a: LatLon, b: LatLon) -> float: 14 """Great-circle distance between two (lat, lon) points, in metres.""" 15 lat1, lon1 = math.radians(a[0]), math.radians(a[1]) 16 lat2, lon2 = math.radians(b[0]), math.radians(b[1]) 17 dlat, dlon = lat2 - lat1, lon2 - lon1 18 h = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2 19 return 2 * EARTH_RADIUS_M * math.asin(min(1.0, math.sqrt(h)))
Great-circle distance between two (lat, lon) points, in metres.
def
bearing_deg(a: Tuple[float, float], b: Tuple[float, float]) -> float:
22def bearing_deg(a: LatLon, b: LatLon) -> float: 23 """Initial compass bearing from a to b, degrees in [0, 360).""" 24 lat1, lat2 = math.radians(a[0]), math.radians(b[0]) 25 dlon = math.radians(b[1] - a[1]) 26 x = math.sin(dlon) * math.cos(lat2) 27 y = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon) 28 return (math.degrees(math.atan2(x, y)) + 360.0) % 360.0
Initial compass bearing from a to b, degrees in [0, 360).
def
relative_bearing(target_abs: float, heading: float) -> float:
31def relative_bearing(target_abs: float, heading: float) -> float: 32 """Signed angle (deg) of ``target_abs`` relative to ``heading``, in (-180, 180]. 33 34 0 = straight ahead, +90 = to the right, -90 = to the left, 180 = behind. 35 """ 36 diff = (target_abs - heading + 180.0) % 360.0 - 180.0 37 return diff if diff != -180.0 else 180.0
Signed angle (deg) of target_abs relative to heading, in (-180, 180].
0 = straight ahead, +90 = to the right, -90 = to the left, 180 = behind.
def
bucket_from_relative(relative: float) -> int:
40def bucket_from_relative(relative: float) -> int: 41 """Map a relative bearing to one of 8 motor indices (0 = front, clockwise).""" 42 return round(relative / 45.0) % 8
Map a relative bearing to one of 8 motor indices (0 = front, clockwise).