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).