Functional Weave
Code in TypeScript

geo.distance@2.0.0

impl/rust.rs

3,673 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_atan::atan;  ← from math.atan ^1.0.0 · built alongside by fune
use super::math_round_float::round_float;  ← from math.round-float ^1.0.0 · built alongside by fune
use super::math_sin_cos::sin_cos;  ← from math.sin-cos ^1.0.0 · built alongside by fune

// Sine, cosine and arctangent come from math.sin-cos and math.atan, not
// f64::sin or f64::atan2: those come from the platform maths library, which may
// differ in the last bit between Rust, JavaScript and Python, and a value next
// to a rounding boundary can then round differently. With the shared helpers
// the unrounded distance is the same double everywhere, and math.round-float
// rounds it the same way 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).
const EARTH_RADIUS_METRES: f64 = 6371008.8;

const PI: f64 = 3.141592653589793;
const HALF_PI: f64 = PI / 2.0;
const DEGREES: f64 = PI / 180.0;

/// atan2 for y >= 0 and x >= 0. x is 0 only for exactly antipodal points,
/// where y / x would be infinite, which math.atan refuses.
fn atan2_positive(y: f64, x: f64) -> f64 {
    if x == 0.0 {
        return if y == 0.0 { 0.0 } else { HALF_PI };
    }
    atan(y / x)
}

fn check_latitude(value: f64) {
    if !value.is_finite() {
        panic!("latitude must be a finite number of degrees");
    }
    if value < -90.0 || value > 90.0 {
        panic!("latitude must be between -90 and 90 degrees");
    }
}

fn check_longitude(value: f64) {
    if !value.is_finite() {
        panic!("longitude must be a finite number of degrees");
    }
    if value < -180.0 || value > 180.0 {
        panic!("longitude must be between -180 and 180 degrees");
    }
}

/// Great-circle distance in metres (haversine, IUGG mean radius), to the millimetre.
///
/// # Panics
/// Panics if a coordinate is not finite or out of range.
pub fn distance(from_lat: f64, from_lng: f64, to_lat: f64, to_lng: f64) -> f64 {
    check_latitude(from_lat);
    check_longitude(from_lng);
    check_latitude(to_lat);
    check_longitude(to_lng);

    // 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.
    let sin_half_lat = sin_cos(((to_lat - from_lat) * DEGREES) / 2.0).sin;
    let sin_half_lng = sin_cos(((to_lng - from_lng) * DEGREES) / 2.0).sin;
    let cos1 = sin_cos(from_lat * DEGREES).cos;
    let cos2 = sin_cos(to_lat * DEGREES).cos;

    let mut a = sin_half_lat * sin_half_lat + cos1 * cos2 * sin_half_lng * sin_half_lng;
    if a < 0.0 {
        a = 0.0;
    }
    if a > 1.0 {
        a = 1.0;
    }
    // atan2 rather than asin(sqrt(a)): stays accurate for antipodal points too.
    let c = 2.0 * atan2_positive(a.sqrt(), (1.0 - a).sqrt());
    // Half away from zero on the exact value of the double (math.round-float).
    round_float(EARTH_RADIUS_METRES * c, 3)
}

pub fn fune_vector(args: &[Value]) -> Value {
    // Refuse what the typed signature cannot hold, with the wording TypeScript
    // and Python use, rather than let the conversion below quietly change it.
    if !matches!(args[0], Value::Int(_) | Value::Float(_)) {
        panic!("latitude must be a finite number of degrees");
    }
    if !matches!(args[1], Value::Int(_) | Value::Float(_)) {
        panic!("longitude must be a finite number of degrees");
    }
    if !matches!(args[2], Value::Int(_) | Value::Float(_)) {
        panic!("latitude must be a finite number of degrees");
    }
    if !matches!(args[3], Value::Int(_) | Value::Float(_)) {
        panic!("longitude must be a finite number of degrees");
    }
    Value::Float(distance(
        args[0].as_f64(),
        args[1].as_f64(),
        args[2].as_f64(),
        args[3].as_f64(),
    ))
}