use geometry_cs::{CoordinateSystem, SphericalFamily};
use geometry_tag::SameAs;
use geometry_trait::{Closure, Point, PointOrder, Polygon, Ring};
use crate::area::AreaStrategy;
#[cfg(feature = "std")]
use crate::normalise::{HasAngularUnits, lonlat_radians};
#[derive(Debug, Clone, Copy)]
pub struct SphericalArea {
pub radius: f64,
}
impl SphericalArea {
pub const EARTH: Self = Self {
radius: 6_371_000.0,
};
pub const UNIT: Self = Self { radius: 1.0 };
}
impl Default for SphericalArea {
#[inline]
fn default() -> Self {
Self::EARTH
}
}
#[derive(Debug, Clone, Copy)]
pub struct SphericalPolygonArea {
pub radius: f64,
}
impl SphericalPolygonArea {
pub const EARTH: Self = Self {
radius: 6_371_000.0,
};
pub const UNIT: Self = Self { radius: 1.0 };
}
impl Default for SphericalPolygonArea {
#[inline]
fn default() -> Self {
Self::EARTH
}
}
#[cfg(feature = "std")]
impl<R> AreaStrategy<R> for SphericalArea
where
R: Ring,
<R::Point as Point>::Cs: CoordinateSystem,
<<R::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<SphericalFamily>,
R::Point: Point<Scalar = f64>,
<R::Point as Point>::Cs: HasAngularUnits,
{
type Out = f64;
#[inline]
fn area(&self, r: &R) -> f64 {
let excess = excess_accumulator::<R>(r);
let signed = excess * self.radius * self.radius;
match r.point_order() {
PointOrder::Clockwise => signed,
PointOrder::CounterClockwise => -signed,
}
}
}
#[cfg(feature = "std")]
impl<P> AreaStrategy<P> for SphericalPolygonArea
where
P: Polygon,
SphericalArea: AreaStrategy<P::Ring, Out = f64>,
{
type Out = f64;
#[inline]
fn area(&self, p: &P) -> f64 {
let ring = SphericalArea {
radius: self.radius,
};
let mut total = ring.area(p.exterior());
for inner in p.interiors() {
total += ring.area(inner);
}
total
}
}
#[cfg(feature = "std")]
#[inline]
pub(crate) fn excess_accumulator<R>(r: &R) -> f64
where
R: Ring,
R::Point: Point<Scalar = f64>,
<R::Point as Point>::Cs: HasAngularUnits,
{
let mut acc = 0.0;
let it = r.points();
let next = it.clone().skip(1);
for (a, b) in it.zip(next) {
acc += segment_excess::<R::Point>(a, b);
}
if matches!(r.closure(), Closure::Open) {
let mut points = r.points();
if let Some(first) = points.next() {
let last = points.last().unwrap_or(first);
acc += segment_excess::<R::Point>(last, first);
}
}
acc
}
#[allow(clippy::float_cmp)]
#[cfg(feature = "std")]
#[inline]
fn segment_excess<P>(a: &P, b: &P) -> f64
where
P: Point<Scalar = f64>,
P::Cs: HasAngularUnits,
{
let (lon1, lat1) = lonlat_radians(a);
let (lon2, lat2) = lonlat_radians(b);
if lon1 == lon2 {
return 0.0;
}
let mut dlon = lon2 - lon1;
let pi = core::f64::consts::PI;
let two_pi = 2.0 * pi;
while dlon > pi {
dlon -= two_pi;
}
while dlon <= -pi {
dlon += two_pi;
}
let tan_lat1 = (lat1 / 2.0).tan();
let tan_lat2 = (lat2 / 2.0).tan();
2.0 * (((tan_lat1 + tan_lat2) / (1.0 + tan_lat1 * tan_lat2)) * (dlon / 2.0).tan()).atan()
}
#[cfg(all(test, feature = "std"))]
mod tests {
#![allow(
clippy::float_cmp,
reason = "areas are compared with an explicit relative tolerance, not `==`"
)]
use super::{SphericalArea, SphericalPolygonArea};
use crate::area::AreaStrategy;
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Spherical};
use geometry_model::{Polygon, Ring};
type Sp = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
#[inline]
fn sp(lon: f64, lat: f64) -> Sp {
WithCs::new(Adapt([lon, lat]))
}
#[test]
fn unit_sphere_octant_is_pi_over_2() {
let r: Ring<Sp> = Ring::from_vec(vec![sp(0., 0.), sp(0., 90.), sp(90., 0.), sp(0., 0.)]);
let got = SphericalArea::UNIT.area(&r);
let expected = core::f64::consts::FRAC_PI_2;
assert!(
(got - expected).abs() / expected < 1e-6,
"got {got} expected {expected}"
);
}
#[test]
fn radius_2_sphere_octant_scales_by_4() {
let r: Ring<Sp> = Ring::from_vec(vec![sp(0., 0.), sp(0., 90.), sp(90., 0.), sp(0., 0.)]);
let got = SphericalArea { radius: 2.0 }.area(&r);
let expected = 4.0 * core::f64::consts::FRAC_PI_2;
assert!(
(got - expected).abs() / expected < 1e-6,
"got {got} expected {expected}"
);
}
#[test]
fn reversed_octant_is_negative() {
let r: Ring<Sp> = Ring::from_vec(vec![sp(0., 0.), sp(90., 0.), sp(0., 90.), sp(0., 0.)]);
let got = SphericalArea::UNIT.area(&r);
let expected = -core::f64::consts::FRAC_PI_2;
assert!((got - expected).abs() / expected.abs() < 1e-6, "got {got}");
}
#[test]
fn polygon_octant_matches_ring() {
let pg: Polygon<Sp> = Polygon::new(Ring::from_vec(vec![
sp(0., 0.),
sp(0., 90.),
sp(90., 0.),
sp(0., 0.),
]));
let got = SphericalPolygonArea::UNIT.area(&pg);
let expected = core::f64::consts::FRAC_PI_2;
assert!((got - expected).abs() / expected < 1e-6, "got {got}");
}
#[test]
fn defaults_are_mean_earth_radius() {
assert_eq!(SphericalArea::default().radius, 6_371_000.0);
assert_eq!(SphericalPolygonArea::default().radius, 6_371_000.0);
}
}