use chrono::{Duration, NaiveDate, TimeZone, Utc};
use qtty::angular::Degrees;
use qtty::angular::Turn;
use qtty::angular_rate::AngularRate;
use qtty::time::Day;
use tempoch::{Time, UTC};
use super::TleError;
use crate::formats::tle::{Classification, InternationalDesignator, SatelliteNumber, TLE};
pub fn validate_tle_checksum(line: &str) -> Result<(), TleError> {
let line_no = if line.starts_with('1') {
1u8
} else if line.starts_with('2') {
2u8
} else {
0u8
};
if line.len() != 69 {
return Err(TleError::BadLength {
line: line_no,
got: line.len(),
});
}
let bytes = line.as_bytes();
let stated_byte = bytes[68];
if !stated_byte.is_ascii_digit() {
return Err(TleError::BadChecksum {
line: line_no,
stated: 0,
computed: compute_checksum(&line[..68]),
});
}
let stated = stated_byte - b'0';
let computed = compute_checksum(&line[..68]);
if stated != computed {
return Err(TleError::BadChecksum {
line: line_no,
stated,
computed,
});
}
Ok(())
}
pub fn compute_tle_checksum(prefix_68: &str) -> u8 {
compute_checksum(prefix_68)
}
pub(crate) fn compute_checksum(prefix_68: &str) -> u8 {
let mut sum: u32 = 0;
for b in prefix_68.bytes() {
if b.is_ascii_digit() {
sum += (b - b'0') as u32;
} else if b == b'-' {
sum += 1;
}
}
(sum % 10) as u8
}
pub fn parse_tle(line1: &str, line2: &str) -> Result<TLE, TleError> {
parse_tle_inner(None, line1, line2)
}
pub fn parse_3le(name: &str, line1: &str, line2: &str) -> Result<TLE, TleError> {
let trimmed = name.trim_start_matches("0 ").trim();
let owned = if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
};
parse_tle_inner(owned, line1, line2)
}
fn parse_tle_inner(name: Option<String>, line1: &str, line2: &str) -> Result<TLE, TleError> {
if line1.len() != 69 {
return Err(TleError::BadLength {
line: 1,
got: line1.len(),
});
}
if line2.len() != 69 {
return Err(TleError::BadLength {
line: 2,
got: line2.len(),
});
}
let l1b = line1.as_bytes();
let l2b = line2.as_bytes();
if l1b[0] as char != '1' {
return Err(TleError::BadLeadingChar {
line: 1,
expected: '1',
found: l1b[0] as char,
});
}
if l2b[0] as char != '2' {
return Err(TleError::BadLeadingChar {
line: 2,
expected: '2',
found: l2b[0] as char,
});
}
validate_tle_checksum(line1)?;
validate_tle_checksum(line2)?;
let norad_l1 = SatelliteNumber::parse(&line1[2..7])?;
let classification = Classification::from_char(line1.as_bytes()[7] as char)?;
let intl_des = InternationalDesignator(line1[9..17].trim().to_string());
let epoch_year_2 = parse_int_field(&line1[18..20], "epoch_year")?;
let epoch_day = parse_float_field(&line1[20..32], "epoch_day")?;
let mean_motion_dot = parse_signed_decimal(&line1[33..43], "mean_motion_dot")?;
let mean_motion_ddot = parse_assumed_decimal_exponent(&line1[44..52], "mean_motion_ddot")?;
let bstar = parse_assumed_decimal_exponent(&line1[53..61], "bstar")?;
let element_set_number =
parse_int_field(line1[64..68].trim_start(), "element_set_number")? as u16;
let norad_l2 = SatelliteNumber::parse(&line2[2..7])?;
if norad_l1 != norad_l2 {
return Err(TleError::MismatchedSatelliteNumber {
l1: norad_l1.0,
l2: norad_l2.0,
});
}
let inclination = Degrees::new(parse_float_field(&line2[8..16], "inclination")?);
let raan = Degrees::new(parse_float_field(&line2[17..25], "raan")?);
let eccentricity = {
let raw = line2[26..33].trim();
if raw.is_empty() || !raw.chars().all(|c| c.is_ascii_digit()) {
return Err(TleError::InvalidNumber {
field: "eccentricity",
raw: raw.into(),
});
}
let scaled: u64 = raw.parse().map_err(|_| TleError::InvalidNumber {
field: "eccentricity",
raw: raw.into(),
})?;
scaled as f64 * 10f64.powi(-(raw.len() as i32))
};
let argp = Degrees::new(parse_float_field(&line2[34..42], "argument_of_perigee")?);
let mean_anomaly = Degrees::new(parse_float_field(&line2[43..51], "mean_anomaly")?);
let mean_motion_revs_per_day = parse_float_field(&line2[52..63], "mean_motion")?;
let mean_motion = AngularRate::<Turn, Day>::new(mean_motion_revs_per_day);
let revolution_number_at_epoch =
parse_int_field(line2[63..68].trim_start(), "revolution_number")? as u32;
let epoch = epoch_from_year_doy(expand_two_digit_year(epoch_year_2 as i32), epoch_day)?;
Ok(TLE {
name,
norad_id: norad_l1,
classification,
international_designator: intl_des,
epoch,
mean_motion_dot,
mean_motion_ddot,
bstar,
element_set_number,
revolution_number_at_epoch,
inclination,
raan,
eccentricity,
argument_of_perigee: argp,
mean_anomaly,
mean_motion,
})
}
pub(crate) fn expand_two_digit_year(yy: i32) -> i32 {
if yy >= 57 {
1900 + yy
} else {
2000 + yy
}
}
pub(crate) fn epoch_from_year_doy(year: i32, day_of_year: f64) -> Result<Time<UTC>, TleError> {
if !day_of_year.is_finite() || !(1.0..367.0).contains(&day_of_year) {
return Err(TleError::InvalidEpoch {
year,
day_of_year,
reason: "day-of-year out of range",
});
}
let day_int = day_of_year.floor() as i64;
let frac_day = day_of_year - day_int as f64;
let date = NaiveDate::from_yo_opt(year, day_int as u32).ok_or(TleError::InvalidEpoch {
year,
day_of_year,
reason: "calendar date does not exist",
})?;
let nanos = (frac_day * 86_400.0 * 1e9).round() as i64;
let dt =
Utc.from_utc_datetime(&date.and_hms_opt(0, 0, 0).unwrap()) + Duration::nanoseconds(nanos);
Time::<UTC>::try_from_chrono(dt).map_err(|e| TleError::EpochConversion(format!("{e:?}")))
}
fn parse_int_field(raw: &str, field: &'static str) -> Result<i64, TleError> {
let s = raw.trim();
s.parse::<i64>().map_err(|_| TleError::InvalidNumber {
field,
raw: raw.into(),
})
}
fn parse_float_field(raw: &str, field: &'static str) -> Result<f64, TleError> {
let s = raw.trim();
s.parse::<f64>().map_err(|_| TleError::InvalidNumber {
field,
raw: raw.into(),
})
}
fn parse_signed_decimal(raw: &str, field: &'static str) -> Result<f64, TleError> {
let s = raw.trim();
s.parse::<f64>().map_err(|_| TleError::InvalidNumber {
field,
raw: raw.into(),
})
}
pub(crate) fn parse_assumed_decimal_exponent(
raw: &str,
field: &'static str,
) -> Result<f64, TleError> {
let s = raw.trim();
if s.is_empty() {
return Ok(0.0);
}
let (sign, rest) = match s.as_bytes()[0] {
b'-' => (-1.0_f64, &s[1..]),
b'+' => (1.0, &s[1..]),
_ => (1.0, s),
};
if rest.len() < 2 {
return Err(TleError::InvalidNumber {
field,
raw: raw.into(),
});
}
let exp_sep = rest.rfind(['-', '+']).ok_or(TleError::InvalidNumber {
field,
raw: raw.into(),
})?;
let mantissa_digits = &rest[..exp_sep];
let exp_str = &rest[exp_sep..];
if !mantissa_digits.chars().all(|c| c.is_ascii_digit()) || mantissa_digits.is_empty() {
return Err(TleError::InvalidNumber {
field,
raw: raw.into(),
});
}
let mantissa: f64 = mantissa_digits
.parse::<u64>()
.map_err(|_| TleError::InvalidNumber {
field,
raw: raw.into(),
})? as f64
* 10f64.powi(-(mantissa_digits.len() as i32));
let exp: i32 = exp_str.parse().map_err(|_| TleError::InvalidNumber {
field,
raw: raw.into(),
})?;
Ok(sign * mantissa * 10f64.powi(exp))
}