use std::{collections::HashMap, fmt, sync::Arc};
use nalgebra::Matrix3;
use once_cell::sync::OnceCell;
use crate::{
constants::{Degree, Kilometer, MpcCode, MpcCodeObs},
env_state::OutfitEnv,
error_models::{get_bias_rms, ErrorModel, ErrorModelData},
jpl_ephem::download_jpl_file::EphemFileSource,
observers::{observatories::Observatories, Observer},
outfit_errors::OutfitError,
ref_system::{rotpn, RefEpoch, RefSystem},
};
use crate::jpl_ephem::JPLEphem;
#[derive(Debug, Clone)]
pub struct Outfit {
env_state: OutfitEnv,
observatories: Observatories,
jpl_source: EphemFileSource,
jpl_ephem: OnceCell<JPLEphem>,
pub error_model: ErrorModel,
error_model_data: ErrorModelData,
rot_equmj2000_to_eclmj2000: Matrix3<f64>,
rot_eclmj2000_to_equmj2000: Matrix3<f64>,
}
impl Outfit {
pub fn new(jpl_file: &str, error_model: ErrorModel) -> Result<Self, OutfitError> {
let rot1 = rotpn(
&RefSystem::Equm(RefEpoch::J2000),
&RefSystem::Eclm(RefEpoch::J2000),
)?;
let rot2 = rotpn(
&RefSystem::Eclm(RefEpoch::J2000),
&RefSystem::Equm(RefEpoch::J2000),
)?;
Ok(Outfit {
env_state: OutfitEnv::new(),
observatories: Observatories::new(),
jpl_source: jpl_file.try_into()?,
jpl_ephem: OnceCell::new(),
error_model,
error_model_data: error_model.read_error_model_file()?,
rot_equmj2000_to_eclmj2000: rot1,
rot_eclmj2000_to_equmj2000: rot2,
})
}
pub fn get_rot_equmj2000_to_eclmj2000(&self) -> &Matrix3<f64> {
&self.rot_equmj2000_to_eclmj2000
}
pub fn get_rot_eclmj2000_to_equmj2000(&self) -> &Matrix3<f64> {
&self.rot_eclmj2000_to_equmj2000
}
pub fn get_jpl_ephem(&self) -> Result<&JPLEphem, OutfitError> {
self.jpl_ephem
.get_or_try_init(|| JPLEphem::new(&self.jpl_source))
}
pub fn get_ut1_provider(&self) -> &hifitime::ut1::Ut1Provider {
&self.env_state.ut1_provider
}
pub(crate) fn get_observatories(&self) -> &MpcCodeObs {
self.observatories
.mpc_code_obs
.get_or_init(|| self.init_observatories())
}
pub fn get_observer_from_mpc_code(&self, mpc_code: &MpcCode) -> Arc<Observer> {
self.get_observatories()
.get(mpc_code)
.unwrap_or_else(|| panic!("MPC code not found: {mpc_code}"))
.clone()
}
pub(crate) fn init_observatories(&self) -> MpcCodeObs {
let mut observatories: MpcCodeObs = HashMap::new();
let mpc_code_response = self
.env_state
.get_from_url("https://www.minorplanetcenter.net/iau/lists/ObsCodes.html");
let mpc_code_csv = mpc_code_response
.trim()
.strip_prefix("<pre>")
.and_then(|s| s.strip_suffix("</pre>"))
.expect("Failed to strip pre tags");
for lines in mpc_code_csv.lines().skip(2) {
let line = lines.trim();
if let Some((code, remain)) = line.split_at_checked(3) {
let remain = remain.trim_end();
let (longitude, cos, sin, name) = parse_remain(remain, code);
let bias_rms =
get_bias_rms(&self.error_model_data, code.to_string(), "c".to_string());
let observer = Observer::from_parallax(
longitude as f64,
cos as f64,
sin as f64,
Some(name),
bias_rms.map(|(ra, _)| ra as f64),
bias_rms.map(|(_, dec)| dec as f64),
)
.expect("Failed to create observer");
observatories.insert(code.to_string(), Arc::new(observer));
};
}
observatories
}
pub(crate) fn uint16_from_mpc_code(&mut self, mpc_code: &MpcCode) -> u16 {
let observer = self.get_observer_from_mpc_code(mpc_code);
self.observatories.uint16_from_observer(observer)
}
pub(crate) fn uint16_from_observer(&mut self, observer: Arc<Observer>) -> u16 {
self.observatories.uint16_from_observer(observer)
}
pub(crate) fn get_observer_from_uint16(&self, observer_idx: u16) -> &Observer {
self.observatories.get_observer_from_uint16(observer_idx)
}
pub fn new_observer(
&mut self,
longitude: Degree,
latitude: Degree,
elevation: Kilometer,
name: Option<String>,
) -> Arc<Observer> {
self.observatories
.create_observer(longitude, latitude, elevation, name)
}
pub(crate) fn add_observer_internal(&mut self, observer: Arc<Observer>) -> u16 {
self.observatories.add_observer(observer)
}
pub fn add_observer(&mut self, observer: Arc<Observer>) {
self.add_observer_internal(observer);
}
#[inline]
pub fn show_observatories_string(&self) -> String {
self.observatories.to_string()
}
#[inline]
pub fn show_observatories(&self) -> ObservatoriesView<'_> {
ObservatoriesView(&self.observatories)
}
}
pub struct ObservatoriesView<'a>(&'a Observatories);
impl<'a> fmt::Display for ObservatoriesView<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
fn parse_f32(
s: &str,
slice: std::ops::Range<usize>,
code: &str,
) -> Result<f32, std::num::ParseFloatError> {
s.get(slice)
.unwrap_or_else(|| panic!("Failed to parse float for observer code: {code}"))
.trim()
.parse()
}
fn parse_remain(remain: &str, code: &str) -> (f32, f32, f32, String) {
let name = remain
.get(27..)
.unwrap_or_else(|| panic!("Failed to parse name value for code: {code}"));
let Some(longitude) = parse_f32(remain, 1..10, code).ok() else {
return (0.0, 0.0, 0.0, name.to_string());
};
let Some(cos) = parse_f32(remain, 10..18, code).ok() else {
return (longitude, 0.0, 0.0, name.to_string());
};
let Some(sin) = parse_f32(remain, 18..27, code).ok() else {
return (longitude, cos, 0.0, name.to_string());
};
(longitude, cos, sin, name.to_string())
}
#[cfg(test)]
mod outfit_show_observatories_tests {
use super::*;
use std::sync::Arc;
fn build_outfit_with_users(users: &[(&str, f64, f64, f64)]) -> Outfit {
let mut outfit = Outfit::new("horizon:DE440", crate::error_models::ErrorModel::FCCT14)
.expect("Failed to construct Outfit for display tests");
for (name, lon_deg, lat_deg, elev_km) in users.iter().copied() {
let obs = Observer::new(
lon_deg,
lat_deg,
elev_km,
Some(name.to_string()),
None,
None,
)
.expect("Failed to create user observer");
outfit.add_observer(Arc::new(obs));
}
outfit
}
#[test]
fn show_observatories_empty() {
let outfit = build_outfit_with_users(&[]);
let s_string = outfit.show_observatories_string();
let s_view = format!("{}", outfit.show_observatories());
assert_eq!(
s_string, s_view,
"String output and Display adaptor should match"
);
assert!(
s_string.starts_with("No observatories defined (user or MPC)."),
"Missing 'User-defined observers:' header. Got:\n{s_string}"
);
assert!(
!s_string.contains("MPC observers:"),
"Should not show 'MPC observers:' when OnceLock is unset. Got:\n{s_string}"
);
}
#[test]
fn show_observatories_with_users() {
let outfit = build_outfit_with_users(&[
("UserA", 10.0, 0.0, 0.0),
("UserB", 20.0, 45.0, 2.0), ]);
let s_string = outfit.show_observatories_string();
let s_view = format!("{}", outfit.show_observatories());
assert_eq!(
s_string, s_view,
"String output and Display adaptor should match"
);
assert!(
s_string.starts_with("User-defined observers:\n"),
"Missing 'User-defined observers:' header. Got:\n{s_string}"
);
assert!(
s_string.contains("UserA (lon: 10.000000°"),
"Missing formatted line for UserA. Got:\n{s_string}"
);
assert!(
s_string.contains("UserB (lon: 20.000000°"),
"Missing formatted line for UserB. Got:\n{s_string}"
);
}
#[test]
fn show_observatories_with_mpc_section() {
let outfit = build_outfit_with_users(&[("UserOnly", 0.0, 0.0, 0.0)]);
let mpc_site = Observer::new(
-156.2575,
20.7075,
3.055,
Some("Haleakala".to_string()),
None,
None,
)
.expect("Failed to create MPC observer");
let mut mpc_table: crate::constants::MpcCodeObs = Default::default();
mpc_table.insert("I41".to_string(), Arc::new(mpc_site));
outfit
.observatories
.mpc_code_obs
.set(mpc_table)
.expect("OnceLock<MpcCodeObs> already initialized");
let s_string = outfit.show_observatories_string();
let s_view = format!("{}", outfit.show_observatories());
assert_eq!(
s_string, s_view,
"String output and Display adaptor should match"
);
assert!(
s_string.contains("MPC observers:"),
"Missing 'MPC observers:' header after setting OnceLock. Got:\n{s_string}"
);
assert!(
s_string.contains("[I41]"),
"Missing MPC code tag '[I41]' in output. Got:\n{s_string}"
);
}
}