use float_cmp::approx_eq;
use crate::{
Projection, ProjectionError,
ellipsoids::Ellipsoid,
errors::{ensure_finite, ensure_within_range, unpack_required_parameter},
};
#[cfg(feature = "tracing")]
use tracing::instrument;
#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
pub struct ModifiedAzimuthalEquidistant {
lon_0: f64,
lat_0: f64,
n_1: f64,
g: f64,
ellps: Ellipsoid,
}
impl ModifiedAzimuthalEquidistant {
#[must_use]
pub fn builder() -> ModifiedAzimuthalEquidistantBuilder {
ModifiedAzimuthalEquidistantBuilder::default()
}
}
#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
pub struct ModifiedAzimuthalEquidistantBuilder {
ref_lon: Option<f64>,
ref_lat: Option<f64>,
ellipsoid: Ellipsoid,
}
impl Default for ModifiedAzimuthalEquidistantBuilder {
fn default() -> Self {
Self {
ref_lon: None,
ref_lat: None,
ellipsoid: Ellipsoid::WGS84,
}
}
}
impl ModifiedAzimuthalEquidistantBuilder {
pub const fn ref_lonlat(&mut self, lon: f64, lat: f64) -> &mut Self {
self.ref_lon = Some(lon);
self.ref_lat = Some(lat);
self
}
pub const fn ellipsoid(&mut self, ellps: Ellipsoid) -> &mut Self {
self.ellipsoid = ellps;
self
}
pub fn initialize_projection(&self) -> Result<ModifiedAzimuthalEquidistant, ProjectionError> {
let ref_lon = unpack_required_parameter!(self, ref_lon);
let ref_lat = unpack_required_parameter!(self, ref_lat);
let ellps = self.ellipsoid;
ensure_finite!(ref_lon, ref_lat);
ensure_within_range!(ref_lon, -180.0..180.0);
ensure_within_range!(ref_lat, -90.0..90.0);
let lon_0 = ref_lon.to_radians();
let lat_0 = ref_lat.to_radians();
let n_1 = ellps.A / ellps.E.powi(2).mul_add(-(lat_0.sin()).powi(2), 1.0).sqrt();
let g = ellps.E * lat_0.sin() / ellps.E.mul_add(-ellps.E, 1.0).sqrt();
Ok(ModifiedAzimuthalEquidistant {
lon_0,
lat_0,
n_1,
g,
ellps,
})
}
}
impl Projection for ModifiedAzimuthalEquidistant {
#[inline]
#[allow(clippy::many_single_char_names)]
#[cfg_attr(feature = "tracing", instrument(level = "trace"))]
fn project_unchecked(&self, lon: f64, lat: f64) -> (f64, f64) {
let lon = lon.to_radians();
let lat = lat.to_radians();
let n = self.ellps.A / self.ellps.E.powi(2).mul_add(-(lat.sin()).powi(2), 1.0).sqrt();
let psi = self.ellps.E.mul_add(-self.ellps.E, 1.0).mul_add(lat.tan(), (self.ellps.E.powi(2) * self.n_1 * self.lat_0.sin()) / (n * lat.cos()))
.atan();
let az = ((lon - self.lon_0).sin())
.atan2(self.lat_0.cos().mul_add(psi.tan(), -(self.lat_0.sin() * (lon - self.lon_0).cos())));
let s = if approx_eq!(f64, az.sin(), 0.0) {
self.lat_0.cos().mul_add(psi.sin(), -(self.lat_0.sin() * psi.cos()))
.asin()
.abs()
* az.cos().signum()
} else {
(((lon - self.lon_0).sin() * psi.cos()) / (az.sin())).asin()
};
let h = self.ellps.E * self.lat_0.cos() * az.cos() / self.ellps.E.mul_add(-self.ellps.E, 1.0).sqrt();
let c = (self.n_1 * s)
* ((s.powi(5) / 48.0) * self.g).mul_add(-h, (s.powi(4) / 120.0).mul_add(h.powi(2).mul_add(7.0f64.mul_add(-h.powi(2), 4.0), -(3.0 * self.g.powi(2) * 7.0f64.mul_add(-h.powi(2), 1.0))), ((s.powi(3) / 8.0) * self.g * h).mul_add(2.0f64.mul_add(-h.powi(2), 1.0), 1.0 - (s.powi(2) * h.powi(2) * h.mul_add(-h, 1.0) / 6.0))));
let x = c * az.sin();
let y = c * az.cos();
(x, y)
}
#[inline]
#[cfg_attr(feature = "tracing", instrument(level = "trace"))]
fn inverse_project_unchecked(&self, x: f64, y: f64) -> (f64, f64) {
let c = x.hypot(y);
let az = x.atan2(y);
let big_a =
-self.ellps.E * self.ellps.E * ((self.lat_0.cos()).powi(2)) * ((az.cos()).powi(2))
/ self.ellps.E.mul_add(-self.ellps.E, 1.0);
let big_b = 3.0
* self.ellps.E
* self.ellps.E
* (1.0 - big_a)
* self.lat_0.sin()
* self.lat_0.cos()
* az.cos()
/ self.ellps.E.mul_add(-self.ellps.E, 1.0);
let big_d = c / self.n_1;
let big_e = big_d
- (big_a * (1.0 + big_a) * big_d.powi(3) / 6.0)
- (big_b * 3.0f64.mul_add(big_a, 1.0) * big_d.powi(4) / 24.0);
let big_f = 1.0 - (big_a * big_e * big_e / 2.0) - (big_b * big_e.powi(3) / 6.0);
let psi =
self.lat_0.sin().mul_add(big_e.cos(), self.lat_0.cos() * big_e.sin() * az.cos()).asin();
let lon = self.lon_0 + (az.sin() * big_e.sin() / psi.cos()).asin();
let lat = ((1.0 - (self.ellps.E * self.ellps.E * big_f * self.lat_0.sin() / psi.sin()))
* psi.tan()
/ self.ellps.E.mul_add(-self.ellps.E, 1.0))
.atan();
let lon = lon.to_degrees();
let lat = lat.to_degrees();
(lon, lat)
}
}