///|
/// A named TLE entry loaded from a two-line or three-line catalog.
pub(all) struct TleCatalogEntry {
name : String
tle : Tle
} derive(Eq, Debug)
///|
fn catalog_error(line : Int, message : String, fragment : String) -> SatError {
SatError::new(InvalidFormat, message, fragment~, line~)
}
///|
fn catalog_lines(text : String) -> Array[(Int, String)] {
let result : Array[(Int, String)] = []
let mut line_number = 0
for line in text.split("\n") {
line_number = line_number + 1
let value = line.trim().to_owned()
if !value.is_empty() && !value.view().has_prefix("#") {
result.push((line_number, value))
}
}
result
}
///|
fn default_catalog_name(tle : Tle) -> String {
"NORAD \{tle.catalog_number}"
}
///|
/// Parse a catalog containing either repeated two-line TLE records or the
/// common three-line format with a satellite name before each pair.
///
/// Blank lines are ignored. Named records keep their display name; unnamed
/// records receive a NORAD name.
pub fn parse_tle_catalog(
text : String,
) -> Result[Array[TleCatalogEntry], SatError] {
let lines = catalog_lines(text)
if lines.is_empty() {
return Err(SatError::new(EmptyInput, "TLE catalog must not be empty"))
}
let entries : Array[TleCatalogEntry] = []
let mut index = 0
while index < lines.length() {
let (line_number, first) = lines[index]
let (name, line1_number, line1) = if first.view().has_prefix("1 ") {
("", line_number, first)
} else {
index = index + 1
if index >= lines.length() {
return Err(
catalog_error(line_number, "catalog name is missing TLE lines", first),
)
}
let (number, value) = lines[index]
if !value.view().has_prefix("1 ") {
return Err(
catalog_error(number, "expected TLE line 1 after catalog name", value),
)
}
(first, number, value)
}
index = index + 1
if index >= lines.length() {
return Err(catalog_error(line1_number, "TLE line 2 is missing", line1))
}
let (line2_number, line2) = lines[index]
if !line2.view().has_prefix("2 ") {
return Err(catalog_error(line2_number, "expected TLE line 2", line2))
}
let tle = match validate_tle(line1, line2) {
Ok(value) => value
Err(error) =>
return Err(
catalog_error(
line1_number,
"invalid TLE catalog entry: \{error.to_string()}",
line1,
),
)
}
let display_name = if name.is_empty() {
default_catalog_name(tle)
} else {
name
}
entries.push({ name: display_name, tle })
index = index + 1
}
Ok(entries)
}
///|
/// Predict SGP4 pass windows for every entry in a named TLE catalog.
pub fn predict_catalog_passes_sgp4(
entries : Array[TleCatalogEntry],
station : GroundStation,
from : UtcDateTime,
duration_hours : Int,
minimum_elevation_deg : Double,
) -> Result[Array[CatalogPassWindow], SatError] {
let result : Array[CatalogPassWindow] = []
for entry in entries {
let windows = match
predict_passes_sgp4(
entry.tle,
station,
from,
duration_hours,
minimum_elevation_deg,
) {
Ok(value) => value
Err(error) => return Err(error)
}
for window in windows {
result.push({
name: entry.name,
catalog_number: entry.tle.catalog_number,
window,
})
}
}
Ok(result)
}