vrl/stdlib/
haversine.rs

1use std::collections::BTreeMap;
2use std::sync::LazyLock;
3
4use crate::compiler::function::EnumVariant;
5use crate::compiler::prelude::*;
6use crate::value;
7
8use super::util::round_to_precision;
9
10const EARTH_RADIUS_IN_METERS: f64 = 6_371_008.8;
11const EARTH_RADIUS_IN_KILOMETERS: f64 = EARTH_RADIUS_IN_METERS / 1000.0;
12const EARTH_RADIUS_IN_MILES: f64 = EARTH_RADIUS_IN_KILOMETERS * 0.621_371_2;
13
14static DEFAULT_MEASUREMENT_UNIT: LazyLock<Value> = LazyLock::new(|| value!("kilometers"));
15
16fn haversine_distance(
17    latitude1: Value,
18    longitude1: Value,
19    latitude2: Value,
20    longitude2: Value,
21    measurement_unit: &MeasurementUnit,
22) -> Resolved {
23    let latitude1 = latitude1.try_float()?.to_radians();
24    let longitude1 = longitude1.try_float()?.to_radians();
25    let latitude2 = latitude2.try_float()?.to_radians();
26    let longitude2 = longitude2.try_float()?.to_radians();
27
28    let mut result = ObjectMap::new();
29
30    // Distance calculation
31    let dlon = longitude2 - longitude1;
32    let dlat = latitude2 - latitude1;
33    let a =
34        (dlat / 2.0).sin().powi(2) + latitude1.cos() * latitude2.cos() * (dlon / 2.0).sin().powi(2);
35    let distance = 2.0 * a.sqrt().asin();
36
37    result.insert(
38        "distance".into(),
39        match measurement_unit {
40            MeasurementUnit::Kilometers => Value::from_f64_or_zero(round_to_precision(
41                distance * EARTH_RADIUS_IN_KILOMETERS,
42                7,
43                f64::round,
44            )),
45            MeasurementUnit::Miles => Value::from_f64_or_zero(round_to_precision(
46                distance * EARTH_RADIUS_IN_MILES,
47                7,
48                f64::round,
49            )),
50        },
51    );
52
53    // Bearing calculation
54    let y = dlon.sin() * latitude2.cos();
55    let x = latitude1.cos() * latitude2.sin() - latitude1.sin() * latitude2.cos() * dlon.cos();
56    let bearing = (y.atan2(x).to_degrees() + 360.0) % 360.0;
57
58    result.insert(
59        "bearing".into(),
60        Value::from_f64_or_zero(round_to_precision(bearing, 3, f64::round)),
61    );
62
63    Ok(result.into())
64}
65
66fn measurement_systems() -> Vec<Value> {
67    vec![value!("kilometers"), value!("miles")]
68}
69
70#[derive(Clone, Debug)]
71enum MeasurementUnit {
72    Kilometers,
73    Miles,
74}
75
76#[derive(Clone, Copy, Debug)]
77pub struct Haversine;
78
79impl Function for Haversine {
80    fn identifier(&self) -> &'static str {
81        "haversine"
82    }
83
84    fn usage(&self) -> &'static str {
85        "Calculates [haversine](https://en.wikipedia.org/wiki/Haversine_formula) distance and bearing between two points."
86    }
87
88    fn category(&self) -> &'static str {
89        Category::Map.as_ref()
90    }
91
92    fn return_kind(&self) -> u16 {
93        kind::OBJECT
94    }
95
96    fn parameters(&self) -> &'static [Parameter] {
97        const PARAMETERS: &[Parameter] = &[
98            Parameter::required("latitude1", kind::FLOAT, "Latitude of the first point."),
99            Parameter::required("longitude1", kind::FLOAT, "Longitude of the first point."),
100            Parameter::required("latitude2", kind::FLOAT, "Latitude of the second point."),
101            Parameter::required("longitude2", kind::FLOAT, "Longitude of the second point."),
102            Parameter::optional(
103                "measurement_unit",
104                kind::BYTES,
105                "Measurement system to use for resulting distance.",
106            )
107            .enum_variants(&[
108                EnumVariant {
109                    value: "kilometers",
110                    description: "Use kilometers for the resulting distance.",
111                },
112                EnumVariant {
113                    value: "miles",
114                    description: "Use miles for the resulting distance.",
115                },
116            ]),
117        ];
118        PARAMETERS
119    }
120
121    fn compile(
122        &self,
123        state: &state::TypeState,
124        _ctx: &mut FunctionCompileContext,
125        arguments: ArgumentList,
126    ) -> Compiled {
127        let latitude1 = arguments.required("latitude1");
128        let longitude1 = arguments.required("longitude1");
129        let latitude2 = arguments.required("latitude2");
130        let longitude2 = arguments.required("longitude2");
131
132        let measurement_unit = match arguments
133            .optional_enum("measurement_unit", &measurement_systems(), state)?
134            .unwrap_or_else(|| DEFAULT_MEASUREMENT_UNIT.clone())
135            .try_bytes()
136            .ok()
137            .as_deref()
138        {
139            Some(b"kilometers") => MeasurementUnit::Kilometers,
140            Some(b"miles") => MeasurementUnit::Miles,
141            _ => return Err(Box::new(ExpressionError::from("invalid measurement unit"))),
142        };
143
144        Ok(HaversineFn {
145            latitude1,
146            longitude1,
147            latitude2,
148            longitude2,
149            measurement_unit,
150        }
151        .as_expr())
152    }
153
154    fn examples(&self) -> &'static [Example] {
155        &[
156            example! {
157                title: "Haversine in kilometers",
158                source: "haversine(0.0, 0.0, 10.0, 10.0)",
159                result: Ok(indoc!(
160                    r#"{
161                        "distance": 1568.5227233,
162                        "bearing": 44.561
163                    }"#
164                )),
165            },
166            example! {
167                title: "Haversine in miles",
168                source: r#"haversine(0.0, 0.0, 10.0, 10.0, measurement_unit: "miles")"#,
169                result: Ok(indoc!(
170                    r#"{
171                        "distance": 974.6348468,
172                        "bearing": 44.561
173                    }"#
174                )),
175            },
176        ]
177    }
178}
179
180#[derive(Clone, Debug)]
181struct HaversineFn {
182    latitude1: Box<dyn Expression>,
183    longitude1: Box<dyn Expression>,
184    latitude2: Box<dyn Expression>,
185    longitude2: Box<dyn Expression>,
186    measurement_unit: MeasurementUnit,
187}
188
189impl FunctionExpression for HaversineFn {
190    fn resolve(&self, ctx: &mut Context) -> Resolved {
191        let latitude1 = self.latitude1.resolve(ctx)?;
192        let longitude1 = self.longitude1.resolve(ctx)?;
193        let latitude2 = self.latitude2.resolve(ctx)?;
194        let longitude2 = self.longitude2.resolve(ctx)?;
195
196        haversine_distance(
197            latitude1,
198            longitude1,
199            latitude2,
200            longitude2,
201            &self.measurement_unit,
202        )
203    }
204
205    fn type_def(&self, _state: &state::TypeState) -> TypeDef {
206        TypeDef::object(inner_kind()).infallible()
207    }
208}
209
210fn inner_kind() -> BTreeMap<Field, Kind> {
211    BTreeMap::from([
212        (Field::from("distance"), Kind::float()),
213        (Field::from("bearing"), Kind::float()),
214    ])
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use crate::value;
221
222    test_function![
223        haversine => Haversine;
224
225        basic_kilometers {
226            args: func_args![latitude1: value!(0.0), longitude1: value!(0.0), latitude2: value!(10.0), longitude2: value!(10.0)],
227            want: Ok(value!({ "distance": 1_568.522_723_3, "bearing": 44.561 })),
228            tdef: TypeDef::object(inner_kind()).infallible(),
229        }
230
231        basic_miles {
232            args: func_args![latitude1: value!(0.0), longitude1: value!(0.0), latitude2: value!(10.0), longitude2: value!(10.0), measurement_unit: value!("miles")],
233            want: Ok(value!({ "distance": 974.634_846_8, "bearing": 44.561 })),
234            tdef: TypeDef::object(inner_kind()).infallible(),
235        }
236    ];
237}