246 lines
8.9 KiB
Rust
246 lines
8.9 KiB
Rust
//! Line of sight and visibility from a ground point.
|
|
|
|
use crate::geometry::earth_central_angle;
|
|
use apsis_geodesy::{Ellipsoid, Geodetic, WGS84};
|
|
use apsis_math::Vec3;
|
|
use core::f64::consts::FRAC_PI_2;
|
|
|
|
/// A ground station, given as a geodetic position on an ellipsoid.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct GroundPoint {
|
|
/// The geodetic position of the station.
|
|
pub position: Geodetic,
|
|
/// The ellipsoid the position is referred to.
|
|
pub ellipsoid: Ellipsoid,
|
|
/// The sensor elevation mask, in radians.
|
|
///
|
|
/// The minimum elevation above the local horizon at which the station
|
|
/// can see a satellite. A mask of zero means the station sees down to
|
|
/// its own horizon.
|
|
pub elevation_mask: f64,
|
|
}
|
|
|
|
impl GroundPoint {
|
|
/// Creates a ground point on the WGS 84 ellipsoid with a given mask.
|
|
pub fn new(position: Geodetic, elevation_mask: f64) -> Self {
|
|
Self {
|
|
position,
|
|
ellipsoid: WGS84,
|
|
elevation_mask,
|
|
}
|
|
}
|
|
|
|
/// Returns the position of the station in the Earth-fixed Cartesian
|
|
/// frame, in kilometers.
|
|
///
|
|
/// The geodetic conversion yields meters, so the result is divided by a
|
|
/// thousand to match the frame the orbit layer uses.
|
|
pub fn ecef(&self) -> Vec3 {
|
|
self.position.to_cartesian(self.ellipsoid) / 1_000.0
|
|
}
|
|
|
|
/// Returns the distance from the centre of the Earth, in kilometers.
|
|
///
|
|
/// This is the local radius of the ellipsoid at the station's latitude
|
|
/// plus its height, so it includes the effect of the flattening.
|
|
pub fn radius(&self) -> f64 {
|
|
self.ecef().norm()
|
|
}
|
|
|
|
/// Returns `true` when the station and its mask describe a usable
|
|
/// sensor.
|
|
pub fn is_valid(&self) -> bool {
|
|
self.position.is_valid()
|
|
&& self.elevation_mask.is_finite()
|
|
&& (-FRAC_PI_2..=FRAC_PI_2).contains(&self.elevation_mask)
|
|
&& self.ellipsoid.semi_major_axis() > 0.0
|
|
}
|
|
}
|
|
|
|
/// A sensor at a ground point, with a pointing direction.
|
|
///
|
|
/// The direction is a unit vector in the Earth-fixed frame. It is optional,
|
|
/// because a station that can point anywhere with sufficient gain is
|
|
/// modelled by its elevation mask alone; the direction matters only when the
|
|
/// station has a fixed beam.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct Sensor {
|
|
/// The station.
|
|
pub ground: GroundPoint,
|
|
/// The pointing direction in the Earth-fixed frame, if the station is
|
|
/// directional.
|
|
pub pointing: Option<Vec3>,
|
|
}
|
|
|
|
impl Sensor {
|
|
/// Creates a station that can point anywhere above its mask.
|
|
pub const fn omnidirectional(ground: GroundPoint) -> Self {
|
|
Self {
|
|
ground,
|
|
pointing: None,
|
|
}
|
|
}
|
|
|
|
/// Creates a station with a fixed pointing direction.
|
|
///
|
|
/// The direction is normalized when it is used, so the caller may pass
|
|
/// an unnormalized vector.
|
|
pub const fn directional(ground: GroundPoint, pointing: Vec3) -> Self {
|
|
Self {
|
|
ground,
|
|
pointing: Some(pointing),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The point on the Earth's surface directly below a satellite.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct SubSatellitePoint {
|
|
/// The geodetic position of the sub-satellite point.
|
|
pub position: Geodetic,
|
|
/// The ellipsoid the position is referred to.
|
|
pub ellipsoid: Ellipsoid,
|
|
}
|
|
|
|
/// Returns the sub-satellite point for a position in the Earth-fixed frame.
|
|
///
|
|
/// The satellite position is in kilometers. The sub-satellite point is the
|
|
/// intersection of the line from the centre of the Earth through the
|
|
/// satellite with the ellipsoid, which is the geodetic position of the
|
|
/// satellite with its height set to zero at the same latitude and longitude.
|
|
///
|
|
/// This is the geocentric projection, not the point at which the ellipsoid
|
|
/// normal through the satellite meets the surface. The difference is at most
|
|
/// about twenty kilometers for a low Earth satellite, and the geocentric
|
|
/// form is the one the footprint geometry uses.
|
|
///
|
|
/// Returns `None` when the position is not finite or lies on the polar axis,
|
|
/// where no longitude is defined.
|
|
pub fn sub_satellite_point(
|
|
position_ecef_km: Vec3,
|
|
ellipsoid: Ellipsoid,
|
|
) -> Option<SubSatellitePoint> {
|
|
if !position_ecef_km.is_finite() {
|
|
return None;
|
|
}
|
|
let position_m = position_ecef_km * 1_000.0;
|
|
let geodetic = apsis_geodesy::geodetic_from_cartesian(position_m, ellipsoid)?;
|
|
Some(SubSatellitePoint {
|
|
position: Geodetic::new(geodetic.latitude, geodetic.longitude, 0.0),
|
|
ellipsoid,
|
|
})
|
|
}
|
|
|
|
/// Returns the elevation of a satellite as seen from a station, in radians.
|
|
///
|
|
/// The satellite position is in the Earth-fixed frame, in kilometers. The
|
|
/// elevation is measured from the local horizon, which is the plane
|
|
/// perpendicular to the ellipsoid normal at the station.
|
|
///
|
|
/// A positive value means the satellite is above the horizon, a value of a
|
|
/// right angle means it is at the zenith, and a negative value means it is
|
|
/// below the horizon.
|
|
///
|
|
/// Returns `None` when either position is not usable.
|
|
pub fn elevation_angle(satellite_ecef_km: Vec3, ground: &GroundPoint) -> Option<f64> {
|
|
if !satellite_ecef_km.is_finite() || !ground.is_valid() {
|
|
return None;
|
|
}
|
|
let station = ground.ecef();
|
|
let line_of_sight = satellite_ecef_km - station;
|
|
if line_of_sight.norm() == 0.0 {
|
|
return None;
|
|
}
|
|
// The local up direction is the ellipsoid normal at the station, which
|
|
// for a geodetic position is the direction the surface normal points.
|
|
let up = local_up(ground);
|
|
let sine = line_of_sight.normalize()?.dot(up.normalize()?);
|
|
Some(sine.clamp(-1.0, 1.0).asin())
|
|
}
|
|
|
|
/// Returns the unit vector along the ellipsoid normal at a station.
|
|
///
|
|
/// This is the local vertical, the direction a plumb line points. For a
|
|
/// geodetic position it follows from the latitude and longitude, because the
|
|
/// normal of a biaxial ellipsoid lies in the meridian plane and makes the
|
|
/// geodetic latitude with the equatorial plane.
|
|
pub fn local_up(ground: &GroundPoint) -> Vec3 {
|
|
let latitude = ground.position.latitude;
|
|
let longitude = ground.position.longitude;
|
|
let (sin_latitude, cos_latitude) = latitude.sin_cos();
|
|
let (sin_longitude, cos_longitude) = longitude.sin_cos();
|
|
Vec3::new(
|
|
cos_latitude * cos_longitude,
|
|
cos_latitude * sin_longitude,
|
|
sin_latitude,
|
|
)
|
|
}
|
|
|
|
/// Returns the elevation of a satellite as seen from a station, in radians.
|
|
///
|
|
/// This is the same as [`elevation_angle`], named for symmetry with the
|
|
/// other visibility functions.
|
|
///
|
|
/// Returns `None` when either position is not usable.
|
|
pub fn line_of_sight(satellite_ecef_km: Vec3, ground: &GroundPoint) -> Option<f64> {
|
|
elevation_angle(satellite_ecef_km, ground)
|
|
}
|
|
|
|
/// Returns `true` when a satellite is visible from a sensor.
|
|
///
|
|
/// The satellite is visible when its elevation at the station is at least
|
|
/// the sensor's mask, and, for a directional sensor, when the direction to
|
|
/// the satellite is within a right angle of the pointing direction, so that
|
|
/// the satellite is in the forward hemisphere of the beam.
|
|
///
|
|
/// Returns `None` when the satellite position or the sensor is not usable.
|
|
pub fn visible_from(satellite_ecef_km: Vec3, sensor: &Sensor) -> Option<bool> {
|
|
let elevation = elevation_angle(satellite_ecef_km, &sensor.ground)?;
|
|
if elevation < sensor.ground.elevation_mask {
|
|
return Some(false);
|
|
}
|
|
match sensor.pointing {
|
|
None => Some(true),
|
|
Some(pointing) => {
|
|
let to_satellite = satellite_ecef_km - sensor.ground.ecef();
|
|
let pointing = pointing.normalize()?;
|
|
let to_satellite = to_satellite.normalize()?;
|
|
Some(pointing.dot(to_satellite) > 0.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the elevation of a satellite as seen from a geodetic station.
|
|
///
|
|
/// This is a convenience for a station given directly as a geodetic
|
|
/// position, without building a [`GroundPoint`].
|
|
///
|
|
/// Returns `None` when the satellite position or the station is not usable.
|
|
pub fn visible_from_geodetic(
|
|
satellite_ecef_km: Vec3,
|
|
position: Geodetic,
|
|
elevation_mask: f64,
|
|
) -> Option<bool> {
|
|
let ground = GroundPoint::new(position, elevation_mask);
|
|
visible_from(satellite_ecef_km, &Sensor::omnidirectional(ground))
|
|
}
|
|
|
|
/// Returns the central angle between a satellite and a ground station, in
|
|
/// radians.
|
|
///
|
|
/// This uses the spherical relation between the two radii and the elevation,
|
|
/// so it is consistent with the footprint geometry. The satellite position
|
|
/// is used only for its radius.
|
|
///
|
|
/// Returns `None` when the geometry is not usable.
|
|
pub fn central_angle_to_station(
|
|
satellite_ecef_km: Vec3,
|
|
ground: &GroundPoint,
|
|
elevation: f64,
|
|
) -> Option<f64> {
|
|
if !satellite_ecef_km.is_finite() || !ground.is_valid() {
|
|
return None;
|
|
}
|
|
earth_central_angle(satellite_ecef_km.norm(), ground.radius(), elevation)
|
|
}
|