use geometry_cs::{CoordinateSystem, GeographicFamily, Spheroid};
use geometry_tag::SameAs;
use geometry_trait::{Point, PointOrder, Polygon, Ring};
use crate::area::AreaStrategy;
#[cfg(feature = "std")]
use crate::normalise::HasAngularUnits;
#[cfg(feature = "std")]
use crate::spherical::area::excess_accumulator;
#[derive(Debug, Clone, Copy)]
pub struct GeographicArea {
pub spheroid: Spheroid,
}
impl GeographicArea {
pub const WGS84: Self = Self {
spheroid: Spheroid::WGS84,
};
#[cfg(feature = "std")]
#[inline]
#[must_use]
fn authalic_radius(&self) -> f64 {
let a = self.spheroid.equatorial_radius;
let e2 = self.spheroid.eccentricity_squared();
let e = e2.sqrt();
(a * a / 2.0 * (1.0 + (1.0 - e2) / e * e.atanh())).sqrt()
}
}
impl Default for GeographicArea {
#[inline]
fn default() -> Self {
Self::WGS84
}
}
#[derive(Debug, Clone, Copy)]
pub struct GeographicPolygonArea {
pub spheroid: Spheroid,
}
impl GeographicPolygonArea {
pub const WGS84: Self = Self {
spheroid: Spheroid::WGS84,
};
}
impl Default for GeographicPolygonArea {
#[inline]
fn default() -> Self {
Self::WGS84
}
}
#[cfg(feature = "std")]
impl<R> AreaStrategy<R> for GeographicArea
where
R: Ring,
<R::Point as Point>::Cs: CoordinateSystem,
<<R::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<GeographicFamily>,
R::Point: Point<Scalar = f64>,
<R::Point as Point>::Cs: HasAngularUnits,
{
type Out = f64;
#[inline]
fn area(&self, r: &R) -> f64 {
let radius = self.authalic_radius();
let signed = excess_accumulator::<R>(r) * radius * radius;
match r.point_order() {
PointOrder::Clockwise => signed,
PointOrder::CounterClockwise => -signed,
}
}
}
#[cfg(feature = "std")]
impl<P> AreaStrategy<P> for GeographicPolygonArea
where
P: Polygon,
GeographicArea: AreaStrategy<P::Ring, Out = f64>,
{
type Out = f64;
#[inline]
fn area(&self, p: &P) -> f64 {
let ring = GeographicArea {
spheroid: self.spheroid,
};
let mut total = ring.area(p.exterior());
for inner in p.interiors() {
total += ring.area(inner);
}
total
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
#![allow(
clippy::float_cmp,
reason = "areas are compared with an explicit relative tolerance, not `==`"
)]
use super::{GeographicArea, GeographicPolygonArea};
use crate::area::AreaStrategy;
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Geographic};
use geometry_model::{Polygon, Ring};
type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
#[inline]
fn gg(lon: f64, lat: f64) -> Gg {
WithCs::new(Adapt([lon, lat]))
}
#[test]
fn one_degree_box_near_equator_wgs84() {
let r: Ring<Gg> = Ring::from_vec(vec![
gg(0., 0.),
gg(1., 0.),
gg(1., 1.),
gg(0., 1.),
gg(0., 0.),
]);
let got = GeographicArea::WGS84.area(&r).abs();
let expected = 12_309e6;
assert!(
(got - expected).abs() / expected < 0.02,
"got {} km² expected ~12309 km²",
got / 1e6
);
}
#[test]
fn polygon_box_matches_ring() {
let pg: Polygon<Gg> = Polygon::new(Ring::from_vec(vec![
gg(0., 0.),
gg(1., 0.),
gg(1., 1.),
gg(0., 1.),
gg(0., 0.),
]));
let got = GeographicPolygonArea::WGS84.area(&pg).abs();
let expected = 12_309e6;
assert!(
(got - expected).abs() / expected < 0.02,
"got {}",
got / 1e6
);
}
}