use super::funejson::Value; use super::math_atan::atan; use super::math_round_float::round_float; use super::math_sin_cos::sin_cos; // 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(), )) }