use crate::frames::{ecef_to_geodetic, look_angles};
use crate::iq::C_M_PER_S;
use crate::pvt::sagnac_rotate;
use crate::rinex::RinexEphemeris;
pub type Vec3 = [f64; 3];
#[derive(Clone, Debug, PartialEq)]
pub enum Trajectory {
Static(Vec3),
ConstantVelocity {
pos0: Vec3,
vel: Vec3,
},
Waypoints(Vec<(f64, Vec3)>),
}
impl Trajectory {
pub fn position(&self, t_true_s: f64) -> Vec3 {
match self {
Trajectory::Static(p) => *p,
Trajectory::ConstantVelocity { pos0, vel } => [
pos0[0] + vel[0] * t_true_s,
pos0[1] + vel[1] * t_true_s,
pos0[2] + vel[2] * t_true_s,
],
Trajectory::Waypoints(w) => {
let (i, s) = waypoint_segment(w, t_true_s);
match i {
None => w.first().map(|p| p.1).unwrap_or([0.0; 3]),
Some(i) if i + 1 >= w.len() => w[w.len() - 1].1,
Some(i) => {
let (a, b) = (w[i].1, w[i + 1].1);
[
a[0] + (b[0] - a[0]) * s,
a[1] + (b[1] - a[1]) * s,
a[2] + (b[2] - a[2]) * s,
]
}
}
}
}
}
pub fn velocity(&self, t_true_s: f64) -> Vec3 {
match self {
Trajectory::Static(_) => [0.0; 3],
Trajectory::ConstantVelocity { vel, .. } => *vel,
Trajectory::Waypoints(w) => match waypoint_segment(w, t_true_s).0 {
Some(i) if i + 1 < w.len() => {
let dt = w[i + 1].0 - w[i].0;
let (a, b) = (w[i].1, w[i + 1].1);
[(b[0] - a[0]) / dt, (b[1] - a[1]) / dt, (b[2] - a[2]) / dt]
}
_ => [0.0; 3],
},
}
}
pub fn validate(&self) -> Result<(), String> {
let finite = |v: &Vec3| v.iter().all(|x| x.is_finite());
match self {
Trajectory::Static(p) if !finite(p) => Err("static position not finite".into()),
Trajectory::ConstantVelocity { pos0, vel } if !finite(pos0) || !finite(vel) => {
Err("constant-velocity trajectory not finite".into())
}
Trajectory::Waypoints(w) => {
if w.is_empty() {
return Err("waypoint trajectory has no waypoints".into());
}
if w.iter().any(|(t, p)| !t.is_finite() || !finite(p)) {
return Err("waypoint not finite".into());
}
if w.windows(2).any(|p| p[1].0 <= p[0].0) {
return Err("waypoint times must increase strictly".into());
}
Ok(())
}
_ => Ok(()),
}
}
}
fn waypoint_segment(w: &[(f64, Vec3)], t: f64) -> (Option<usize>, f64) {
if w.is_empty() || t < w[0].0 {
return (None, 0.0);
}
let last = w.len() - 1;
if t >= w[last].0 {
return (Some(last), 0.0);
}
let i = w.partition_point(|p| p.0 <= t) - 1;
let s = (t - w[i].0) / (w[i + 1].0 - w[i].0);
(Some(i), s)
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ReceiverClock {
pub bias_s: f64,
pub drift_s_per_s: f64,
}
impl ReceiverClock {
pub fn offset_at(&self, t_true_s: f64) -> f64 {
self.bias_s + self.drift_s_per_s * t_true_s
}
pub fn true_time(&self, t_rx_s: f64) -> f64 {
(t_rx_s - self.bias_s) / (1.0 + self.drift_s_per_s)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RangeProfile {
pub range_m: f64,
pub range_rate_mps: f64,
pub range_accel_mps2: f64,
pub elevation_deg: f64,
pub azimuth_deg: f64,
}
impl RangeProfile {
pub fn range_at(&self, t_true_s: f64) -> f64 {
self.range_m
+ self.range_rate_mps * t_true_s
+ 0.5 * self.range_accel_mps2 * t_true_s * t_true_s
}
pub fn range_rate_at(&self, t_true_s: f64) -> f64 {
self.range_rate_mps + self.range_accel_mps2 * t_true_s
}
}
#[derive(Clone, Debug)]
pub enum SatGeometry {
Broadcast(Box<RinexEphemeris>),
Profile(RangeProfile),
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct GeomPoint {
pub pseudorange_m: f64,
pub elevation_deg: f64,
pub azimuth_deg: f64,
}
const RATE_STEP_S: f64 = 1e-3;
impl SatGeometry {
pub fn point(
&self,
t_rx_s: f64,
start_tow_s: f64,
traj: &Trajectory,
clock: &ReceiverClock,
) -> GeomPoint {
let tt = clock.true_time(t_rx_s);
let b = clock.offset_at(tt);
match self {
SatGeometry::Profile(p) => GeomPoint {
pseudorange_m: p.range_at(tt) + C_M_PER_S * b,
elevation_deg: p.elevation_deg,
azimuth_deg: p.azimuth_deg,
},
SatGeometry::Broadcast(eph) => {
let rx = traj.position(tt);
let t_gps = start_tow_s + tt;
let mut tau = 0.075;
let mut sat = [0.0; 3];
for _ in 0..5 {
sat = sagnac_rotate(eph.sv_position_ecef(t_gps - tau), tau);
tau = dist(sat, rx) / C_M_PER_S;
}
let t_tx = t_gps - tau;
let dt_sv = eph.sv_clock_bias_s(t_tx) - eph.clock_reference_group_delay_s();
let look = look_angles(ecef_to_geodetic(rx), sat);
GeomPoint {
pseudorange_m: tau * C_M_PER_S + C_M_PER_S * (b - dt_sv),
elevation_deg: look.el_rad.to_degrees(),
azimuth_deg: look.az_rad.to_degrees(),
}
}
}
}
pub fn pseudorange_rate(
&self,
t_rx_s: f64,
start_tow_s: f64,
traj: &Trajectory,
clock: &ReceiverClock,
) -> f64 {
match self {
SatGeometry::Profile(p) => {
let tt = clock.true_time(t_rx_s);
(p.range_rate_at(tt) + C_M_PER_S * clock.drift_s_per_s)
/ (1.0 + clock.drift_s_per_s)
}
SatGeometry::Broadcast(_) => {
let h = RATE_STEP_S;
let a = self.point(t_rx_s + h, start_tow_s, traj, clock);
let b = self.point(t_rx_s - h, start_tow_s, traj, clock);
(a.pseudorange_m - b.pseudorange_m) / (2.0 * h)
}
}
}
}
fn dist(a: Vec3, b: Vec3) -> f64 {
let (dx, dy, dz) = (a[0] - b[0], a[1] - b[1], a[2] - b[2]);
(dx * dx + dy * dy + dz * dz).sqrt()
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ElevationCn0 {
pub zenith_dbhz: f64,
pub horizon_dbhz: f64,
}
impl Default for ElevationCn0 {
fn default() -> Self {
Self {
zenith_dbhz: 47.0,
horizon_dbhz: 35.0,
}
}
}
impl ElevationCn0 {
pub fn cn0_dbhz(&self, elevation_deg: f64) -> f64 {
let s = elevation_deg.clamp(0.0, 90.0).to_radians().sin();
self.horizon_dbhz + (self.zenith_dbhz - self.horizon_dbhz) * s
}
}