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}