use super::bimap::BiMap;
use super::Observer;
use crate::constants::{Degree, Kilometer, MpcCodeObs};
use std::{
fmt,
sync::{Arc, OnceLock},
};
#[derive(Debug, Clone)]
pub(crate) struct Observatories {
pub(crate) mpc_code_obs: OnceLock<MpcCodeObs>,
obs_to_uint16: BiMap<Arc<Observer>, u16>,
}
impl Observatories {
pub(crate) fn new() -> Self {
Observatories {
mpc_code_obs: OnceLock::new(),
obs_to_uint16: BiMap::new(),
}
}
pub(crate) fn create_observer(
&mut self,
longitude: Degree,
latitude: Degree,
elevation: Kilometer,
name: Option<String>,
) -> Arc<Observer> {
let obs = Observer::new(longitude, latitude, elevation, name.clone(), None, None)
.expect("Failed to create observer");
let arc_observer = Arc::new(obs);
self.obs_to_uint16
.entry_or_insert_by_key(arc_observer.clone(), self.obs_to_uint16.len() as u16);
arc_observer
}
pub(crate) fn add_observer(&mut self, observer: Arc<Observer>) -> u16 {
let obs_idx = self.obs_to_uint16.len() as u16;
*self.obs_to_uint16.entry_or_insert_by_key(observer, obs_idx)
}
pub(crate) fn get_observer_from_uint16(&self, observer_idx: u16) -> &Observer {
self.obs_to_uint16
.get_by_value(&observer_idx)
.unwrap_or_else(|| panic!("Observer index not found: {observer_idx}"))
}
pub(crate) fn uint16_from_observer(&mut self, observer: Arc<Observer>) -> u16 {
let obs_idx = self.obs_to_uint16.len() as u16;
*self.obs_to_uint16.entry_or_insert_by_key(observer, obs_idx)
}
}
impl fmt::Display for Observatories {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.obs_to_uint16.is_empty() {
if self.mpc_code_obs.get().is_none() {
writeln!(f, "No observatories defined (user or MPC).\nTrying to get an observer from the MPC or insert a new one to initialize the observatory list.")?;
return Ok(());
} else {
writeln!(f, "No user-defined observers.")?;
}
}
writeln!(f, "User-defined observers:")?;
for obs in self.obs_to_uint16.keys() {
let (lat, height) = obs.geodetic_lat_height_wgs84();
writeln!(
f,
" {} (lon: {:.6}°, lat: {:.6}°, elev: {:.2} km)",
obs.name.clone().unwrap_or_else(|| "Unnamed".to_string()),
obs.longitude,
lat,
height
)?;
}
if let Some(mpc_code_obs) = self.mpc_code_obs.get() {
writeln!(f, "MPC observers:")?;
for (code, obs) in mpc_code_obs.iter() {
let (lat, height) = obs.geodetic_lat_height_wgs84();
writeln!(
f,
" {} [{}] (lon: {:.6}°, lat: {:.6}°, elev: {:.2} km)",
obs.name.clone().unwrap_or_else(|| "Unnamed".to_string()),
code,
obs.longitude,
lat,
height
)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod observatories_test {
use super::*;
#[test]
fn test_observatories() {
let mut observatories = Observatories::new();
let obs = observatories.create_observer(1.0, 2.0, 3.0, Some("Test".to_string()));
assert_eq!(obs.longitude, 1.0);
assert_eq!(obs.rho_cos_phi, 0.999395371426802);
assert_eq!(obs.rho_sin_phi, 0.0346660237964843);
assert_eq!(obs.name, Some("Test".to_string()));
assert_eq!(observatories.obs_to_uint16.len(), 1);
let observer = observatories.get_observer_from_uint16(0);
assert_eq!(observer.name, Some("Test".to_string()));
observatories.create_observer(4.0, 5.0, 6.0, Some("Test2".to_string()));
assert_eq!(observatories.obs_to_uint16.len(), 2);
let observer = observatories.get_observer_from_uint16(1);
assert_eq!(observer.name, Some("Test2".to_string()));
}
#[cfg(test)]
mod observatories_display_tests {
use super::*;
#[test]
fn display_user_defined_only() {
let mut obs = Observatories::new();
obs.create_observer(10.0, 0.0, 0.0, Some("UserA".to_string()));
obs.create_observer(20.0, 45.0, 2.0, Some("UserB".to_string()));
let s = format!("{obs}");
assert!(
s.starts_with("User-defined observers:\n"),
"Missing 'User-defined observers:' header. Got:\n{s}"
);
assert!(
s.contains("UserA (lon: 10.000000°"),
"Missing formatted line for UserA. Got:\n{s}"
);
assert!(
s.contains("UserB (lon: 20.000000°"),
"Missing formatted line for UserB. Got:\n{s}"
);
assert!(
!s.contains("MPC observers:"),
"Unexpected 'MPC observers:' section when OnceLock is unset. Got:\n{s}"
);
}
#[test]
fn display_includes_mpc_section_when_set() {
let mut obs = Observatories::new();
obs.create_observer(0.0, 0.0, 0.0, Some("UserOnly".to_string()));
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: MpcCodeObs = Default::default();
use std::sync::Arc;
mpc_table.insert("I41".to_string(), Arc::new(mpc_site));
obs.mpc_code_obs
.set(mpc_table)
.expect("OnceLock<MpcCodeObs> was already initialized");
let s = format!("{obs}");
assert!(
s.contains("MPC observers:"),
"Missing 'MPC observers:' header after setting OnceLock. Got:\n{s}"
);
assert!(
s.contains("[I41]"),
"Missing MPC code tag '[I41]' in output. Got:\n{s}"
);
}
}
}