use crate::trajectory::{Trajectory, TrajectoryPoint, NATIVE_RATE_HZ};
use crate::vector::Vector3;
use std::f64::consts::PI;
const GRAVITY: f64 = 9.81; const FAIRWAY_FRICTION_COEFF: f64 = 0.18; const ROLL_EFFICIENCY: f64 = 0.85; const ROLL_SCALING_COEFF: f64 = 0.15;
pub fn get_landing_position(trajectory: &Trajectory) -> Vector3 {
trajectory
.points
.last()
.map(|p| p.position())
.unwrap_or(Vector3::new(f64::NAN, f64::NAN, f64::NAN))
}
pub fn get_landing_velocity(trajectory: &Trajectory) -> Vector3 {
trajectory
.points
.last()
.map(|p| p.velocity())
.unwrap_or(Vector3::new(f64::NAN, f64::NAN, f64::NAN))
}
pub fn get_hang_time(trajectory: &Trajectory) -> f64 {
trajectory.points.last().map(|p| p.t).unwrap_or(f64::NAN)
}
pub fn get_apex_position(trajectory: &Trajectory) -> Vector3 {
trajectory
.points
.iter()
.max_by(|a, b| a.z.partial_cmp(&b.z).unwrap())
.map(|p| p.position())
.unwrap_or(Vector3::new(f64::NAN, f64::NAN, f64::NAN))
}
pub fn get_time_to_apex(trajectory: &Trajectory) -> f64 {
trajectory
.points
.iter()
.max_by(|a, b| a.z.partial_cmp(&b.z).unwrap())
.map(|p| p.t)
.unwrap_or(f64::NAN)
}
pub fn get_peak_height(trajectory: &Trajectory) -> f64 {
trajectory
.points
.iter()
.map(|p| p.z)
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(f64::NAN)
}
pub fn get_descent_angle(trajectory: &Trajectory) -> f64 {
let landing_vel = get_landing_velocity(trajectory);
let horizontal_speed = (landing_vel.x.powi(2) + landing_vel.y.powi(2)).sqrt();
let descent_angle_rad = (-landing_vel.z).atan2(horizontal_speed);
descent_angle_rad * 180.0 / PI
}
pub fn get_carry_distance(trajectory: &Trajectory) -> f64 {
let landing_pos = get_landing_position(trajectory);
landing_pos.magnitude()
}
pub fn get_offline_distance(trajectory: &Trajectory) -> f64 {
let landing_pos = get_landing_position(trajectory);
landing_pos.y
}
pub fn get_total_distance(trajectory: &Trajectory) -> f64 {
let carry = get_carry_distance(trajectory);
let landing_pos = get_landing_position(trajectory);
let landing_vel = get_landing_velocity(trajectory);
let horizontal_speed = (landing_vel.x.powi(2) + landing_vel.y.powi(2)).sqrt();
if horizontal_speed <= 0.1 {
return carry;
}
let descent_angle = get_descent_angle(trajectory).clamp(0.0, 90.0);
let descent_factor = ((90.0 - descent_angle) / 90.0).powf(1.4);
let base_roll = horizontal_speed.powi(2) / (2.0 * FAIRWAY_FRICTION_COEFF * GRAVITY);
let mut roll_distance = base_roll * descent_factor * ROLL_EFFICIENCY * ROLL_SCALING_COEFF;
roll_distance = roll_distance.max(0.0);
let mut heading = Vector3::new(landing_vel.x, landing_vel.y, 0.0);
if heading.magnitude() <= 0.01 {
heading = Vector3::new(landing_pos.x, landing_pos.y, 0.0);
}
let heading = heading.normalize();
let roll_vector = Vector3::new(heading.x * roll_distance, heading.y * roll_distance, 0.0);
let total_vector = landing_pos.add(&roll_vector);
total_vector.magnitude()
}
pub fn down_sample_trajectory(
trajectory: &Trajectory,
target_hz: f64,
) -> Vec<TrajectoryPoint> {
let native = &trajectory.points;
if native.is_empty() {
return Vec::new();
}
if !target_hz.is_finite() || target_hz <= 0.0 || target_hz >= NATIVE_RATE_HZ {
return native.clone();
}
let dt = 1.0 / target_hz;
let last_t = native.last().unwrap().t;
let mut out: Vec<TrajectoryPoint> = Vec::with_capacity(((last_t * target_hz) as usize) + 2);
let mut cursor = 0usize;
let mut k = 0u64;
loop {
let sample_t = (k as f64) * dt;
if sample_t > last_t {
break;
}
while cursor + 1 < native.len() && native[cursor + 1].t <= sample_t {
cursor += 1;
}
if cursor + 1 >= native.len() {
out.push(native[cursor]);
} else {
let a = &native[cursor];
let b = &native[cursor + 1];
let span = b.t - a.t;
let frac = if span > 0.0 {
((sample_t - a.t) / span).clamp(0.0, 1.0)
} else {
0.0
};
out.push(lerp_point(a, b, frac, sample_t));
}
k += 1;
}
let last_native = *native.last().unwrap();
match out.last() {
Some(p) if (p.t - last_native.t).abs() < 1e-9 => {
*out.last_mut().unwrap() = last_native;
}
_ => out.push(last_native),
}
out
}
fn lerp_point(a: &TrajectoryPoint, b: &TrajectoryPoint, frac: f64, t: f64) -> TrajectoryPoint {
let lerp = |x: f64, y: f64| x + (y - x) * frac;
TrajectoryPoint {
x: lerp(a.x, b.x),
y: lerp(a.y, b.y),
z: lerp(a.z, b.z),
vx: lerp(a.vx, b.vx),
vy: lerp(a.vy, b.vy),
vz: lerp(a.vz, b.vz),
t,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn point(t: f64, x: f64, z: f64) -> TrajectoryPoint {
TrajectoryPoint {
x,
y: 0.0,
z,
vx: 1.0,
vy: 0.0,
vz: -1.0,
t,
}
}
#[test]
fn down_sample_interpolates_between_native_steps() {
let mut points = Vec::new();
for k in 0..=500 {
let t = k as f64 * 0.002;
points.push(point(t, t * 100.0, 1.0 - t));
}
let trajectory = Trajectory { points };
let down = down_sample_trajectory(&trajectory, 60.0);
assert!(down.len() >= 60);
assert!((down[0].t - 0.0).abs() < 1e-12);
for w in down.windows(2).take(down.len() - 2) {
assert!((w[1].t - w[0].t - 1.0 / 60.0).abs() < 1e-9);
}
let mid = &down[10];
assert!((mid.x - mid.t * 100.0).abs() < 1e-9);
let last_native = trajectory.points.last().unwrap();
let last_out = down.last().unwrap();
assert_eq!(last_out.t, last_native.t);
assert_eq!(last_out.x, last_native.x);
}
#[test]
fn down_sample_returns_native_when_rate_at_or_above_native() {
let mut points = Vec::new();
for k in 0..=10 {
let t = k as f64 * 0.002;
points.push(point(t, t, 0.0));
}
let trajectory = Trajectory {
points: points.clone(),
};
for rate in [500.0_f64, 1000.0_f64, f64::INFINITY] {
let out = down_sample_trajectory(&trajectory, rate);
assert_eq!(out.len(), points.len(), "rate {} should pass through", rate);
}
}
#[test]
fn down_sample_handles_non_positive_rate_as_passthrough() {
let trajectory = Trajectory {
points: vec![point(0.0, 0.0, 0.0), point(0.002, 1.0, 0.0)],
};
for rate in [0.0_f64, -10.0_f64, f64::NAN] {
let out = down_sample_trajectory(&trajectory, rate);
assert_eq!(out.len(), 2, "rate {} should pass through", rate);
}
}
#[test]
fn down_sample_empty_trajectory_returns_empty() {
let trajectory = Trajectory { points: vec![] };
assert!(down_sample_trajectory(&trajectory, 60.0).is_empty());
}
}