Functional Weave
Code in TypeScript

geo.distance@1.0.0

impl/python.py

4,692 bytes · the Python implementation · view raw

import math
from typing import Tuple

# Why this does not call math.sin, math.cos or math.atan2: those come from the
# platform maths library, which may differ in the last bit between Python,
# JavaScript and Rust, and a value next to a rounding boundary can then round
# differently. The trigonometry below uses only operations IEEE 754 requires to
# be correctly rounded (+ - * / sqrt floor), in the same order as the other two
# implementations, so the unrounded result is the same double everywhere.

# IUGG mean radius R1 = (2a + b) / 3 of the GRS 80 ellipsoid (Moritz,
# "Geodetic Reference System 1980", Journal of Geodesy 74 (2000) 128-133).
EARTH_RADIUS_METRES = 6371008.8

PI = 3.141592653589793
HALF_PI = PI / 2
DEGREES = PI / 180

S3, S5, S7, S9, S11 = -1 / 6, 1 / 120, -1 / 5040, 1 / 362880, -1 / 39916800
S13, S15, S17 = 1 / 6227020800, -1 / 1307674368000, 1 / 355687428096000
C2, C4, C6, C8, C10 = -1 / 2, 1 / 24, -1 / 720, 1 / 40320, -1 / 3628800
C12, C14, C16, C18 = 1 / 479001600, -1 / 87178291200, 1 / 20922789888000, -1 / 6402373705728000
A3, A5, A7, A9, A11 = -1 / 3, 1 / 5, -1 / 7, 1 / 9, -1 / 11
A13, A15, A17, A19, A21 = 1 / 13, -1 / 15, 1 / 17, -1 / 19, 1 / 21


def _sin_small(r: float) -> float:
    s = r * r
    p = S17
    p = S15 + s * p
    p = S13 + s * p
    p = S11 + s * p
    p = S9 + s * p
    p = S7 + s * p
    p = S5 + s * p
    p = S3 + s * p
    return r + r * s * p


def _cos_small(r: float) -> float:
    s = r * r
    p = C18
    p = C16 + s * p
    p = C14 + s * p
    p = C12 + s * p
    p = C10 + s * p
    p = C8 + s * p
    p = C6 + s * p
    p = C4 + s * p
    p = C2 + s * p
    return 1 + s * p


def _sin_cos(x: float) -> Tuple[float, float]:
    k = math.floor(x / HALF_PI + 0.5)
    r = x - float(k) * HALF_PI
    sr = _sin_small(r)
    cr = _cos_small(r)
    quadrant = k % 4
    if quadrant == 0:
        return sr, cr
    if quadrant == 1:
        return cr, -sr
    if quadrant == 2:
        return -sr, -cr
    return -cr, sr


def _atan_unit(u: float) -> float:
    v = u
    v = v / (1 + math.sqrt(1 + v * v))
    v = v / (1 + math.sqrt(1 + v * v))
    v = v / (1 + math.sqrt(1 + v * v))
    s = v * v
    p = A21
    p = A19 + s * p
    p = A17 + s * p
    p = A15 + s * p
    p = A13 + s * p
    p = A11 + s * p
    p = A9 + s * p
    p = A7 + s * p
    p = A5 + s * p
    p = A3 + s * p
    return 8 * (v + v * s * p)


def _atan2_positive(y: float, x: float) -> float:
    if x == 0:
        return 0.0 if y == 0 else HALF_PI
    t = y / x
    return HALF_PI - _atan_unit(x / y) if t > 1 else _atan_unit(t)


def _round_to(x: float, scale: float) -> float:
    # Half away from zero on the binary64 value, not Python's round(), which
    # rounds half to even. + 0.0 turns -0.0 into 0.0.
    y = abs(x) * scale
    r = float(math.floor(y))
    if y - r >= 0.5:
        r += 1
    out = r / scale
    return (-out if x < 0 else out) + 0.0


def _is_number(value: object) -> bool:
    return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)


def _check_latitude(value: float) -> None:
    if not _is_number(value):
        raise TypeError("latitude must be a finite number of degrees")
    if value < -90 or value > 90:
        raise ValueError("latitude must be between -90 and 90 degrees")


def _check_longitude(value: float) -> None:
    if not _is_number(value):
        raise TypeError("longitude must be a finite number of degrees")
    if value < -180 or value > 180:
        raise ValueError("longitude must be between -180 and 180 degrees")


def distance(from_lat: float, from_lng: float, to_lat: float, to_lng: float) -> float:
    """Great-circle distance in metres (haversine, IUGG mean radius), to the millimetre."""
    _check_latitude(from_lat)
    _check_longitude(from_lng)
    _check_latitude(to_lat)
    _check_longitude(to_lng)
    from_lat, from_lng, to_lat, to_lng = float(from_lat), float(from_lng), float(to_lat), float(to_lng)

    phi1 = from_lat * DEGREES
    phi2 = to_lat * DEGREES
    # A longitude difference of 359 degrees is a 1 degree step across the
    # antimeridian; sin^2 of the half-angle is the same either way.
    sin_half_lat = _sin_cos(((to_lat - from_lat) * DEGREES) / 2)[0]
    sin_half_lng = _sin_cos(((to_lng - from_lng) * DEGREES) / 2)[0]
    cos1 = _sin_cos(phi1)[1]
    cos2 = _sin_cos(phi2)[1]

    a = sin_half_lat * sin_half_lat + cos1 * cos2 * sin_half_lng * sin_half_lng
    if a < 0:
        a = 0.0
    if a > 1:
        a = 1.0
    # atan2 rather than asin(sqrt(a)): stays accurate for antipodal points too.
    c = 2 * _atan2_positive(math.sqrt(a), math.sqrt(1 - a))
    return _round_to(EARTH_RADIUS_METRES * c, 1000.0)