apsis-rs/crates/orbit/apsis-tle/src/element.rs

535 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Structured fields of a two-line element set.
use crate::checksum::verify_line_checksum;
use crate::field::{ElementError, ElementErrorKind, Result};
use apsis_time::{Epoch, TimeScale};
/// The security classification of an element set.
///
/// 根数集的安全密级。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Classification {
/// Unclassified.
///
/// 未分类。
Unclassified,
/// Classified as secret by the originating authority.
///
/// 由发布机构列为机密。
Secret,
/// Classified at some level the parser does not recognise.
///
/// 列为解析器无法识别的密级。
Other(char),
}
impl Classification {
/// Returns the character used in the element set.
///
/// 返回根数集中所用的字符。
pub const fn as_char(self) -> char {
match self {
Self::Unclassified => 'U',
Self::Secret => 'S',
Self::Other(character) => character,
}
}
}
/// A parsed two-line element set.
///
/// The fields are the mean elements and identification data exactly as
/// written in the element set. Units are the ones the format uses, named in
/// each field's documentation. Converting them to a state vector is the
/// concern of `apsis-sgp4`.
///
/// 已解析的两线根数集。
///
/// 各字段即根数集中原样写下的平均根数与标识信息。单位沿用格式所用者,在各字段
/// 文档中注明。把它们转换为状态矢量是 `apsis-sgp4` 的职责。
#[derive(Debug, Clone, PartialEq)]
pub struct TwoLineElement {
/// Satellite name from the optional first line, if one was present.
///
/// 可选首行中的卫星名称,若存在。
pub name: Option<String>,
/// Satellite catalog number.
///
/// 卫星编目号。
pub satellite_number: u32,
/// Classification of the element set.
///
/// 根数集的密级。
pub classification: Classification,
/// International designator, such as `98067A`.
///
/// 国际designator,如 `98067A`。
pub international_designator: String,
/// Epoch of the mean elements, on the UTC scale.
///
/// The format stores the epoch as a two digit year and a fraction of a
/// day. The two digit year is expanded with the convention that years
/// `57` through `99` are in the twentieth century and years `00`
/// through `56` are in the twenty-first, which places the boundary
/// before the start of the satellite catalogue.
///
/// 平均根数的历元,位于 UTC 标度。
///
/// 格式以两位数年份加一日的小数部分存储历元。两位数年份按如下约定展开:
/// `57` 至 `99` 属二十世纪,`00` 至 `56` 属二十一世纪,该分界位于卫星编目
/// 开始之前。
pub epoch: Epoch,
/// First derivative of mean motion divided by two, in revolutions per
/// day squared.
///
/// The format stores half of the derivative, which is why the field is
/// documented as `ndot / 2`.
///
/// 平运动的一阶导数除以二,单位为每日每二次方圈。
///
/// 格式存储该导数的一半,故本字段记作 `ndot / 2`。
pub mean_motion_dot: f64,
/// Second derivative of mean motion divided by six, in revolutions per
/// day cubed.
///
/// The format stores one sixth of the derivative, which is why the
/// field is documented as `nddot / 6`.
///
/// 平运动的二阶导数除以六,单位为每日每三次方圈。
///
/// 格式存储该导数的六分之一,故本字段记作 `nddot / 6`。
pub mean_motion_double_dot: f64,
/// The `BSTAR` drag term, in units of inverse Earth radii.
///
/// The term is a free parameter of the SGP4 drag model. Its units are
/// the reciprocal of the reference distance used by the model, which is
/// the Earth equatorial radius in kilometers.
///
/// `BSTAR` 阻力项,单位为地球半径的倒数。
///
/// 该项是 SGP4 阻力模型的自由参数。其单位是模型所用参考距离的倒数,该距离
/// 即以千米表示的地球赤道半径。
pub bstar: f64,
/// The element set number, which increments when a satellite's elements
/// are refreshed.
///
/// 根数集编号,在卫星根数更新时递增。
pub element_set_number: u32,
/// Orbital inclination, in degrees.
///
/// 轨道倾角,单位为度。
pub inclination: f64,
/// Right ascension of the ascending node, in degrees.
///
/// 升交点赤经,单位为度。
pub right_ascension: f64,
/// Orbital eccentricity, dimensionless.
///
/// 轨道偏心率,无量纲。
pub eccentricity: f64,
/// Argument of perigee, in degrees.
///
/// 近地点幅角,单位为度。
pub argument_of_perigee: f64,
/// Mean anomaly, in degrees.
///
/// 平近点角,单位为度。
pub mean_anomaly: f64,
/// Mean motion, in revolutions per day.
///
/// 平运动,单位为每日圈数。
pub mean_motion: f64,
/// Revolution number at epoch.
///
/// 历元处的转数。
pub revolution_number: u32,
}
/// Returns the one based inclusive character range of a field, as a slice of
/// characters.
///
/// Ranges are inclusive of both ends, matching the column numbering in the
/// format definition.
///
/// 返回某字段的从一开始、含两端的字符区间,以字符切片表示。
///
/// 区间含两端,与格式定义中的列编号一致。
fn field(line: &str, first: usize, last: usize) -> Option<String> {
if first < 1 || last < first {
return None;
}
let characters: Vec<char> = line.chars().collect();
if last > characters.len() {
return None;
}
Some(characters[first - 1..last].iter().collect())
}
/// Reads a field and returns it trimmed of surrounding spaces.
///
/// 读取某字段并去除首尾空格。
fn trimmed_field(line: &str, first: usize, last: usize, line_number: usize) -> Result<String> {
match field(line, first, last) {
Some(text) => Ok(text.trim().to_string()),
None => Err(ElementError::new(
ElementErrorKind::TooShort,
line_number,
Some((first, last)),
)),
}
}
/// Parses an unsigned integer field.
///
/// 解析无符号整数字段。
fn unsigned_field(line: &str, first: usize, last: usize, line_number: usize) -> Result<u32> {
let text = trimmed_field(line, first, last, line_number)?;
text.parse::<u32>().map_err(|_| {
ElementError::new(
ElementErrorKind::InvalidField,
line_number,
Some((first, last)),
)
.with_detail(format!("expected an unsigned integer, found {text:?}"))
})
}
/// Parses a floating point field written with an explicit decimal point.
///
/// 解析带显式小数点的浮点字段。
fn float_field(line: &str, first: usize, last: usize, line_number: usize) -> Result<f64> {
let text = trimmed_field(line, first, last, line_number)?;
text.parse::<f64>().map_err(|_| {
ElementError::new(
ElementErrorKind::InvalidField,
line_number,
Some((first, last)),
)
.with_detail(format!("expected a number, found {text:?}"))
})
}
/// Parses an exponent field of the form `smmmmmse` where the first `s` is an
/// optional sign for the mantissa, `e` is a signed single digit exponent, and
/// the mantissa is interpreted with an assumed decimal point before its first
/// digit.
///
/// For example ` 12345-3` is `0.12345e-3` and `-11606-4` is `-0.11606e-4`.
/// An all blank field is zero, which is how the format writes an absent
/// term.
///
/// 解析形如 `smmmmmse` 的指数字段:首个 `s` 是尾数的可选符号,`e` 是带符号的
/// 一位指数,尾数按首位之前带隐含小数点解释。
///
/// 例如 ` 12345-3` 即 `0.12345e-3`,`-11606-4` 即 `-0.11606e-4`。全空白
/// 字段为零,这正是格式表示某项缺省的方式。
fn exponent_field(line: &str, first: usize, last: usize, line_number: usize) -> Result<f64> {
let text = trimmed_field(line, first, last, line_number)?;
let columns = Some((first, last));
if text.is_empty() {
return Ok(0.0);
}
// The last character is the exponent sign and the one before it is the
// exponent magnitude, so the mantissa is everything before those two.
// 末位字符是指数符号,其前一位是指数大小,故尾数为这两位之前的全部内容。
let characters: Vec<char> = text.chars().collect();
if characters.len() < 3 {
return Err(
ElementError::new(ElementErrorKind::InvalidField, line_number, columns)
.with_detail(format!("exponent field {text:?} is too short")),
);
}
let exponent_sign = match characters[characters.len() - 2] {
'+' => 1,
'-' => -1,
other => {
return Err(ElementError::new(
ElementErrorKind::UnexpectedCharacter,
line_number,
columns,
)
.with_detail(format!("exponent sign is {other:?}, expected + or -")));
}
};
let exponent_magnitude = characters[characters.len() - 1];
let Some(exponent_magnitude) = exponent_magnitude.to_digit(10) else {
return Err(
ElementError::new(ElementErrorKind::UnexpectedCharacter, line_number, columns)
.with_detail(format!(
"exponent is {exponent_magnitude:?}, expected a digit"
)),
);
};
let mantissa: String = characters[..characters.len() - 2].iter().collect();
let mantissa = mantissa.replace(' ', "");
let (mantissa_sign, mantissa) = if let Some(rest) = mantissa.strip_prefix('-') {
(-1.0, rest.to_string())
} else if let Some(rest) = mantissa.strip_prefix('+') {
(1.0, rest.to_string())
} else {
(1.0, mantissa)
};
let digits: String = mantissa
.chars()
.filter(|character| character.is_ascii_digit())
.collect();
if digits.is_empty() {
return Err(
ElementError::new(ElementErrorKind::InvalidField, line_number, columns)
.with_detail(format!("mantissa is {mantissa:?}, expected digits")),
);
}
let value = format!("0.{digits}").parse::<f64>().map_err(|_| {
ElementError::new(ElementErrorKind::InvalidField, line_number, columns)
.with_detail(format!("mantissa is {mantissa:?}"))
})?;
let exponent = exponent_sign * exponent_magnitude as i32;
Ok(mantissa_sign * value * 10f64.powi(exponent))
}
/// Parses the eccentricity field, which is written with an assumed decimal
/// point before its first digit.
///
/// 解析偏心率字段,该字段按首位之前带隐含小数点书写。
fn assumed_decimal_field(line: &str, first: usize, last: usize, line_number: usize) -> Result<f64> {
let text = trimmed_field(line, first, last, line_number)?;
let columns = Some((first, last));
if text.is_empty() || !text.chars().all(|character| character.is_ascii_digit()) {
return Err(
ElementError::new(ElementErrorKind::InvalidField, line_number, columns)
.with_detail(format!("expected digits, found {text:?}")),
);
}
format!("0.{text}").parse::<f64>().map_err(|_| {
ElementError::new(ElementErrorKind::InvalidField, line_number, columns)
.with_detail(format!("eccentricity is {text:?}"))
})
}
/// Expands a two digit year using the convention that `57` through `99` are
/// in the twentieth century and `00` through `56` are in the twenty-first.
///
/// The boundary sits before the start of the satellite catalogue, so no
/// catalogue entry is placed a century away from its true date.
///
/// 按如下约定展开两位数年份:`57` 至 `99` 属二十世纪,`00` 至 `56` 属
/// 二十一世纪。
///
/// 该分界位于卫星编目开始之前,故不会有编目条目被放到距真实日期一个世纪之处。
pub const fn expand_two_digit_year(two_digit: u32) -> i32 {
if two_digit >= 57 {
1900 + two_digit as i32
} else {
2000 + two_digit as i32
}
}
/// Parses the epoch field, which is a two digit year and a fractional day of
/// year, and resolves it on the UTC scale.
///
/// 解析历元字段,即两位数年份与年内的日小数,并在 UTC 标度上确定其值。
fn epoch_field(line: &str, first: usize, last: usize, line_number: usize) -> Result<Epoch> {
let text = trimmed_field(line, first, last, line_number)?;
let columns = Some((first, last));
let invalid = |detail: String| {
ElementError::new(ElementErrorKind::InvalidField, line_number, columns).with_detail(detail)
};
// The field is a two digit year followed by three digits of day of year
// and then a fraction of that day.
// 该字段为两位数年份,后接三位年日与该日的小数部分。
if text.len() < 5 {
return Err(invalid(format!("epoch {text:?} is too short")));
}
let (year_digits, rest) = text.split_at(2);
let (day_digits, fraction_digits) = rest.split_at(3);
if !year_digits.chars().all(|c| c.is_ascii_digit())
|| !day_digits.chars().all(|c| c.is_ascii_digit())
{
return Err(invalid(format!("epoch {text:?} is not numeric")));
}
let year = expand_two_digit_year(
year_digits
.parse::<u32>()
.map_err(|_| invalid(format!("year {year_digits:?}")))?,
);
let day_of_year = day_digits
.parse::<u32>()
.map_err(|_| invalid(format!("day of year {day_digits:?}")))?;
if !(1..=366).contains(&day_of_year) {
return Err(invalid(format!(
"day of year {day_of_year} is out of range"
)));
}
let fraction = if fraction_digits.is_empty() {
0.0
} else {
// The field writes the decimal point explicitly, so the fraction
// begins with a dot. Strip it before forming the value, otherwise
// the parse would see two decimal points.
// 该字段显式写出小数点,故小数部分以点开头。构造数值前先去掉它,否则
// 解析会遇到两个小数点。
let digits = fraction_digits.strip_prefix('.').unwrap_or(fraction_digits);
format!("0.{digits}")
.parse::<f64>()
.map_err(|_| invalid(format!("day fraction {fraction_digits:?}")))?
};
let calendar = calendar_from_day_of_year(year, day_of_year)?;
let mjd = apsis_time::mjd_from_ymd(calendar.0, calendar.1, calendar.2) + fraction;
Epoch::from_mjd(mjd, TimeScale::Utc)
.ok_or_else(|| invalid(format!("epoch {text:?} is outside the leap second table")))
}
/// Converts a year and a day of year, both one based, to a calendar date.
///
/// 把从一开始的年与年日转换为日历日期。
fn calendar_from_day_of_year(year: i32, day_of_year: u32) -> Result<(i32, u32, u32)> {
let mut remaining = day_of_year;
let mut month = 1;
while month <= 12 {
let length = apsis_time::days_in_month(year, month);
if remaining <= length {
return Ok((year, month, remaining));
}
remaining -= length;
month += 1;
}
Err(
ElementError::new(ElementErrorKind::InvalidField, 0, None).with_detail(format!(
"day of year {day_of_year} does not exist in {year}"
)),
)
}
impl TwoLineElement {
/// Parses a two-line element set from one, two, or three lines.
///
/// Line endings may be `\n`, `\r\n`, or a mix; blank lines and lines
/// that are entirely whitespace are ignored. A leading line that does
/// not begin with `1 ` is taken as the satellite name, which is how the
/// optional three line form is written.
///
/// Each line is checked for the expected line number in column one, for
/// its length, and for its checksum. The element set number of the two
/// lines must agree.
///
/// Returns an error describing the first failure and the line on which
/// it occurred.
///
/// 从一行、两行或三行文本解析两线根数集。
///
/// 行结束符可以是 `\n`、`\r\n` 或二者混用;空行与全为空白字符的行会被
/// 忽略。不以 `1 ` 开头的首行被取作卫星名称,这正是可选三行形式的写法。
///
/// 每一行都检查第一列的行号、长度与校验和。两行的根数集编号必须一致。
///
/// 返回描述首个失败及其所在行的错误。
pub fn parse(text: &str) -> Result<Self> {
let lines: Vec<&str> = text
.lines()
.filter(|line| !line.trim().is_empty())
.collect();
let (name, first, second) = match lines.len() {
2 => (None, lines[0], lines[1]),
3 => (Some(lines[0].trim().to_string()), lines[1], lines[2]),
count => {
return Err(ElementError::new(ElementErrorKind::TooShort, 1, None)
.with_detail(format!("expected 2 or 3 non-empty lines, found {count}")));
}
};
// The name line, when present, is not part of the checksummed
// format.
// 名称行若存在,不属于带校验和的格式。
let first_number = if name.is_some() { 2 } else { 1 };
let second_number = first_number + 1;
verify_line_checksum(first, first_number)?;
verify_line_checksum(second, second_number)?;
if !first.starts_with('1') {
return Err(ElementError::new(
ElementErrorKind::UnexpectedCharacter,
first_number,
Some((1, 1)),
)
.with_detail("first data line must begin with 1"));
}
if !second.starts_with('2') {
return Err(ElementError::new(
ElementErrorKind::UnexpectedCharacter,
second_number,
Some((1, 1)),
)
.with_detail("second data line must begin with 2"));
}
let satellite_number = unsigned_field(first, 3, 7, first_number)?;
let second_satellite_number = unsigned_field(second, 3, 7, second_number)?;
if satellite_number != second_satellite_number {
return Err(ElementError::new(
ElementErrorKind::MismatchedElementSet,
second_number,
Some((3, 7)),
)
.with_detail(format!(
"line {first_number} has {satellite_number}, line {second_number} has {second_satellite_number}"
)));
}
let classification = match field(first, 8, 8).as_deref() {
Some("U") => Classification::Unclassified,
Some("S") => Classification::Secret,
Some(other) => {
let character = other.chars().next().unwrap_or(' ');
Classification::Other(character)
}
None => {
return Err(ElementError::new(
ElementErrorKind::TooShort,
first_number,
Some((8, 8)),
));
}
};
let international_designator = trimmed_field(first, 10, 17, first_number)?;
let epoch = epoch_field(first, 19, 32, first_number)?;
let mean_motion_dot = float_field(first, 34, 43, first_number)?;
let mean_motion_double_dot = exponent_field(first, 45, 52, first_number)?;
let bstar = exponent_field(first, 54, 61, first_number)?;
let element_set_number = unsigned_field(first, 65, 68, first_number)?;
let inclination = float_field(second, 9, 16, second_number)?;
let right_ascension = float_field(second, 18, 25, second_number)?;
let eccentricity = assumed_decimal_field(second, 27, 33, second_number)?;
let argument_of_perigee = float_field(second, 35, 42, second_number)?;
let mean_anomaly = float_field(second, 44, 51, second_number)?;
let mean_motion = float_field(second, 53, 63, second_number)?;
let revolution_number = unsigned_field(second, 64, 68, second_number)?;
Ok(Self {
name,
satellite_number,
classification,
international_designator,
epoch,
mean_motion_dot,
mean_motion_double_dot,
bstar,
element_set_number,
inclination,
right_ascension,
eccentricity,
argument_of_perigee,
mean_anomaly,
mean_motion,
revolution_number,
})
}
}