geo/algorithm/line_measures/metric_spaces/
rhumb.rs

1use num_traits::FromPrimitive;
2
3use super::super::{Bearing, Destination, Distance, InterpolatePoint};
4use crate::rhumb::RhumbCalculations;
5use crate::{CoordFloat, Point, MEAN_EARTH_RADIUS};
6
7/// Provides [rhumb line] (a.k.a. loxodrome) geometry operations. A rhumb line appears as a straight
8/// line on a Mercator projection map.
9///
10/// Rhumb distance is measured in meters.
11///
12/// # References
13///
14/// The distance, destination, and bearing implementations are adapted in part
15/// from their equivalents in [Turf.js](https://turfjs.org/), which in turn are
16/// adapted from the Movable Type
17/// [spherical geodesy tools](https://www.movable-type.co.uk/scripts/latlong.html).
18///
19/// Turf.js is copyright its authors and the geodesy tools are copyright Chris
20/// Veness; both are available under an MIT license.
21///
22/// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line
23pub struct Rhumb;
24
25impl<F: CoordFloat + FromPrimitive> Bearing<F> for Rhumb {
26    /// Returns the bearing from `origin` to `destination` in degrees along a [rhumb line].
27    ///
28    /// # Units
29    ///
30    /// - `origin`, `destination`: Points where x/y are lon/lat degree coordinates
31    /// - returns: degrees, where: North: 0°, East: 90°, South: 180°, West: 270°/
32    ///
33    /// # Examples
34    ///
35    /// ```
36    /// # use approx::assert_relative_eq;
37    /// use geo::{Rhumb, Bearing};
38    /// use geo::Point;
39    ///
40    /// let origin = Point::new(9.177789688110352, 48.776781529534965);
41    /// let destination = Point::new(9.274348757829898, 48.84037308229984);
42    /// let bearing = Rhumb.bearing(origin, destination);
43    /// assert_relative_eq!(bearing, 45., epsilon = 1.0e-6);
44    /// ```
45    ///
46    /// # References
47    ///
48    /// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line
49    ///
50    /// Bullock, R.: Great Circle Distances and Bearings Between Two Locations, 2007.
51    /// (<https://dtcenter.org/met/users/docs/write_ups/gc_simple.pdf>)
52    fn bearing(&self, origin: Point<F>, destination: Point<F>) -> F {
53        let three_sixty = F::from(360.0f64).unwrap();
54
55        let calculations = RhumbCalculations::new(&origin, &destination);
56        (calculations.theta().to_degrees() + three_sixty) % three_sixty
57    }
58}
59
60impl<F: CoordFloat + FromPrimitive> Destination<F> for Rhumb {
61    /// Returns a new point having travelled the `distance` along a [rhumb line]
62    /// from the `origin` point with the given `bearing`.
63    ///
64    /// # Units
65    ///
66    /// - `origin`: Point where x/y are lon/lat degree coordinates
67    /// - `bearing`: degrees, where: North: 0°, East: 90°, South: 180°, West: 270°
68    /// - `distance`: meters
69    /// - returns: Point where x/y are lon/lat degree coordinates
70    ///
71    /// # Examples
72    ///
73    /// ```
74    /// # use approx::assert_relative_eq;
75    /// use geo::{Rhumb, Destination};
76    /// use geo::Point;
77    ///
78    /// let p_1 = Point::new(9.177789688110352, 48.776781529534965);
79    /// let p_2 = Rhumb.destination(p_1, 45., 10000.);
80    /// assert_relative_eq!(p_2, Point::new(9.274348757829898, 48.84037308229984))
81    /// ```
82    ///
83    /// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line
84    fn destination(&self, origin: Point<F>, bearing: F, distance: F) -> Point<F> {
85        let delta = distance / F::from(MEAN_EARTH_RADIUS).unwrap(); // angular distance in radians
86        let lambda1 = origin.x().to_radians();
87        let phi1 = origin.y().to_radians();
88        let theta = bearing.to_radians();
89
90        crate::algorithm::rhumb::calculate_destination(delta, lambda1, phi1, theta)
91    }
92}
93
94impl<F: CoordFloat + FromPrimitive> Distance<F, Point<F>, Point<F>> for Rhumb {
95    /// Determine the distance along the [rhumb line] between two points.
96    ///
97    /// # Units
98    ///
99    /// - `origin`, `destination`: Points where x/y are lon/lat degree coordinates
100    /// - returns: meters
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// use geo::{Rhumb, Distance};
106    /// use geo::point;
107    ///
108    /// // New York City
109    /// let p1 = point!(x: -74.006f64, y: 40.7128);
110    ///
111    /// // London
112    /// let p2 = point!(x: -0.1278, y: 51.5074);
113    ///
114    /// let distance = Rhumb.distance(p1, p2);
115    ///
116    /// assert_eq!(
117    ///     5_794_129., // meters
118    ///     distance.round()
119    /// );
120    /// ```
121    ///
122    /// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line
123    fn distance(&self, origin: Point<F>, destination: Point<F>) -> F {
124        let calculations = RhumbCalculations::new(&origin, &destination);
125        calculations.delta() * F::from(MEAN_EARTH_RADIUS).unwrap()
126    }
127}
128
129/// Interpolate Point(s) along a [rhumb line].
130///
131/// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line
132impl<F: CoordFloat + FromPrimitive> InterpolatePoint<F> for Rhumb {
133    /// Returns a new Point along a [rhumb line] between two existing points.
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// # use approx::assert_relative_eq;
139    /// use geo::{Rhumb, InterpolatePoint};
140    /// use geo::Point;
141    ///
142    /// let p1 = Point::new(10.0, 20.0);
143    /// let p2 = Point::new(125.0, 25.0);
144    ///
145    /// let closer_to_p1 = Rhumb.point_at_distance_between(p1, p2, 100_000.0);
146    /// assert_relative_eq!(closer_to_p1, Point::new(10.96, 20.04), epsilon = 1.0e-2);
147    ///
148    /// let closer_to_p2 = Rhumb.point_at_distance_between(p1, p2, 10_000_000.0);
149    /// assert_relative_eq!(closer_to_p2, Point::new(107.00, 24.23), epsilon = 1.0e-2);
150    /// ```
151    ///
152    /// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line
153    fn point_at_distance_between(
154        &self,
155        start: Point<F>,
156        end: Point<F>,
157        meters_from_start: F,
158    ) -> Point<F> {
159        let bearing = Self.bearing(start, end);
160        Self.destination(start, bearing, meters_from_start)
161    }
162
163    /// Returns a new Point along a [rhumb line] between two existing points.
164    ///
165    /// # Examples
166    ///
167    /// ```
168    /// # use approx::assert_relative_eq;
169    /// use geo::{Rhumb, InterpolatePoint};
170    /// use geo::Point;
171    ///
172    /// let p1 = Point::new(10.0, 20.0);
173    /// let p2 = Point::new(125.0, 25.0);
174    ///
175    /// let closer_to_p1 = Rhumb.point_at_ratio_between(p1, p2, 0.1);
176    /// assert_relative_eq!(closer_to_p1, Point::new(21.32, 20.50), epsilon = 1.0e-2);
177    ///
178    /// let closer_to_p2 = Rhumb.point_at_ratio_between(p1, p2, 0.9);
179    /// assert_relative_eq!(closer_to_p2, Point::new(113.31, 24.50), epsilon = 1.0e-2);
180    ///
181    /// let midpoint = Rhumb.point_at_ratio_between(p1, p2, 0.5);
182    /// assert_relative_eq!(midpoint, Point::new(66.98, 22.50), epsilon = 1.0e-2);
183    /// ```
184    ///
185    /// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line
186    fn point_at_ratio_between(
187        &self,
188        start: Point<F>,
189        end: Point<F>,
190        ratio_from_start: F,
191    ) -> Point<F> {
192        let calculations = RhumbCalculations::new(&start, &end);
193        calculations.intermediate(ratio_from_start)
194    }
195
196    /// Interpolates `Point`s along a [rhumb line] between `start` and `end`.
197    ///
198    /// As many points as necessary will be added such that the distance between points
199    /// never exceeds `max_distance`. If the distance between start and end is less than
200    /// `max_distance`, no additional points will be included in the output.
201    ///
202    /// `include_ends`: Should the start and end points be included in the output?
203    ///
204    /// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line
205    fn points_along_line(
206        &self,
207        start: Point<F>,
208        end: Point<F>,
209        max_distance: F,
210        include_ends: bool,
211    ) -> impl Iterator<Item = Point<F>> {
212        let max_delta = max_distance / F::from(MEAN_EARTH_RADIUS).unwrap();
213        let calculations = RhumbCalculations::new(&start, &end);
214        calculations
215            .intermediate_fill(max_delta, include_ends)
216            .into_iter()
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    mod bearing {
225        use super::*;
226
227        #[test]
228        fn north() {
229            let origin = Point::new(0.0, 0.0);
230            let destination = Point::new(0.0, 1.0);
231            assert_relative_eq!(0.0, Rhumb.bearing(origin, destination));
232        }
233
234        #[test]
235        fn east() {
236            let origin = Point::new(0.0, 0.0);
237            let destination = Point::new(1.0, 0.0);
238            assert_relative_eq!(90.0, Rhumb.bearing(origin, destination));
239        }
240
241        #[test]
242        fn south() {
243            let origin = Point::new(0.0, 0.0);
244            let destination = Point::new(0.0, -1.0);
245            assert_relative_eq!(180.0, Rhumb.bearing(origin, destination));
246        }
247
248        #[test]
249        fn west() {
250            let origin = Point::new(0.0, 0.0);
251            let destination = Point::new(-1.0, 0.0);
252            assert_relative_eq!(270.0, Rhumb.bearing(origin, destination));
253        }
254    }
255
256    mod destination {
257        use super::*;
258
259        #[test]
260        fn north() {
261            let origin = Point::new(0.0, 0.0);
262            let bearing = 0.0;
263            assert_relative_eq!(
264                Point::new(0.0, 0.899320363724538),
265                Rhumb.destination(origin, bearing, 100_000.0)
266            );
267        }
268
269        #[test]
270        fn east() {
271            let origin = Point::new(0.0, 0.0);
272            let bearing = 90.0;
273            assert_relative_eq!(
274                Point::new(0.8993203637245415, 5.506522912913066e-17),
275                Rhumb.destination(origin, bearing, 100_000.0)
276            );
277        }
278
279        #[test]
280        fn south() {
281            let origin = Point::new(0.0, 0.0);
282            let bearing = 180.0;
283            assert_relative_eq!(
284                Point::new(0.0, -0.899320363724538),
285                Rhumb.destination(origin, bearing, 100_000.0)
286            );
287        }
288
289        #[test]
290        fn west() {
291            let origin = Point::new(0.0, 0.0);
292            let bearing = 270.0;
293            assert_relative_eq!(
294                Point::new(-0.8993203637245415, -1.6520247072649334e-16),
295                Rhumb.destination(origin, bearing, 100_000.0)
296            );
297        }
298    }
299
300    mod distance {
301        use super::*;
302
303        #[test]
304        fn new_york_to_london() {
305            let new_york_city = Point::new(-74.006, 40.7128);
306            let london = Point::new(-0.1278, 51.5074);
307
308            let distance: f64 = Rhumb.distance(new_york_city, london);
309
310            assert_relative_eq!(
311                5_794_129., // meters
312                distance.round()
313            );
314        }
315    }
316
317    mod interpolate_point {
318        use super::*;
319
320        #[test]
321        fn point_at_ratio_between_midpoint() {
322            let start = Point::new(10.0, 20.0);
323            let end = Point::new(125.0, 25.0);
324            let midpoint = Rhumb.point_at_ratio_between(start, end, 0.5);
325            assert_relative_eq!(
326                midpoint,
327                Point::new(66.98011173721943, 22.500000000000007),
328                epsilon = 1.0e-10
329            );
330        }
331        #[test]
332        fn points_along_line_with_endpoints() {
333            let start = Point::new(10.0, 20.0);
334            let end = Point::new(125.0, 25.0);
335            let max_dist = 1000000.0; // meters
336            let route = Rhumb
337                .points_along_line(start, end, max_dist, true)
338                .collect::<Vec<_>>();
339            assert_eq!(route.len(), 13);
340            assert_eq!(route[0], start);
341            assert_eq!(route.last().unwrap(), &end);
342            assert_relative_eq!(
343                route[1],
344                Point::new(19.43061818495096, 20.416666666666668),
345                epsilon = 1.0e-10
346            );
347        }
348        #[test]
349        fn points_along_line_without_endpoints() {
350            let start = Point::new(10.0, 20.0);
351            let end = Point::new(125.0, 25.0);
352            let max_dist = 1000000.0; // meters
353            let route = Rhumb
354                .points_along_line(start, end, max_dist, false)
355                .collect::<Vec<_>>();
356            assert_eq!(route.len(), 11);
357            assert_relative_eq!(
358                route[0],
359                Point::new(19.43061818495096, 20.416666666666668),
360                epsilon = 1.0e-10
361            );
362        }
363    }
364}