177 lines
5.7 KiB
Rust
177 lines
5.7 KiB
Rust
//! Footprint geometry and coverage fractions.
|
|
|
|
use crate::geometry::{earth_central_angle, elevation_from_central_angle};
|
|
use apsis_math::Vec3;
|
|
use core::f64::consts::PI;
|
|
|
|
/// A circular ground footprint.
|
|
///
|
|
/// The footprint is the part of the Earth's surface from which a satellite
|
|
/// is visible above a given elevation mask. On a sphere it is an exact
|
|
/// circle, described here by the central angle from the sub-satellite point
|
|
/// and the radius of the body.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct Footprint {
|
|
/// The central half angle of the footprint, in radians.
|
|
///
|
|
/// This is the angle at the centre of the Earth from the sub-satellite
|
|
/// point to the edge of the footprint.
|
|
pub central_angle: f64,
|
|
/// The radius of the body, in kilometers.
|
|
pub body_radius: f64,
|
|
}
|
|
|
|
impl Footprint {
|
|
/// Creates a footprint from its central angle and body radius.
|
|
pub const fn new(central_angle: f64, body_radius: f64) -> Self {
|
|
Self {
|
|
central_angle,
|
|
body_radius,
|
|
}
|
|
}
|
|
|
|
/// Returns the footprint radius measured along the surface, in
|
|
/// kilometers.
|
|
pub fn surface_radius(&self) -> f64 {
|
|
self.central_angle * self.body_radius
|
|
}
|
|
|
|
/// Returns the area of the footprint, in square kilometers.
|
|
///
|
|
/// The area is the part of a sphere cut off by a cone,
|
|
///
|
|
/// ```text
|
|
/// A = 2 pi R^2 (1 - cos(central))
|
|
/// ```
|
|
///
|
|
/// which is exact for a spherical body.
|
|
pub fn area(&self) -> f64 {
|
|
2.0 * PI * self.body_radius * self.body_radius * (1.0 - self.central_angle.cos())
|
|
}
|
|
|
|
/// Returns the fraction of the whole sphere covered by the footprint.
|
|
///
|
|
/// The value is between zero and one half, because a footprint from a
|
|
/// point above the surface can never cover more than a hemisphere.
|
|
pub fn fraction_of_surface(&self) -> f64 {
|
|
self.area() / (4.0 * PI * self.body_radius * self.body_radius)
|
|
}
|
|
|
|
/// Returns `true` when a ground point at the given central angle from
|
|
/// the sub-satellite point lies inside the footprint.
|
|
///
|
|
/// The comparison is inclusive, so a point exactly on the edge is
|
|
/// inside.
|
|
pub fn contains_central_angle(&self, central_angle: f64) -> bool {
|
|
central_angle.is_finite() && central_angle >= 0.0 && central_angle <= self.central_angle
|
|
}
|
|
}
|
|
|
|
/// Returns the central half angle of the footprint, in radians.
|
|
///
|
|
/// This is the largest central angle at which the satellite is still above
|
|
/// the elevation mask.
|
|
///
|
|
/// Returns `None` when the geometry is not usable.
|
|
pub fn footprint_central_angle(
|
|
satellite_radius: f64,
|
|
body_radius: f64,
|
|
elevation_mask: f64,
|
|
) -> Option<f64> {
|
|
let central = earth_central_angle(satellite_radius, body_radius, elevation_mask)?;
|
|
if central.is_finite() && central >= 0.0 {
|
|
Some(central)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Returns the footprint for a satellite and an elevation mask.
|
|
///
|
|
/// Returns `None` when the geometry is not usable.
|
|
pub fn footprint(
|
|
satellite_radius: f64,
|
|
body_radius: f64,
|
|
elevation_mask: f64,
|
|
) -> Option<Footprint> {
|
|
Some(Footprint::new(
|
|
footprint_central_angle(satellite_radius, body_radius, elevation_mask)?,
|
|
body_radius,
|
|
))
|
|
}
|
|
|
|
/// Returns the area of a footprint, in square kilometers.
|
|
///
|
|
/// This is a free function form of [`Footprint::area`].
|
|
///
|
|
/// Returns `None` when the geometry is not usable.
|
|
pub fn footprint_area(satellite_radius: f64, body_radius: f64, elevation_mask: f64) -> Option<f64> {
|
|
Some(footprint(satellite_radius, body_radius, elevation_mask)?.area())
|
|
}
|
|
|
|
/// Returns the fraction of the Earth's surface covered by a footprint.
|
|
///
|
|
/// Returns `None` when the geometry is not usable.
|
|
pub fn footprint_fraction(
|
|
satellite_radius: f64,
|
|
body_radius: f64,
|
|
elevation_mask: f64,
|
|
) -> Option<f64> {
|
|
Some(footprint(satellite_radius, body_radius, elevation_mask)?.fraction_of_surface())
|
|
}
|
|
|
|
/// Returns the angle between two surface points, in radians.
|
|
///
|
|
/// The vectors must be positions relative to the centre of the body, in the
|
|
/// same frame. The angle is the central angle between them, as seen from the
|
|
/// centre.
|
|
///
|
|
/// Returns `None` when either vector has zero length or a non-finite
|
|
/// component.
|
|
pub fn central_angle_between(first: Vec3, second: Vec3) -> Option<f64> {
|
|
if !first.is_finite() || !second.is_finite() {
|
|
return None;
|
|
}
|
|
let first_norm = first.norm();
|
|
let second_norm = second.norm();
|
|
if first_norm == 0.0 || second_norm == 0.0 {
|
|
return None;
|
|
}
|
|
Some(
|
|
(first.dot(second) / (first_norm * second_norm))
|
|
.clamp(-1.0, 1.0)
|
|
.acos(),
|
|
)
|
|
}
|
|
|
|
/// Returns `true` when a ground point lies inside a footprint.
|
|
///
|
|
/// The point and the sub-satellite point are positions relative to the
|
|
/// centre of the body, in kilometers. The footprint supplies the body radius
|
|
/// and the central half angle.
|
|
///
|
|
/// Returns `None` when either vector is degenerate or not finite.
|
|
pub fn point_in_footprint(
|
|
footprint: &Footprint,
|
|
point: Vec3,
|
|
sub_satellite_point: Vec3,
|
|
) -> Option<bool> {
|
|
let central = central_angle_between(point, sub_satellite_point)?;
|
|
Some(footprint.contains_central_angle(central))
|
|
}
|
|
|
|
/// Returns the elevation at which a footprint edge is seen from the ground,
|
|
/// in radians.
|
|
///
|
|
/// This is the inverse of [`footprint_central_angle`], useful when a
|
|
/// footprint is known and the mask it corresponds to is wanted.
|
|
///
|
|
/// Returns `None` when the geometry is not usable.
|
|
pub fn footprint_edge_elevation(satellite_radius: f64, footprint: &Footprint) -> Option<f64> {
|
|
elevation_from_central_angle(
|
|
satellite_radius,
|
|
footprint.body_radius,
|
|
footprint.central_angle,
|
|
)
|
|
}
|