use core::f64::consts::PI;
use libm::{atan2, cos, sin, sqrt};
const EARTH_RADIUS_M: f64 = 6_371_008.8;
fn to_radians(degrees: f64) -> f64 {
degrees * (PI / 180.0)
}
fn to_degrees(radians: f64) -> f64 {
radians * (180.0 / PI)
}
fn magnitude(value: f64) -> f64 {
if value < 0.0 {
-value
} else {
value
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Coordinate {
pub latitude: f64,
pub longitude: f64,
}
impl Coordinate {
pub fn new(latitude: f64, longitude: f64) -> Self {
Self {
latitude,
longitude,
}
}
pub fn distance_to(&self, other: Coordinate) -> f64 {
let lat1 = to_radians(self.latitude);
let lat2 = to_radians(other.latitude);
let half_dlat = to_radians(other.latitude - self.latitude) / 2.0;
let half_dlon = to_radians(other.longitude - self.longitude) / 2.0;
let sin_lat = sin(half_dlat);
let sin_lon = sin(half_dlon);
let a = sin_lat * sin_lat + cos(lat1) * cos(lat2) * sin_lon * sin_lon;
let c = 2.0 * atan2(sqrt(a), sqrt(1.0 - a));
EARTH_RADIUS_M * c
}
pub fn bearing_to(&self, other: Coordinate) -> f64 {
let lat1 = to_radians(self.latitude);
let lat2 = to_radians(other.latitude);
let dlon = to_radians(other.longitude - self.longitude);
let y = sin(dlon) * cos(lat2);
let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlon);
let bearing = to_degrees(atan2(y, x));
if bearing < 0.0 {
bearing + 360.0
} else {
bearing
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Boundary {
Inside,
Outside,
Exited,
Entered,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Geofence {
center: Coordinate,
radius_m: f64,
inside: Option<bool>,
}
impl Geofence {
pub fn new(center: Coordinate, radius_m: f64) -> Self {
Self {
center,
radius_m: magnitude(radius_m),
inside: None,
}
}
pub fn contains(&self, point: Coordinate) -> bool {
self.center.distance_to(point) <= self.radius_m
}
pub fn update(&mut self, point: Coordinate) -> Boundary {
let now_inside = self.contains(point);
let boundary = match self.inside {
Some(true) if !now_inside => Boundary::Exited,
Some(false) if now_inside => Boundary::Entered,
_ if now_inside => Boundary::Inside,
_ => Boundary::Outside,
};
self.inside = Some(now_inside);
boundary
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn distance_to_self_is_zero() {
let point = Coordinate::new(12.34, -56.78);
assert_eq!(point.distance_to(point), 0.0);
}
#[test]
fn one_degree_of_longitude_at_the_equator() {
let here = Coordinate::new(0.0, 0.0);
let east = Coordinate::new(0.0, 1.0);
let metres = here.distance_to(east);
assert!((metres - 111_195.0).abs() < 5.0);
}
#[test]
fn distance_is_symmetric() {
let a = Coordinate::new(40.7128, -74.0060);
let b = Coordinate::new(51.5074, -0.1278);
assert!((a.distance_to(b) - b.distance_to(a)).abs() < 1.0);
}
#[test]
fn an_intercontinental_distance_is_accurate() {
let nyc = Coordinate::new(40.7128, -74.0060);
let london = Coordinate::new(51.5074, -0.1278);
let km = nyc.distance_to(london) / 1000.0;
assert!((km - 5570.0).abs() < 30.0);
}
#[test]
fn antipodal_points_are_half_the_circumference() {
let here = Coordinate::new(0.0, 0.0);
let opposite = Coordinate::new(0.0, 180.0);
let km = here.distance_to(opposite) / 1000.0;
assert!((km - 20_015.0).abs() < 5.0);
}
#[test]
fn bearing_to_the_cardinal_directions() {
let here = Coordinate::new(0.0, 0.0);
assert!((here.bearing_to(Coordinate::new(1.0, 0.0)) - 0.0).abs() < 1e-6); assert!((here.bearing_to(Coordinate::new(0.0, 1.0)) - 90.0).abs() < 1e-6); let north = Coordinate::new(1.0, 0.0);
assert!((north.bearing_to(here) - 180.0).abs() < 1e-6); let east = Coordinate::new(0.0, 1.0);
assert!((east.bearing_to(here) - 270.0).abs() < 1e-6); }
#[test]
fn bearing_matches_a_worked_example() {
let baghdad = Coordinate::new(35.0, 45.0);
let osaka = Coordinate::new(35.0, 135.0);
assert!((baghdad.bearing_to(osaka) - 60.0).abs() < 1.0);
}
#[test]
fn a_fence_reports_crossings_once() {
let mut fence = Geofence::new(Coordinate::new(37.0, -122.0), 100.0);
let near = Coordinate::new(37.0005, -122.0); let far = Coordinate::new(37.002, -122.0);
assert!(fence.contains(near));
assert!(!fence.contains(far));
assert_eq!(fence.update(near), Boundary::Inside);
assert_eq!(fence.update(far), Boundary::Exited); assert_eq!(fence.update(far), Boundary::Outside); assert_eq!(fence.update(near), Boundary::Entered); assert_eq!(fence.update(near), Boundary::Inside);
}
#[test]
fn a_point_on_the_boundary_counts_as_inside() {
let center = Coordinate::new(0.0, 0.0);
let edge = Coordinate::new(0.0, 1.0);
let fence = Geofence::new(center, center.distance_to(edge));
assert!(fence.contains(edge));
}
#[test]
fn a_negative_radius_is_treated_as_its_magnitude() {
let fence = Geofence::new(Coordinate::new(0.0, 0.0), -100.0);
assert!(fence.contains(Coordinate::new(0.0, 0.0)));
}
}