use geometry_cs::Spheroid;
use super::direct::DirectResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MeridianSegmentKind {
NonMeridian,
NotCrossingPole,
CrossingPole,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MeridianInverseResult {
pub distance: f64,
pub meridian: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Meridian {
pub spheroid: Spheroid,
}
impl Meridian {
pub const WGS84: Self = Self {
spheroid: Spheroid::WGS84,
};
#[must_use]
pub fn quarter_length(&self) -> f64 {
const COEFFICIENTS: [f64; 9] = [
1_073_741_824.0,
268_435_456.0,
16_777_216.0,
4_194_304.0,
1_638_400.0,
802_816.0,
451_584.0,
278_784.0,
184_041.0,
];
let f = self.spheroid.flattening;
let n = f / (2.0 - f);
let ab4 = (self.spheroid.equatorial_radius + self.spheroid.polar_radius()) / 4.0;
let series = COEFFICIENTS[..8]
.iter()
.rev()
.fold(0.0, |value, coefficient| value * n * n + coefficient);
core::f64::consts::PI * ab4 * series / COEFFICIENTS[0]
}
#[cfg(feature = "std")]
#[must_use]
pub fn arc_length(&self, latitude: f64) -> f64 {
let f = self.spheroid.flattening;
let n = f / (2.0 - f);
let n2 = n * n;
let n3 = n2 * n;
let n4 = n2 * n2;
let n5 = n4 * n;
let scale = self.spheroid.equatorial_radius / (1.0 + n);
let c0 = 1.0 + 0.25 * n2;
let c2 = -1.5 * n + 0.1875 * n3;
let c4 = 0.9375 * n2 - 0.234_375 * n4;
let c6 = -0.729_166_667 * n3 + 0.227_864_583 * n5;
let c8 = 0.615_234_375 * n4;
let c10 = -0.541_406_25 * n5;
scale
* (c0 * latitude
+ c2 * (2.0 * latitude).sin()
+ c4 * (4.0 * latitude).sin()
+ c6 * (6.0 * latitude).sin()
+ c8 * (8.0 * latitude).sin()
+ c10 * (10.0 * latitude).sin())
}
#[cfg(feature = "std")]
#[must_use]
pub fn latitude_at_arc(&self, distance: f64) -> f64 {
let f = self.spheroid.flattening;
let n = f / (2.0 - f);
let n2 = n * n;
let n3 = n2 * n;
let n4 = n2 * n2;
let mu = core::f64::consts::FRAC_PI_2 * distance / self.quarter_length();
let h2 = 1.5 * n - 0.843_75 * n3;
let h4 = 1.3125 * n2 - 1.718_75 * n4;
let h6 = 1.572_916_667 * n3;
let h8 = 2.142_578_125 * n4;
mu + h2 * (2.0 * mu).sin()
+ h4 * (4.0 * mu).sin()
+ h6 * (6.0 * mu).sin()
+ h8 * (8.0 * mu).sin()
}
#[cfg(feature = "std")]
#[must_use]
#[allow(
clippy::unused_self,
reason = "the instance method keeps the formula strategy API uniform with the spheroid-dependent methods"
)]
pub fn classify_segment(
&self,
longitude1: f64,
latitude1: f64,
longitude2: f64,
latitude2: f64,
) -> MeridianSegmentKind {
let difference = normalize_longitude(longitude2 - longitude1);
if nearly_zero(difference)
|| (nearly_equal(latitude2, core::f64::consts::FRAC_PI_2)
&& nearly_equal(latitude1, -core::f64::consts::FRAC_PI_2))
|| (nearly_equal(latitude1, core::f64::consts::FRAC_PI_2)
&& nearly_equal(latitude2, -core::f64::consts::FRAC_PI_2))
{
MeridianSegmentKind::NotCrossingPole
} else if nearly_equal(difference.abs(), core::f64::consts::PI) {
MeridianSegmentKind::CrossingPole
} else {
MeridianSegmentKind::NonMeridian
}
}
#[cfg(feature = "std")]
#[must_use]
pub fn inverse(
&self,
longitude1: f64,
mut latitude1: f64,
longitude2: f64,
mut latitude2: f64,
) -> MeridianInverseResult {
let kind = self.classify_segment(longitude1, latitude1, longitude2, latitude2);
if latitude1 > latitude2 {
core::mem::swap(&mut latitude1, &mut latitude2);
}
let distance = match kind {
MeridianSegmentKind::NonMeridian => 0.0,
MeridianSegmentKind::NotCrossingPole => {
(self.arc_length(latitude2) - self.arc_length(latitude1)).abs()
}
MeridianSegmentKind::CrossingPole => {
let latitude_sign = if latitude1 + latitude2 < 0.0 {
-1.0
} else {
1.0
};
(latitude_sign * 2.0 * self.arc_length(core::f64::consts::FRAC_PI_2)
- self.arc_length(latitude1)
- self.arc_length(latitude2))
.abs()
}
};
MeridianInverseResult {
distance,
meridian: kind != MeridianSegmentKind::NonMeridian,
}
}
#[cfg(feature = "std")]
#[must_use]
pub fn direct(
&self,
longitude1: f64,
latitude1: f64,
distance: f64,
north: bool,
) -> DirectResult {
let initial_arc = self.arc_length(latitude1);
let signed_distance = if north { distance } else { -distance };
let raw_latitude = self.latitude_at_arc(initial_arc + signed_distance);
let (lon2, lat2, reflected) = normalize_coordinates(longitude1, raw_latitude);
let final_north = north ^ reflected;
DirectResult::solved(
longitude1,
latitude1,
if north { 0.0 } else { core::f64::consts::PI },
self.spheroid,
lon2,
lat2,
if final_north {
0.0
} else {
core::f64::consts::PI
},
)
}
}
impl Default for Meridian {
fn default() -> Self {
Self::WGS84
}
}
#[cfg(feature = "std")]
fn normalize_coordinates(longitude: f64, latitude: f64) -> (f64, f64, bool) {
let pi = core::f64::consts::PI;
let mut lat = (latitude + pi).rem_euclid(core::f64::consts::TAU) - pi;
let mut lon = longitude;
let reflected = if lat > core::f64::consts::FRAC_PI_2 {
lat = pi - lat;
lon += pi;
true
} else if lat < -core::f64::consts::FRAC_PI_2 {
lat = -pi - lat;
lon += pi;
true
} else {
false
};
(normalize_longitude(lon), lat, reflected)
}
#[cfg(feature = "std")]
fn normalize_longitude(longitude: f64) -> f64 {
let pi = core::f64::consts::PI;
(longitude + pi).rem_euclid(core::f64::consts::TAU) - pi
}
#[cfg(feature = "std")]
fn nearly_zero(value: f64) -> bool {
value.abs() <= 1e-12
}
#[cfg(feature = "std")]
fn nearly_equal(first: f64, second: f64) -> bool {
(first - second).abs() <= 1e-12
}