use std::ops::Range;
use camino::Utf8Path;
use thiserror::Error;
use crate::{
constants::Observations,
conversion::{parse_dec_to_deg, parse_ra_to_deg},
observations::Observation,
time::frac_date_to_mjd,
ObjectNumber, Outfit, OutfitError, RADH, RADSEC,
};
#[derive(Error, Debug, PartialEq)]
pub enum ParseObsError {
#[error("The line is too short")]
TooShortLine,
#[error("The line is not a CCD observation")]
NotCCDObs,
#[error("Error parsing RA: {0}")]
InvalidRA(String),
#[error("Invalid Dec value: {0}")]
InvalidDec(String),
#[error("Invalid date: {0}")]
InvalidDate(String),
}
fn from_80col(env_state: &mut Outfit, line: &str) -> Result<Observation, OutfitError> {
if line.len() < 80 {
return Err(OutfitError::Parsing80ColumnFileError(
ParseObsError::TooShortLine,
));
}
if line.chars().nth(14) == Some('s') {
return Err(OutfitError::Parsing80ColumnFileError(
ParseObsError::NotCCDObs,
));
}
let (ra, error_ra) = parse_ra_to_deg(line[32..44].trim()).ok_or_else(|| {
OutfitError::Parsing80ColumnFileError(ParseObsError::InvalidRA(
line[32..44].trim().to_string(),
))
})?;
let (dec, error_dec) = parse_dec_to_deg(line[44..56].trim()).ok_or_else(|| {
OutfitError::Parsing80ColumnFileError(ParseObsError::InvalidDec(
line[44..56].trim().to_string(),
))
})?;
let time = frac_date_to_mjd(line[15..32].trim()).map_err(|_| {
OutfitError::Parsing80ColumnFileError(ParseObsError::InvalidDate(
line[15..32].trim().to_string(),
))
})?;
let observer_id = env_state.uint16_from_mpc_code(&line[77..80].trim().into());
let observer = env_state.get_observer_from_uint16(observer_id);
let max_rms = |observation_error: f64, observer_error: f64, factor: f64| {
f64::max(observation_error, observer_error * factor)
};
let dec_radians = dec.to_radians();
let dec_rad_cos = dec_radians.cos();
let ra_error = max_rms(
(error_ra * RADH) / dec_rad_cos,
observer.ra_accuracy.map(|v| v.into_inner()).unwrap_or(0.0),
RADSEC / dec_rad_cos,
);
let dec_error = max_rms(
error_dec.to_radians(),
observer.dec_accuracy.map(|v| v.into_inner()).unwrap_or(0.0),
RADSEC,
);
let observation = Observation::new(
env_state,
observer_id,
ra.to_radians(),
ra_error,
dec_radians,
dec_error,
time,
)?;
Ok(observation)
}
pub(crate) fn extract_80col(
env_state: &mut Outfit,
colfile: &Utf8Path,
) -> Result<(Observations, ObjectNumber), OutfitError> {
let file_content = std::fs::read_to_string(colfile)
.unwrap_or_else(|_| panic!("Could not read file {}", colfile.as_str()));
let first_line = file_content
.lines()
.next()
.unwrap_or_else(|| panic!("Could not read first line of file {}", colfile.as_str()));
fn get_object_number(line: &str, range: Range<usize>) -> String {
line[range].trim_start_matches('0').trim().to_string()
}
let mut object_number = get_object_number(first_line, 0..5);
if object_number.is_empty() {
object_number = get_object_number(first_line, 5..12);
}
Ok((
file_content
.lines()
.filter_map(|line| match from_80col(env_state, line) {
Ok(obs) => Some(obs),
Err(OutfitError::Parsing80ColumnFileError(ParseObsError::NotCCDObs)) => None,
Err(e) => panic!("Error parsing line: {e:?}"),
})
.collect(),
ObjectNumber::String(object_number),
))
}
#[cfg(test)]
#[cfg(feature = "jpl-download")]
mod mpc_80col_test {
use super::*;
#[test]
fn test_from_80col_valid_line() {
use crate::unit_test_global::OUTFIT_HORIZON_TEST;
let line =
" K09R05F C2009 09 15.23433 22 52 22.62 -14 47 03.2 20.8 Vr~097wG96";
let mut env_state = OUTFIT_HORIZON_TEST.0.clone();
let result = from_80col(&mut env_state, line);
assert!(result.is_ok());
let obs = result.unwrap();
assert_eq!(
obs,
Observation {
observer: 0,
ra: 5.988124307160555,
error_ra: 1.2535340843609459e-6,
dec: -0.25803335512429054,
error_dec: 1.0181086985431635e-6,
time: 55089.23509601851,
observer_earth_position: [
3.0499942822953885e-5,
-8.594304778250371e-6,
2.8491013919142154e-5
]
.into(),
observer_helio_position: [
0.9968138444702415,
-0.12221921296802639,
-0.05295724448160355
]
.into(),
}
);
}
#[test]
fn test_from_80col_too_short_line() {
use crate::unit_test_global::OUTFIT_HORIZON_TEST;
let line = "short line";
let mut env_state = OUTFIT_HORIZON_TEST.0.clone();
let result = from_80col(&mut env_state, line);
assert!(matches!(
result,
Err(OutfitError::Parsing80ColumnFileError(
ParseObsError::TooShortLine
))
));
}
#[test]
fn test_from_80col_invalid_date() {
use crate::unit_test_global::OUTFIT_HORIZON_TEST;
let line =
" K09R05F C20xx 09 15.23433 22 52 22.62 -14 47 03.2 20.8 Vr~097wG96";
let mut env_state = OUTFIT_HORIZON_TEST.0.clone();
let result = from_80col(&mut env_state, line);
assert!(matches!(
result,
Err(OutfitError::Parsing80ColumnFileError(
ParseObsError::InvalidDate(_)
))
));
}
#[test]
fn test_from_80col_invalid_ra_dec() {
use crate::unit_test_global::OUTFIT_HORIZON_TEST;
let line =
" K09R05F C2009 09 15.23433 XX YY ZZ.ZZ -AA BB CC.C 20.8 Vr~097wG96";
let mut env_state = OUTFIT_HORIZON_TEST.0.clone();
let result = from_80col(&mut env_state, line);
assert!(matches!(
result,
Err(OutfitError::Parsing80ColumnFileError(
ParseObsError::InvalidRA(_)
))
));
}
}