Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GeodesicBearing and HaversineBearing return result #1161

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions geo-types/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* <https://github.com/georust/geo/pull/1134>
* Add feature for rstar v0.12.0
* Add num_rings and num_interior_rings methods to polygons.
* Point has a new public method `check_coordinate_limits()` that will check if latitude and longitude are within
their ranges, returns `Result<T,String>` - used for bearing calculations.

## 0.7.12

Expand Down
73 changes: 73 additions & 0 deletions geo-types/src/geometry/point.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,47 @@ impl<T: CoordNum> Point<T> {
pub fn set_lat(&mut self, lat: T) -> &mut Self {
self.set_y(lat)
}

fn check_x_in_limits(&self) -> Result<(), String> {
let x_in_64 = self.x().to_f64().unwrap();
if x_in_64 < -180.0 || x_in_64 > 180.0 {
return Err("x is out of bounds: [ -180, 180 ]".into());
}
Ok(())
}

fn check_y_in_limits(&self) -> Result<(), String> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Points can be projected - they might not be lat/lon.

let y_in_64 = self.y().to_f64().unwrap();
if y_in_64 < -90.0 || y_in_64 > 90.0 {
return Err("y is out of bounds: [ -90, 90 ]".into());
}
Ok(())
}

/// Check whether latitude and longitude of a point are within the allowed values of
/// - lat [ -90,90 ]
/// - lon [-180, 180]
///
///
/// returns: Result<(), OutOfBoundsError>
///
/// # Examples
///
/// ```
/// use crate::geo_types::Point;
///
/// let p0 = Point::new(0_f64, 45_f64);
/// p0.check_coordinate_limits().unwrap();
/// ```
pub fn check_coordinate_limits(&self) -> Result<(), String> {
self.check_y_in_limits()?;
self.check_x_in_limits()?;
Ok(())
}
}



impl<T: CoordNum> Point<T> {
/// Returns the dot product of the two points:
/// `dot = x1 * x2 + y1 * y2`
Expand Down Expand Up @@ -779,4 +818,38 @@ mod test {
let p_inf = Point::new(f64::INFINITY, 1.);
assert!(p.relative_ne(&p_inf, 1e-2, 1e-2));
}

#[test]
fn test_x_out_of_bounds() {
let p0 = point!(x:180.1, y:0.5);
let err_x = p0.check_x_in_limits().unwrap_err();
let err_both = p0.check_coordinate_limits().unwrap_err();

assert_eq!(err_x, "x is out of bounds: [ -180, 180 ]".to_string());
assert_eq!(err_both, "x is out of bounds: [ -180, 180 ]".to_string());

let p0 = point!(x:-180.1, y:0.5);
let err_x = p0.check_x_in_limits().unwrap_err();
let err_both = p0.check_coordinate_limits().unwrap_err();

assert_eq!(err_x, "x is out of bounds: [ -180, 180 ]".to_string());
assert_eq!(err_both, "x is out of bounds: [ -180, 180 ]".to_string());
}

#[test]
fn test_y_out_of_bounds() {
let p0 = point!(x:40, y:95);
let err_y = p0.check_y_in_limits().unwrap_err();
let err_both = p0.check_coordinate_limits().unwrap_err();

assert_eq!(err_y, "y is out of bounds: [ -90, 90 ]".to_string());
assert_eq!(err_both, "y is out of bounds: [ -90, 90 ]".to_string());

let p0 = point!(x:0.1, y:-113.0);
let err_y = p0.check_y_in_limits().unwrap_err();
let err_both = p0.check_coordinate_limits().unwrap_err();

assert_eq!(err_y, "y is out of bounds: [ -90, 90 ]".to_string());
assert_eq!(err_both, "y is out of bounds: [ -90, 90 ]".to_string());
}
}
2 changes: 2 additions & 0 deletions geo/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

## 0.28.0

* BREAKING: HaversineBearing (also Bearing) and GeodesicBearing now return `Result<T,String>`
meaning that there is a need to use unwrap.
* BREAKING: The `HasKernel` trait was removed and it's functionality was merged
into `GeoNum`. If you are using common scalars for your geometry (f32, f64,
i64, i32, i16, isize), this should have no effect on you. If you are using an
Expand Down
8 changes: 4 additions & 4 deletions geo/src/algorithm/bearing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ pub trait Bearing<T: CoordFloat> {
///
/// let p_1 = Point::new(9.177789688110352, 48.776781529534965);
/// let p_2 = Point::new(9.274410083250379, 48.84033282787534);
/// let bearing = p_1.bearing(p_2);
/// let bearing = p_1.bearing(p_2).unwrap();
/// assert_relative_eq!(bearing, 45., epsilon = 1.0e-6);
/// ```
#[deprecated(
since = "0.24.1",
note = "renamed to `HaversineBearing::haversine_bearing`"
)]
fn bearing(&self, point: Point<T>) -> T;
fn bearing(&self, point: Point<T>) -> Result<T, String>;
}

#[allow(deprecated)]
impl<T: CoordFloat, B: HaversineBearing<T>> Bearing<T> for B {
fn bearing(&self, point: Point<T>) -> T {
self.haversine_bearing(point)
fn bearing(&self, point: Point<T>) -> Result<T, String> {
Ok(self.haversine_bearing(point)?)
}
}
4 changes: 2 additions & 2 deletions geo/src/algorithm/cross_track_distance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ where
fn cross_track_distance(&self, line_point_a: &Point<T>, line_point_b: &Point<T>) -> T {
let mean_earth_radius = T::from(MEAN_EARTH_RADIUS).unwrap();
let l_delta_13: T = line_point_a.haversine_distance(self) / mean_earth_radius;
let theta_13: T = line_point_a.haversine_bearing(*self).to_radians();
let theta_12: T = line_point_a.haversine_bearing(*line_point_b).to_radians();
let theta_13: T = line_point_a.haversine_bearing(*self).unwrap().to_radians();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would change the signature of this function so that it also returns Result

let theta_12: T = line_point_a.haversine_bearing(*line_point_b).unwrap().to_radians();
let l_delta_xt: T = (l_delta_13.sin() * (theta_12 - theta_13).sin()).asin();
mean_earth_radius * l_delta_xt.abs()
}
Expand Down
33 changes: 26 additions & 7 deletions geo/src/algorithm/geodesic_bearing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ pub trait GeodesicBearing<T: CoordNum> {
///
/// let p_1 = Point::new(9.177789688110352, 48.776781529534965);
/// let p_2 = Point::new(9.27411867078536, 48.8403266058781);
/// let bearing = p_1.geodesic_bearing(p_2);
/// let bearing = p_1.geodesic_bearing(p_2).unwrap();
/// assert_relative_eq!(bearing, 45., epsilon = 1.0e-6);
/// ```
fn geodesic_bearing(&self, point: Point<T>) -> T;
fn geodesic_bearing(&self, point: Point<T>) -> Result<T, String>;

/// Returns the bearing and distance to another Point in a (bearing, distance) tuple.
///
Expand All @@ -48,9 +48,11 @@ pub trait GeodesicBearing<T: CoordNum> {
}

impl GeodesicBearing<f64> for Point<f64> {
fn geodesic_bearing(&self, rhs: Point<f64>) -> f64 {
fn geodesic_bearing(&self, rhs: Point<f64>) -> Result<f64, String> {
self.check_coordinate_limits()?;
rhs.check_coordinate_limits()?;
let (azi1, _, _) = Geodesic::wgs84().inverse(self.y(), self.x(), rhs.y(), rhs.x());
azi1
Ok(azi1)
}

fn geodesic_bearing_distance(&self, rhs: Point<f64>) -> (f64, f64) {
Expand All @@ -60,6 +62,7 @@ impl GeodesicBearing<f64> for Point<f64> {
}
}


#[cfg(test)]
mod test {
use super::*;
Expand All @@ -69,26 +72,42 @@ mod test {
fn north_bearing() {
let p_1 = point!(x: 9., y: 47.);
let p_2 = point!(x: 9., y: 48.);
let bearing = p_1.geodesic_bearing(p_2);
let bearing = p_1.geodesic_bearing(p_2).unwrap();
assert_relative_eq!(bearing, 0.);
}

#[test]
fn east_bearing() {
let p_1 = point!(x: 9., y: 10.);
let p_2 = point!(x: 18.118501133357412, y: 9.875322179340463);
let bearing = p_1.geodesic_bearing(p_2);
let bearing = p_1.geodesic_bearing(p_2).unwrap();
assert_relative_eq!(bearing, 90.);
}

#[test]
fn northeast_bearing() {
let p_1 = point!(x: 9.177789688110352f64, y: 48.776781529534965);
let p_2 = point!(x: 9.27411867078536, y: 48.8403266058781);
let bearing = p_1.geodesic_bearing(p_2);
let bearing = p_1.geodesic_bearing(p_2).unwrap();
assert_relative_eq!(bearing, 45., epsilon = 1.0e-11);
}

#[test]
fn should_return_error_on_out_x_of_bounds() {
let p_1 = point!(x: -180.177789688110352f64, y: 48.776781529534965);
let p_2 = point!(x: 9.27411867078536, y: 9.8403266058781);
let err = p_1.geodesic_bearing(p_2).unwrap_err();
assert_eq!(err, "x is out of bounds: [ -180, 180 ]".to_string());
}

#[test]
fn should_return_error_on_out_y_of_bounds() {
let p_1 = point!(x: 9.177789688110352f64, y: 48.776781529534965);
let p_2 = point!(x: 9.27411867078536, y: 98.8403266058781);
let err = p_1.geodesic_bearing(p_2).unwrap_err();
assert_eq!(err, "y is out of bounds: [ -90, 90 ]".to_string());
}

#[test]
fn consistent_with_destination() {
use crate::algorithm::GeodesicDestination;
Expand Down
38 changes: 29 additions & 9 deletions geo/src/algorithm/haversine_bearing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,26 @@ pub trait HaversineBearing<T: CoordFloat> {
///
/// let p_1 = Point::new(9.177789688110352, 48.776781529534965);
/// let p_2 = Point::new(9.274410083250379, 48.84033282787534);
/// let bearing = p_1.haversine_bearing(p_2);
/// let bearing = p_1.haversine_bearing(p_2).unwrap();
/// assert_relative_eq!(bearing, 45., epsilon = 1.0e-6);
/// ```
fn haversine_bearing(&self, point: Point<T>) -> T;
fn haversine_bearing(&self, point: Point<T>) -> Result<T, String>;
}

impl<T> HaversineBearing<T> for Point<T>
where
T: CoordFloat,
{
fn haversine_bearing(&self, point: Point<T>) -> T {
fn haversine_bearing(&self, point: Point<T>) -> Result<T, String> {
self.check_coordinate_limits()?;
point.check_coordinate_limits()?;
let (lng_a, lat_a) = (self.x().to_radians(), self.y().to_radians());
let (lng_b, lat_b) = (point.x().to_radians(), point.y().to_radians());
let delta_lng = lng_b - lng_a;
let s = lat_b.cos() * delta_lng.sin();
let c = lat_a.cos() * lat_b.sin() - lat_a.sin() * lat_b.cos() * delta_lng.cos();

T::atan2(s, c).to_degrees()
Ok(T::atan2(s, c).to_degrees())
}
}

Expand All @@ -48,15 +50,15 @@ mod test {
fn north_bearing() {
let p_1 = point!(x: 9., y: 47.);
let p_2 = point!(x: 9., y: 48.);
let bearing = p_1.haversine_bearing(p_2);
let bearing = p_1.haversine_bearing(p_2).unwrap();
assert_relative_eq!(bearing, 0.);
}

#[test]
fn equatorial_east_bearing() {
let p_1 = point!(x: 9., y: 0.);
let p_2 = point!(x: 10., y: 0.);
let bearing = p_1.haversine_bearing(p_2);
let bearing = p_1.haversine_bearing(p_2).unwrap();
assert_relative_eq!(bearing, 90.);
}

Expand All @@ -65,15 +67,15 @@ mod test {
let p_1 = point!(x: 9., y: 10.);
let p_2 = point!(x: 18.12961917258341, y: 9.875828894123304);

let bearing = p_1.haversine_bearing(p_2);
let bearing = p_1.haversine_bearing(p_2).unwrap();
assert_relative_eq!(bearing, 90.);
}

#[test]
fn northeast_bearing() {
let p_1 = point!(x: 9.177789688110352f64, y: 48.776781529534965);
let p_2 = point!(x: 9.274409949623548, y: 48.84033274015048);
let bearing = p_1.haversine_bearing(p_2);
let bearing = p_1.haversine_bearing(p_2).unwrap();
assert_relative_eq!(bearing, 45., epsilon = 1.0e-6);
}

Expand All @@ -82,7 +84,25 @@ mod test {
let p_1 = point!(x: 9.177789688110352f64, y: 48.776781529534965);
let p_2 = p_1.haversine_destination(45., 10000.);

let b_1 = p_1.haversine_bearing(p_2);
let b_1 = p_1.haversine_bearing(p_2).unwrap();
assert_relative_eq!(b_1, 45., epsilon = 1.0e-6);
}

#[test]
fn returns_an_error_on_y_out_of_bounds() {
let p_1 = point!(x: 9.177789688110352f64, y: 48.776781529534965);
let p_2 = point!(x: 123.0, y: 91.1);

let err = p_1.haversine_bearing(p_2).unwrap_err();
assert_eq!(err, "y is out of bounds: [ -90, 90 ]".to_string())
}

#[test]
fn returns_an_error_on_x_out_of_bounds() {
let p_1 = point!(x: 9.177789688110352f64, y: 48.776781529534965);
let p_2 = point!(x: 183.0, y: 90.0);

let err = p_1.haversine_bearing(p_2).unwrap_err();
assert_eq!(err, "x is out of bounds: [ -180, 180 ]".to_string())
}
}
6 changes: 3 additions & 3 deletions geo/src/algorithm/haversine_closest_point.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,14 @@ where
}

let pi = T::from(std::f64::consts::PI).unwrap();
let crs_ad = p1.haversine_bearing(*from).to_radians();
let crs_ab = p1.haversine_bearing(p2).to_radians();
let crs_ad = p1.haversine_bearing(*from).unwrap().to_radians();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would change signature to return result

let crs_ab = p1.haversine_bearing(p2).unwrap().to_radians();
let crs_ba = if crs_ab > T::zero() {
crs_ab - pi
} else {
crs_ab + pi
};
let crs_bd = p2.haversine_bearing(*from).to_radians();
let crs_bd = p2.haversine_bearing(*from).unwrap().to_radians();
let d_crs1 = crs_ad - crs_ab;
let d_crs2 = crs_bd - crs_ba;

Expand Down
2 changes: 1 addition & 1 deletion geo/src/algorithm/haversine_destination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ mod test {
let pt2 = Point::new(-170.0, -30.0);

for (start, end) in [(pt1, pt2), (pt2, pt1)] {
let bearing = start.haversine_bearing(end);
let bearing = start.haversine_bearing(end).unwrap();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would change signature to return result

let results: Vec<_> = (0..8)
.map(|n| start.haversine_destination(bearing, n as f64 * 250_000.))
.collect();
Expand Down