use crate::ellipsoid::{Ellipsoid, WGS84};
use crate::frame::LocalFrame;
use crate::local::Aer;
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)
}
}
#[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,
cos_half_angle: 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(),
cos_half_angle: half_angle_deg.to_radians().cos(),
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.dot(self.axis_enu) / r >= self.cos_half_angle
}
}
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 v = self.frame.enu_of(point);
v.east * v.east + v.north * v.north <= self.radius_m * self.radius_m
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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));
}
}