use crate::ellipsoid::{Ellipsoid, WGS84};
use crate::frame::LocalFrame;
use crate::local::Aer;
#[cfg(all(feature = "libm", not(feature = "std"), not(test)))]
use crate::math::Float as _;
use crate::position::{Ecef, Geodetic};
use crate::vector::Vec3;
pub trait Volume {
fn contains(&self, point: Geodetic) -> bool;
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AltitudeBand {
pub floor_m: Option<f64>,
pub ceiling_m: Option<f64>,
}
impl AltitudeBand {
#[inline]
pub const fn new(floor_m: Option<f64>, ceiling_m: Option<f64>) -> Self {
AltitudeBand { floor_m, ceiling_m }
}
#[inline]
pub const fn above(floor_m: f64) -> Self {
AltitudeBand::new(Some(floor_m), None)
}
#[inline]
pub const fn below(ceiling_m: f64) -> Self {
AltitudeBand::new(None, Some(ceiling_m))
}
#[inline]
pub fn contains_height(&self, height_m: f64) -> bool {
height_m >= self.floor_m.unwrap_or(f64::NEG_INFINITY) && height_m <= self.ceiling_m.unwrap_or(f64::INFINITY)
}
#[inline]
pub fn within_prism(&self, footprint_contains: bool, height_m: f64) -> bool {
footprint_contains && self.contains_height(height_m)
}
}
impl Volume for AltitudeBand {
#[inline]
fn contains(&self, point: Geodetic) -> bool {
self.contains_height(point.height_m)
}
}
#[cfg(feature = "footprint")]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Prism {
pub footprint: crate::Footprint,
pub band: AltitudeBand,
}
#[cfg(feature = "footprint")]
impl Prism {
#[inline]
pub fn new(footprint: crate::Footprint, band: AltitudeBand) -> Self {
Prism { footprint, band }
}
}
#[cfg(feature = "footprint")]
impl Volume for Prism {
#[inline]
fn contains(&self, point: Geodetic) -> bool {
self.band.contains_height(point.height_m) && self.footprint.containment(point) != crate::Containment::Outside
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Sphere {
ellipsoid: Ellipsoid,
center: Geodetic,
center_ecef: Ecef,
radius_m: f64,
}
impl Sphere {
#[inline]
pub fn new(center: Geodetic, radius_m: f64) -> Self {
Self::on(WGS84, center, radius_m)
}
pub fn on(ellipsoid: Ellipsoid, center: Geodetic, radius_m: f64) -> Self {
Sphere {
ellipsoid,
center,
center_ecef: ellipsoid.to_ecef(center),
radius_m,
}
}
#[inline]
pub const fn center(&self) -> Geodetic {
self.center
}
#[inline]
pub const fn radius_m(&self) -> f64 {
self.radius_m
}
#[inline]
pub fn contains_ecef(&self, p: Ecef) -> bool {
self.center_ecef.distance_to(p) <= self.radius_m
}
}
impl Volume for Sphere {
#[inline]
fn contains(&self, point: Geodetic) -> bool {
self.contains_ecef(self.ellipsoid.to_ecef(point))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Cone {
frame: LocalFrame,
axis_enu: Vec3,
half_angle_rad: f64,
azimuth_deg: f64,
elevation_deg: f64,
half_angle_deg: f64,
range_m: f64,
}
impl Cone {
#[inline]
pub fn new(apex: Geodetic, azimuth_deg: f64, elevation_deg: f64, half_angle_deg: f64, range_m: f64) -> Self {
Self::on(WGS84, apex, azimuth_deg, elevation_deg, half_angle_deg, range_m)
}
pub fn on(
ellipsoid: Ellipsoid,
apex: Geodetic,
azimuth_deg: f64,
elevation_deg: f64,
half_angle_deg: f64,
range_m: f64,
) -> Self {
Cone {
frame: LocalFrame::on(ellipsoid, apex),
axis_enu: Aer::new(azimuth_deg, elevation_deg, 1.0).direction_enu().vec(),
half_angle_rad: half_angle_deg.to_radians(),
azimuth_deg,
elevation_deg,
half_angle_deg,
range_m,
}
}
#[inline]
pub const fn apex(&self) -> Geodetic {
self.frame.origin()
}
#[inline]
pub const fn axis(&self) -> Aer {
Aer::new(self.azimuth_deg, self.elevation_deg, self.range_m)
}
#[inline]
pub const fn half_angle_deg(&self) -> f64 {
self.half_angle_deg
}
pub fn contains_ecef(&self, p: Ecef) -> bool {
let v = self.frame.to_enu(p).vec();
let r = v.norm();
if r > self.range_m {
return false;
}
if r == 0.0 {
return true; }
v.angle_to(self.axis_enu) <= self.half_angle_rad
}
}
impl Volume for Cone {
#[inline]
fn contains(&self, point: Geodetic) -> bool {
self.contains_ecef(self.frame.ellipsoid().to_ecef(point))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Cylinder {
frame: LocalFrame,
radius_m: f64,
band: AltitudeBand,
}
impl Cylinder {
#[inline]
pub fn new(center: Geodetic, radius_m: f64, band: AltitudeBand) -> Self {
Self::on(WGS84, center, radius_m, band)
}
pub fn on(ellipsoid: Ellipsoid, center: Geodetic, radius_m: f64, band: AltitudeBand) -> Self {
Cylinder {
frame: LocalFrame::on(ellipsoid, center),
radius_m,
band,
}
}
#[inline]
pub const fn center(&self) -> Geodetic {
self.frame.origin()
}
#[inline]
pub const fn radius_m(&self) -> f64 {
self.radius_m
}
#[inline]
pub const fn band(&self) -> AltitudeBand {
self.band
}
}
impl Volume for Cylinder {
fn contains(&self, point: Geodetic) -> bool {
if !self.band.contains_height(point.height_m) {
return false;
}
let ground = Geodetic::new(point.lat_deg, point.lon_deg, 0.0);
let v = self.frame.enu_of(ground);
let flat = v.east.hypot(v.north);
if flat > self.radius_m {
return false;
}
let ellipsoid = self.frame.ellipsoid();
let tightest = ellipsoid.b() * ellipsoid.b() / ellipsoid.a();
if flat < 0.5 * tightest && tightest * (flat / tightest).asin() * (1.0 + 1e-6) < self.radius_m {
return true;
}
let centre = self.frame.origin();
ellipsoid
.geodesic_inverse(Geodetic::new(centre.lat_deg, centre.lon_deg, 0.0), ground)
.is_some_and(|g| g.distance_m <= self.radius_m)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cylinder_radius_is_along_the_ground() {
let mut seed: u64 = 0x2545_f491_4f6c_dd1d;
let mut rnd = || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed >> 11) as f64 / (1u64 << 53) as f64
};
for _ in 0..20_000 {
let centre = Geodetic::new(170.0 * rnd() - 85.0, 360.0 * rnd() - 180.0, 50.0);
let radius = 10f64.powf(2.0 + 4.0 * rnd()); let zone = Cylinder::new(centre, radius, AltitudeBand::default());
let s = radius * (0.9 + 0.2 * rnd()) * if rnd() < 0.5 { 1.0 } else { 1.0 + 1e-7 * (rnd() - 0.5) };
let (p, _) = WGS84.geodesic_direct(centre, 360.0 * rnd(), s);
let p = p.with_height(20_000.0 * rnd());
let exact = WGS84.geodesic_inverse(centre, p).unwrap().distance_m <= radius;
assert_eq!(zone.contains(p), exact, "{centre:?} r {radius}: {p:?}");
}
let zone = Cylinder::new(Geodetic::new(0.0, 0.0, 0.0), 50_000.0, AltitudeBand::default());
let (p, _) = WGS84.geodesic_direct(Geodetic::new(0.0, 0.0, 0.0), 0.0, 49_990.0);
assert!(zone.contains(p.with_height(13_700.0)));
assert!(!zone.contains(Geodetic::new(f64::NAN, 0.0, 0.0)));
}
#[test]
fn tiny_cones_keep_their_shape() {
let apex = Geodetic::new(0.0, 0.0, 0.0);
let cone = Cone::new(apex, 90.0, 10.0, 1e-6, 1e9);
let frame = LocalFrame::new(apex);
assert!(cone.contains_ecef(frame.ecef_of_aer(Aer::new(90.0, 10.0 + 0.9e-6, 1e8))));
assert!(!cone.contains_ecef(frame.ecef_of_aer(Aer::new(90.0, 10.0 + 1.1e-6, 1e8))));
}
#[test]
fn altitude_band() {
assert!(AltitudeBand::new(Some(3000.0), Some(10_000.0)).contains_height(5000.0));
assert!(!AltitudeBand::new(Some(3000.0), Some(10_000.0)).contains_height(2000.0));
assert!(AltitudeBand::default().contains_height(999_999.0));
assert!(AltitudeBand::below(2000.0).contains_height(1000.0));
assert!(!AltitudeBand::above(2000.0).contains_height(1000.0));
assert!(AltitudeBand::new(Some(5000.0), Some(12_000.0)).contains(Geodetic::new(1.0, 2.0, 8000.0)));
}
#[test]
fn sphere() {
let site = Geodetic::new(34.0, -118.0, 0.0);
let s = Sphere::new(site, 10_000.0);
assert!(s.contains(Geodetic::new(34.0, -118.0, 1000.0)));
assert!(!s.contains(Geodetic::new(34.2, -118.0, 0.0)));
assert_eq!(s.center(), site);
assert_eq!(s.radius_m(), 10_000.0);
}
#[test]
fn cone_directional_volume() {
let up = Cone::new(Geodetic::new(0.0, 0.0, 0.0), 0.0, 90.0, 30.0, 20_000.0);
assert!(up.contains(Geodetic::new(0.0, 0.0, 5000.0)));
assert!(!up.contains(Geodetic::new(0.0, 0.0, 25_000.0)));
assert!(!up.contains(Geodetic::new(0.2, 0.0, 1000.0)));
assert!(up.contains(up.apex()));
let east = Cone::new(Geodetic::new(0.0, 0.0, 1000.0), 90.0, 0.0, 20.0, 100_000.0);
assert!(east.contains(Geodetic::new(0.0, 0.3, 1000.0)));
assert!(!east.contains(Geodetic::new(0.3, 0.0, 1000.0)));
assert_eq!(east.axis().azimuth_deg, 90.0);
assert_eq!(east.half_angle_deg(), 20.0);
}
#[test]
fn cylinder() {
let c = Cylinder::new(Geodetic::new(34.0, -118.0, 0.0), 5_000.0, AltitudeBand::above(100.0));
assert!(c.contains(Geodetic::new(34.0, -118.0, 200.0)));
assert!(!c.contains(Geodetic::new(34.0, -118.0, 50.0))); assert!(!c.contains(Geodetic::new(34.1, -118.0, 200.0))); assert_eq!(c.center(), Geodetic::new(34.0, -118.0, 0.0));
assert_eq!(c.radius_m(), 5_000.0);
assert_eq!(c.band(), AltitudeBand::above(100.0));
}
}