use super::super::{Bearing, Destination, Distance, InterpolatePoint};
use crate::Point;
use geographiclib_rs::{DirectGeodesic, InverseGeodesic};
use std::sync::LazyLock;
pub struct GeodesicMeasure<F = fn() -> geographiclib_rs::Geodesic> {
geoid: LazyLock<geographiclib_rs::Geodesic, F>,
}
#[allow(non_upper_case_globals)]
pub static Geodesic: GeodesicMeasure = GeodesicMeasure::wgs84();
impl GeodesicMeasure {
pub const fn wgs84() -> Self {
Self {
geoid: LazyLock::new(geographiclib_rs::Geodesic::wgs84),
}
}
}
impl GeodesicMeasure<Box<dyn FnOnce() -> geographiclib_rs::Geodesic>> {
pub fn new(equatorial_radius: f64, inverse_flattening: f64) -> Self {
Self {
geoid: LazyLock::new(Box::new(move || {
geographiclib_rs::Geodesic::new(equatorial_radius, inverse_flattening)
})),
}
}
}
impl<F> Bearing<f64> for GeodesicMeasure<F>
where
F: FnOnce() -> geographiclib_rs::Geodesic,
{
fn bearing(&self, origin: Point<f64>, destination: Point<f64>) -> f64 {
let (azi1, _, _) =
self.geoid
.inverse(origin.y(), origin.x(), destination.y(), destination.x());
(azi1 + 360.0) % 360.0
}
}
impl<F> Destination<f64> for GeodesicMeasure<F>
where
F: FnOnce() -> geographiclib_rs::Geodesic,
{
fn destination(&self, origin: Point<f64>, bearing: f64, distance: f64) -> Point<f64> {
let (lat, lon) = self.geoid.direct(origin.y(), origin.x(), bearing, distance);
Point::new(lon, lat)
}
}
impl<F> Distance<f64, Point<f64>, Point<f64>> for GeodesicMeasure<F>
where
F: FnOnce() -> geographiclib_rs::Geodesic,
{
fn distance(&self, origin: Point<f64>, destination: Point<f64>) -> f64 {
self.geoid
.inverse(origin.y(), origin.x(), destination.y(), destination.x())
}
}
impl<F> InterpolatePoint<f64> for GeodesicMeasure<F>
where
F: FnOnce() -> geographiclib_rs::Geodesic,
{
fn point_at_distance_between(
&self,
start: Point<f64>,
end: Point<f64>,
meters_from_start: f64,
) -> Point<f64> {
if meters_from_start == 0.0 {
return start;
}
let bearing = self.bearing(start, end);
self.destination(start, bearing, meters_from_start)
}
fn point_at_ratio_between(
&self,
start: Point<f64>,
end: Point<f64>,
ratio_from_start: f64,
) -> Point<f64> {
if start == end || ratio_from_start == 0.0 {
return start;
}
if ratio_from_start == 1.0 {
return end;
}
let (total_distance, azi1, _azi2, _a12) =
self.geoid.inverse(start.y(), start.x(), end.y(), end.x());
let distance = total_distance * ratio_from_start;
self.destination(start, azi1, distance)
}
fn points_along_line(
&self,
start: Point<f64>,
end: Point<f64>,
max_distance: f64,
include_ends: bool,
) -> impl Iterator<Item = Point<f64>> {
let (total_distance, azi1, _azi2, _a12) =
self.geoid.inverse(start.y(), start.x(), end.y(), end.x());
if total_distance <= max_distance {
return if include_ends {
vec![start, end].into_iter()
} else {
vec![].into_iter()
};
}
let number_of_points = (total_distance / max_distance).ceil();
let interval = 1.0 / number_of_points;
let mut current_step = interval;
let mut points = if include_ends { vec![start] } else { vec![] };
while current_step < 1.0 {
let (lat2, lon2) =
self.geoid
.direct(start.y(), start.x(), azi1, total_distance * current_step);
let point = Point::new(lon2, lat2);
points.push(point);
current_step += interval;
}
if include_ends {
points.push(end);
}
points.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::point;
mod bearing {
use super::*;
#[test]
fn north() {
let origin = Point::new(0.0, 0.0);
let destination = Point::new(0.0, 1.0);
assert_relative_eq!(0.0, Geodesic.bearing(origin, destination));
}
#[test]
fn east() {
let origin = Point::new(0.0, 0.0);
let destination = Point::new(1.0, 0.0);
assert_relative_eq!(90.0, Geodesic.bearing(origin, destination));
}
#[test]
fn south() {
let origin = Point::new(0.0, 0.0);
let destination = Point::new(0.0, -1.0);
assert_relative_eq!(180.0, Geodesic.bearing(origin, destination));
}
#[test]
fn west() {
let origin = Point::new(0.0, 0.0);
let destination = Point::new(-1.0, 0.0);
assert_relative_eq!(270.0, Geodesic.bearing(origin, destination));
}
}
mod destination {
use super::*;
#[test]
fn north() {
let origin = Point::new(0.0, 0.0);
let bearing = 0.0;
assert_relative_eq!(
Point::new(0.0, 0.9043687229127633),
Geodesic.destination(origin, bearing, 100_000.0)
);
}
#[test]
fn east() {
let origin = Point::new(0.0, 0.0);
let bearing = 90.0;
assert_relative_eq!(
Point::new(0.8983152841195217, 0.0),
Geodesic.destination(origin, bearing, 100_000.0)
);
}
#[test]
fn south() {
let origin = Point::new(0.0, 0.0);
let bearing = 180.0;
assert_relative_eq!(
Point::new(0.0, -0.9043687229127633),
Geodesic.destination(origin, bearing, 100_000.0)
);
}
#[test]
fn west() {
let origin = Point::new(0.0, 0.0);
let bearing = 270.0;
assert_relative_eq!(
Point::new(-0.8983152841195217, 0.0),
Geodesic.destination(origin, bearing, 100_000.0)
);
}
mod distance {
use super::*;
#[test]
fn new_york_to_london() {
let new_york_city = Point::new(-74.006f64, 40.7128f64);
let london = Point::new(-0.1278f64, 51.5074f64);
let distance = Geodesic.distance(new_york_city, london);
assert_relative_eq!(
5_585_234.0, distance.round()
);
}
}
mod interpolate_point {
use super::*;
#[test]
fn point_at_ratio_between_midpoint() {
let start = Point::new(10.0, 20.0);
let end = Point::new(125.0, 25.0);
let midpoint = Geodesic.point_at_ratio_between(start, end, 0.5);
assert_relative_eq!(midpoint, Point::new(65.87936072133309, 37.72225378005785));
}
#[test]
fn points_along_line_with_endpoints() {
let start = Point::new(10.0, 20.0);
let end = Point::new(125.0, 25.0);
let max_dist = 1000000.0; let route = Geodesic
.points_along_line(start, end, max_dist, true)
.collect::<Vec<_>>();
assert_eq!(route.len(), 13);
assert_eq!(route[0], start);
assert_eq!(route.last().unwrap(), &end);
assert_relative_eq!(route[1], Point::new(17.878754355562464, 24.466667836189565));
}
#[test]
fn points_along_line_without_endpoints() {
let start = Point::new(10.0, 20.0);
let end = Point::new(125.0, 25.0);
let max_dist = 1000000.0; let route = Geodesic
.points_along_line(start, end, max_dist, false)
.collect::<Vec<_>>();
assert_eq!(route.len(), 11);
assert_relative_eq!(route[0], Point::new(17.878754355562464, 24.466667836189565));
}
}
}
#[test]
fn test_non_standard_geoid() {
let start = point!(x: 23.319941, y: 42.698334); let finish = point!(x: 24.742168, y: 42.136097);
assert_relative_eq!(132675.5018588206, Geodesic.distance(start, finish));
let mars_equatorial_radius = 3_396_200.;
let mars_flattening = 0.00589;
let mars_geoid = GeodesicMeasure::new(mars_equatorial_radius, mars_flattening);
assert_relative_eq!(70684.36315529353, mars_geoid.distance(start, finish));
}
}