use strum_macros::EnumString;
use thiserror::Error;
use crate::datetime::{parse_datetime, ParseDateTimeError};
use gnss::constellation::Constellation;
pub mod description;
pub mod header;
#[derive(Debug, PartialEq, Clone)]
pub enum TimeSystem {
GNSS(Constellation),
UTC,
TAI,
}
#[derive(Debug, Error)]
pub enum TimeSystemError {
#[error("unknown time system \"{0}\"")]
UnknownSystem(String),
}
impl std::str::FromStr for TimeSystem {
type Err = TimeSystemError;
fn from_str(content: &str) -> Result<Self, Self::Err> {
if content.eq("UTC") {
Ok(Self::UTC)
} else if content.eq("TAI") {
Ok(Self::TAI)
} else if let Ok(c) = Constellation::from_str(content) {
Ok(Self::GNSS(c))
} else {
Err(TimeSystemError::UnknownSystem(content.to_string()))
}
}
}
impl Default for TimeSystem {
fn default() -> Self {
Self::UTC
}
}
#[derive(Debug, Error)]
pub enum DeterminationMethodError {
#[error("unknown determination method \"{0}\"")]
UnknownMethod(String),
}
#[derive(Debug, PartialEq, Clone)]
pub enum DeterminationMethod {
IntraFrequencyEstimation,
InterFrequencyEstimation,
ClockAnalysis,
IonosphereAnalysis,
CombinedAnalysis,
}
impl std::str::FromStr for DeterminationMethod {
type Err = DeterminationMethodError;
fn from_str(content: &str) -> Result<Self, Self::Err> {
if content.eq("CLOCK_ANALYSIS") {
Ok(Self::ClockAnalysis)
} else if content.eq("INTRA-FREQUENCY_BIAS_ESTIMATION") {
Ok(Self::IntraFrequencyEstimation)
} else if content.eq("INTER-FREQUENCY_BIAS_ESTIMATION") {
Ok(Self::InterFrequencyEstimation)
} else if content.eq("IONOSPHERE_ANALYSIS") {
Ok(Self::IonosphereAnalysis)
} else if content.eq("COMBINED_ANALYSIS") {
Ok(Self::CombinedAnalysis)
} else {
Err(DeterminationMethodError::UnknownMethod(content.to_string()))
}
}
}
#[derive(Debug, PartialEq, Clone, EnumString)]
pub enum BiasType {
DSB,
ISB,
OSB,
}
#[derive(Debug, Error)]
pub enum SolutionParsingError {
#[error("failed to parse BiasType")]
ParseBiasTypeError(#[from] strum::ParseError),
#[error("failed to parse bias estimate")]
ParseFloatError(#[from] std::num::ParseFloatError),
#[error("failed to parse datetime field")]
ParseDateTimeError(#[from] ParseDateTimeError),
}
#[derive(Debug, Clone)]
pub struct Solution {
pub btype: BiasType,
pub svn: String,
pub prn: String,
pub station: Option<String>,
pub obs: (String, Option<String>),
pub start_time: chrono::NaiveDateTime,
pub end_time: chrono::NaiveDateTime,
pub unit: String,
pub estimate: f64,
pub stddev: f64,
pub slope: Option<f64>,
pub slope_stddev: Option<f64>,
}
impl std::str::FromStr for Solution {
type Err = SolutionParsingError;
fn from_str(content: &str) -> Result<Self, Self::Err> {
let (bias_type, rem) = content.split_at(5);
let (svn, rem) = rem.split_at(5);
let (prn, rem) = rem.split_at(4);
let (station, rem) = rem.split_at(10);
let (obs1, rem) = rem.split_at(5);
let (obs2, rem) = rem.split_at(5);
let (start_time, rem) = rem.split_at(15);
let (end_time, rem) = rem.split_at(15);
let (unit, rem) = rem.split_at(5);
let (estimate, rem) = rem.split_at(22);
let (stddev, _) = rem.split_at(12);
Ok(Solution {
btype: BiasType::from_str(bias_type.trim())?,
svn: svn.trim().to_string(),
prn: prn.trim().to_string(),
station: {
if !station.trim().is_empty() {
Some(station.trim().to_string())
} else {
None
}
},
unit: unit.trim().to_string(),
start_time: parse_datetime(start_time.trim())?,
end_time: parse_datetime(end_time.trim())?,
obs: {
if !obs2.trim().is_empty() {
(obs1.trim().to_string(), Some(obs2.trim().to_string()))
} else {
(obs1.trim().to_string(), None)
}
},
estimate: f64::from_str(estimate.trim())?,
stddev: f64::from_str(stddev.trim())?,
slope: None,
slope_stddev: None,
})
}
}
impl Solution {
pub fn duration(&self) -> chrono::TimeDelta {
self.end_time - self.start_time
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Sinex;
use gnss::constellation::Constellation;
use std::str::FromStr;
#[test]
fn test_determination_methods() {
let method = DeterminationMethod::from_str("COMBINED_ANALYSIS");
assert!(method.is_ok());
assert_eq!(method.unwrap(), DeterminationMethod::CombinedAnalysis);
}
#[test]
fn test_solution_parser() {
let solution = Solution::from_str(
"ISB G G GIEN C1W C2W 2011:113:86385 2011:115:00285 ns 0.000000000000000E+00 .000000E+00");
assert!(solution.is_ok());
let solution = solution.unwrap();
assert_eq!(solution.btype, BiasType::ISB);
assert_eq!(solution.svn, "G");
assert_eq!(solution.prn, "G");
assert_eq!(solution.station, Some(String::from("GIEN")));
assert_eq!(
solution.obs,
(String::from("C1W"), Some(String::from("C2W")))
);
assert_eq!(solution.estimate, 0.0);
assert_eq!(solution.stddev, 0.0);
let solution = Solution::from_str(
"ISB E E GOUS C1C C7Q 2011:113:86385 2011:115:00285 ns -.101593337222667E+03 .259439E+02");
assert!(solution.is_ok());
let solution = solution.unwrap();
assert_eq!(solution.btype, BiasType::ISB);
assert_eq!(solution.svn, "E");
assert_eq!(solution.prn, "E");
assert_eq!(solution.station, Some(String::from("GOUS")));
assert_eq!(
solution.obs,
(String::from("C1C"), Some(String::from("C7Q")))
);
assert!((solution.estimate - -0.101593337222667E3) < 1E-6);
assert!((solution.stddev - 0.259439E+02) < 1E-6);
let solution = Solution::from_str(
"OSB G063 G01 C1C 2016:296:00000 2016:333:00000 ns 10.2472 0.0062");
assert!(solution.is_ok());
let solution = solution.unwrap();
assert_eq!(solution.btype, BiasType::OSB);
assert_eq!(solution.svn, "G063");
assert_eq!(solution.prn, "G01");
assert_eq!(solution.station, None);
assert_eq!(solution.obs, (String::from("C1C"), None));
assert!((solution.estimate - 10.2472) < 1E-4);
assert!((solution.stddev - 0.0062E+02) < 1E-4);
}
#[test]
fn test_bia_v1_example1() {
let file = env!("CARGO_MANIFEST_DIR").to_owned() + "/data/BIA/V1/example-1a.bia";
let sinex = Sinex::from_file(&file);
assert!(sinex.is_ok());
let sinex = sinex.unwrap();
let reference = &sinex.reference;
assert_eq!(
reference.description,
"CODE, Astronomical Institute, University of Bern"
);
assert_eq!(
reference.input,
"CODE IGS 1-day final and rapid bias solutions for G/R"
);
assert_eq!(
reference.output,
"CODE IGS 30-day bias solution for G/R satellites"
);
assert_eq!(reference.contact, "code@aiub.unibe.ch");
assert_eq!(reference.software, "Bernese GNSS Software Version 5.3");
assert_eq!(reference.hardware, "UBELIX: Linux, x86_64");
assert_eq!(sinex.acknowledgments.len(), 2);
assert_eq!(
sinex.acknowledgments[0],
"COD Center for Orbit Determination in Europe, AIUB, Switzerland"
);
assert_eq!(sinex.acknowledgments[1], "IGS International GNSS Service");
assert_eq!(sinex.comments.len(), 4);
assert_eq!(sinex.comments[0], "CODE final product series for the IGS.");
assert_eq!(
sinex.comments[1],
"Published by Astronomical Institute, University of Bern."
);
assert_eq!(
sinex.comments[2],
"URL: http://www.aiub.unibe.ch/download/CODE"
);
assert_eq!(sinex.comments[3], "DOI: 10.7892/boris.75876");
let description = &sinex.description;
let description = description.bias_description();
assert!(description.is_some());
let description = description.unwrap();
assert_eq!(description.sampling, Some(300));
assert_eq!(description.spacing, Some(86400));
assert_eq!(
description.method,
Some(DeterminationMethod::CombinedAnalysis)
);
assert_eq!(description.bias_mode, header::BiasMode::Absolute);
assert_eq!(description.system, TimeSystem::GNSS(Constellation::GPS));
assert_eq!(description.rcvr_clock_ref, None);
assert_eq!(description.sat_clock_ref.len(), 2);
let solutions = sinex.record.bias_solutions();
assert!(solutions.is_some());
let solutions = solutions.unwrap();
assert_eq!(solutions.len(), 50);
}
#[test]
fn test_bia_v1_example1b() {
let file = env!("CARGO_MANIFEST_DIR").to_owned() + "/data/BIA/V1/example-1b.bia";
let sinex = Sinex::from_file(&file);
assert!(sinex.is_ok());
let sinex = sinex.unwrap();
assert_eq!(sinex.acknowledgments.len(), 2);
assert_eq!(
sinex.acknowledgments[0],
"COD Center for Orbit Determination in Europe, AIUB, Switzerland"
);
assert_eq!(sinex.acknowledgments[1], "IGS International GNSS Service");
let _reference = &sinex.reference;
let description = &sinex.description;
let description = description.bias_description();
assert!(description.is_some());
let description = description.unwrap();
assert_eq!(description.sampling, Some(300));
assert_eq!(description.spacing, Some(86400));
assert_eq!(
description.method,
Some(DeterminationMethod::CombinedAnalysis)
);
assert_eq!(description.bias_mode, header::BiasMode::Relative);
assert_eq!(description.system, TimeSystem::GNSS(Constellation::GPS));
assert_eq!(description.rcvr_clock_ref, None);
assert_eq!(description.sat_clock_ref.len(), 2);
let solutions = sinex.record.bias_solutions();
assert!(solutions.is_some());
let solutions = solutions.unwrap();
assert_eq!(solutions.len(), 50);
for sol in solutions.iter() {
let obs = &sol.obs;
assert!(obs.1.is_some()); }
}
}