Skip to content

Commit 173085f

Browse files
authored
Merge pull request #1090 from apendleton/rhumb-ops
Add rhumb-line operations
2 parents d95daf8 + ed2ff01 commit 173085f

File tree

10 files changed

+676
-3
lines changed

10 files changed

+676
-3
lines changed

geo/CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
* <https://github.com/georust/geo/pull/908>
1919
* Add `ToDegrees` and `ToRadians` traits.
2020
* <https://github.com/georust/geo/pull/1070>
21+
* Add rhumb-line operations analogous to several current haversine operations: `RhumbBearing`, `RhumbDestination`, `RhumbDistance`, `RhumbIntermediate`, `RhumbLength`.
22+
* <https://github.com/georust/geo/pull/1090>
2123

2224
## 0.26.0
2325

geo/src/algorithm/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,3 +283,7 @@ pub use outlier_detection::OutlierDetection;
283283
/// Monotonic polygon subdivision
284284
pub mod monotone;
285285
pub use monotone::{monotone_subdivision, MonoPoly, MonotonicPolygons};
286+
287+
/// Rhumb-line-related algorithms and utils
288+
pub mod rhumb;
289+
pub use rhumb::{RhumbBearing, RhumbDestination, RhumbDistance, RhumbIntermediate, RhumbLength};

geo/src/algorithm/rhumb/bearing.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use num_traits::FromPrimitive;
2+
3+
use crate::{CoordFloat, Point};
4+
5+
use super::RhumbCalculations;
6+
7+
/// Returns the bearing to another Point in degrees.
8+
///
9+
/// Bullock, R.: Great Circle Distances and Bearings Between Two Locations, 2007.
10+
/// (<https://dtcenter.org/met/users/docs/write_ups/gc_simple.pdf>)
11+
12+
pub trait RhumbBearing<T: CoordFloat + FromPrimitive> {
13+
/// Returns the bearing to another Point in degrees along a [rhumb line], where North is 0° and East is 90°.
14+
///
15+
/// # Examples
16+
///
17+
/// ```
18+
/// # use approx::assert_relative_eq;
19+
/// use geo::RhumbBearing;
20+
/// use geo::Point;
21+
///
22+
/// let p_1 = Point::new(9.177789688110352, 48.776781529534965);
23+
/// let p_2 = Point::new(9.274348757829898, 48.84037308229984);
24+
/// let bearing = p_1.rhumb_bearing(p_2);
25+
/// assert_relative_eq!(bearing, 45., epsilon = 1.0e-6);
26+
/// ```
27+
/// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line
28+
29+
fn rhumb_bearing(&self, point: Point<T>) -> T;
30+
}
31+
32+
impl<T> RhumbBearing<T> for Point<T>
33+
where
34+
T: CoordFloat + FromPrimitive,
35+
{
36+
fn rhumb_bearing(&self, point: Point<T>) -> T {
37+
let three_sixty = T::from(360.0f64).unwrap();
38+
39+
let calculations = RhumbCalculations::new(self, &point);
40+
(calculations.theta().to_degrees() + three_sixty) % three_sixty
41+
}
42+
}
43+
44+
#[cfg(test)]
45+
mod test {
46+
use crate::point;
47+
use crate::RhumbBearing;
48+
use crate::RhumbDestination;
49+
50+
#[test]
51+
fn north_bearing() {
52+
let p_1 = point!(x: 9., y: 47.);
53+
let p_2 = point!(x: 9., y: 48.);
54+
let bearing = p_1.rhumb_bearing(p_2);
55+
assert_relative_eq!(bearing, 0.);
56+
}
57+
58+
#[test]
59+
fn equatorial_east_bearing() {
60+
let p_1 = point!(x: 9., y: 0.);
61+
let p_2 = point!(x: 10., y: 0.);
62+
let bearing = p_1.rhumb_bearing(p_2);
63+
assert_relative_eq!(bearing, 90.);
64+
}
65+
66+
#[test]
67+
fn east_bearing() {
68+
let p_1 = point!(x: 9., y: 10.);
69+
let p_2 = point!(x: 18.131938299366652, y: 10.);
70+
71+
let bearing = p_1.rhumb_bearing(p_2);
72+
assert_relative_eq!(bearing, 90.);
73+
}
74+
75+
#[test]
76+
fn northeast_bearing() {
77+
let p_1 = point!(x: 9.177789688110352f64, y: 48.776781529534965);
78+
let p_2 = point!(x: 9.274348757829898, y: 48.84037308229984);
79+
let bearing = p_1.rhumb_bearing(p_2);
80+
assert_relative_eq!(bearing, 45., epsilon = 1.0e-6);
81+
}
82+
83+
#[test]
84+
fn consistent_with_destination() {
85+
let p_1 = point!(x: 9.177789688110352f64, y: 48.776781529534965);
86+
let p_2 = p_1.rhumb_destination(45., 10000.);
87+
88+
let b_1 = p_1.rhumb_bearing(p_2);
89+
assert_relative_eq!(b_1, 45., epsilon = 1.0e-6);
90+
}
91+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use crate::{CoordFloat, Point, MEAN_EARTH_RADIUS};
2+
use num_traits::FromPrimitive;
3+
4+
use super::calculate_destination;
5+
6+
/// Returns the destination Point having travelled the given distance along a [rhumb line]
7+
/// from the origin geometry with the given bearing
8+
///
9+
/// *Note*: this implementation uses a mean earth radius of 6371.088 km, based on the [recommendation of
10+
/// the IUGG](ftp://athena.fsv.cvut.cz/ZFG/grs80-Moritz.pdf)
11+
pub trait RhumbDestination<T: CoordFloat> {
12+
/// Returns the destination Point having travelled the given distance along a [rhumb line]
13+
/// from the origin Point with the given bearing
14+
///
15+
/// # Units
16+
///
17+
/// - `bearing`: degrees, zero degrees is north
18+
/// - `distance`: meters
19+
///
20+
/// # Examples
21+
///
22+
/// ```
23+
/// use geo::RhumbDestination;
24+
/// use geo::Point;
25+
///
26+
/// let p_1 = Point::new(9.177789688110352, 48.776781529534965);
27+
/// let p_2 = p_1.rhumb_destination(45., 10000.);
28+
/// assert_eq!(p_2, Point::new(9.274348757829898, 48.84037308229984))
29+
/// ```
30+
/// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line
31+
fn rhumb_destination(&self, bearing: T, distance: T) -> Point<T>;
32+
}
33+
34+
impl<T> RhumbDestination<T> for Point<T>
35+
where
36+
T: CoordFloat + FromPrimitive,
37+
{
38+
fn rhumb_destination(&self, bearing: T, distance: T) -> Point<T> {
39+
let delta = distance / T::from(MEAN_EARTH_RADIUS).unwrap(); // angular distance in radians
40+
let lambda1 = self.x().to_radians();
41+
let phi1 = self.y().to_radians();
42+
let theta = bearing.to_radians();
43+
44+
calculate_destination(delta, lambda1, phi1, theta)
45+
}
46+
}
47+
48+
#[cfg(test)]
49+
mod test {
50+
use super::*;
51+
use crate::RhumbDistance;
52+
use num_traits::pow;
53+
54+
#[test]
55+
fn returns_a_new_point() {
56+
let p_1 = Point::new(9.177789688110352, 48.776781529534965);
57+
let p_2 = p_1.rhumb_destination(45., 10000.);
58+
assert_eq!(p_2, Point::new(9.274348757829898, 48.84037308229984));
59+
let distance = p_1.rhumb_distance(&p_2);
60+
assert_relative_eq!(distance, 10000., epsilon = 1.0e-6)
61+
}
62+
63+
#[test]
64+
fn direct_and_indirect_destinations_are_close() {
65+
let p_1 = Point::new(9.177789688110352, 48.776781529534965);
66+
let p_2 = p_1.rhumb_destination(45., 10000.);
67+
let square_edge = { pow(10000., 2) / 2f64 }.sqrt();
68+
let p_3 = p_1.rhumb_destination(0., square_edge);
69+
let p_4 = p_3.rhumb_destination(90., square_edge);
70+
assert_relative_eq!(p_4, p_2, epsilon = 1.0e-3);
71+
}
72+
73+
#[test]
74+
fn bearing_zero_is_north() {
75+
let p_1 = Point::new(9.177789688110352, 48.776781529534965);
76+
let p_2 = p_1.rhumb_destination(0., 1000.);
77+
assert_relative_eq!(p_1.x(), p_2.x(), epsilon = 1.0e-6);
78+
assert!(p_2.y() > p_1.y())
79+
}
80+
}

geo/src/algorithm/rhumb/distance.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use crate::{CoordFloat, Point, MEAN_EARTH_RADIUS};
2+
use num_traits::FromPrimitive;
3+
4+
use super::RhumbCalculations;
5+
6+
/// Determine the distance between two geometries along a [rhumb line].
7+
///
8+
/// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line
9+
///
10+
/// *Note*: this implementation uses a mean earth radius of 6371.088 km, based on the [recommendation of
11+
/// the IUGG](ftp://athena.fsv.cvut.cz/ZFG/grs80-Moritz.pdf)
12+
pub trait RhumbDistance<T, Rhs = Self> {
13+
/// Determine the distance between along the [rhumb line] between two geometries.
14+
///
15+
/// # Units
16+
///
17+
/// - return value: meters
18+
///
19+
/// # Examples
20+
///
21+
/// ```rust
22+
/// use geo::prelude::*;
23+
/// use geo::point;
24+
///
25+
/// // New York City
26+
/// let p1 = point!(x: -74.006f64, y: 40.7128f64);
27+
///
28+
/// // London
29+
/// let p2 = point!(x: -0.1278f64, y: 51.5074f64);
30+
///
31+
/// let distance = p1.rhumb_distance(&p2);
32+
///
33+
/// assert_eq!(
34+
/// 5_794_129., // meters
35+
/// distance.round()
36+
/// );
37+
/// ```
38+
///
39+
/// [rhumb line]: https://en.wikipedia.org/wiki/Rhumb_line
40+
fn rhumb_distance(&self, rhs: &Rhs) -> T;
41+
}
42+
43+
impl<T> RhumbDistance<T, Point<T>> for Point<T>
44+
where
45+
T: CoordFloat + FromPrimitive,
46+
{
47+
fn rhumb_distance(&self, rhs: &Point<T>) -> T {
48+
let calculations = RhumbCalculations::new(self, rhs);
49+
calculations.delta() * T::from(MEAN_EARTH_RADIUS).unwrap()
50+
}
51+
}
52+
53+
#[cfg(test)]
54+
mod test {
55+
use crate::Point;
56+
use crate::RhumbDistance;
57+
58+
#[test]
59+
fn distance1_test() {
60+
let a = Point::new(0., 0.);
61+
let b = Point::new(1., 0.);
62+
assert_relative_eq!(
63+
a.rhumb_distance(&b),
64+
111195.0802335329_f64,
65+
epsilon = 1.0e-6
66+
);
67+
}
68+
69+
#[test]
70+
fn distance2_test() {
71+
let a = Point::new(-72.1235, 42.3521);
72+
let b = Point::new(72.1260, 70.612);
73+
assert_relative_eq!(
74+
a.rhumb_distance(&b),
75+
8903668.508603323_f64,
76+
epsilon = 1.0e-6
77+
);
78+
}
79+
80+
#[test]
81+
fn distance3_test() {
82+
// this input comes from issue #100
83+
let a = Point::new(-77.036585, 38.897448);
84+
let b = Point::new(-77.009080, 38.889825);
85+
assert_relative_eq!(
86+
a.rhumb_distance(&b),
87+
2526.7031699343006_f64,
88+
epsilon = 1.0e-6
89+
);
90+
}
91+
92+
#[test]
93+
fn distance3_test_f32() {
94+
// this input comes from issue #100
95+
let a = Point::<f32>::new(-77.03658, 38.89745);
96+
let b = Point::<f32>::new(-77.00908, 38.889825);
97+
assert_relative_eq!(a.rhumb_distance(&b), 2526.7273_f32, epsilon = 1.0e-6);
98+
}
99+
}

0 commit comments

Comments
 (0)