diff --git a/Cargo.toml b/Cargo.toml index 20e28b4..aed737e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,8 @@ cggtts = [ ] [dependencies.rinex] -version = "0.20" +git = "https://github.com/nav-solutions/rinex" +rev = "a10b41abb0ccd6b804dff6acd560992cdcd4c639" features = [ "qc", "processing", @@ -50,13 +51,13 @@ features = [ "ut1", "meteo", "clock", - "ionex", "antex", "serde", ] [dependencies.gnss-qc] -version = "0.4" +git = "https://github.com/nav-solutions/gnss-qc" +rev = "3ee5918a37bf2b18371bd18d9a40b2742004865f" features = [ "sp3", "navigation", @@ -64,12 +65,11 @@ features = [ ] [dependencies.gnss-rtk] -git = "https://github.com/rtk-rs/gnss-rtk" -rev = "5eb681c3f6f123d36b1c5b46cf27429bcf45f3f7" +git = "https://github.com/nav-solutions/gnss-rtk" +rev = "bf0914fb9134eef35a3ef73bc105d5ec28fa783b" optional = true features = [ "serde", - "embed_ephem" ] [dependencies] @@ -89,6 +89,6 @@ csv = { version = "1.3", optional = true } gnss-rs = { version = "2.4", features = ["serde"] } clap = { version = "4.4.13", features = ["derive", "color"] } hifitime = { version = "4.1", features = ["serde", "std"] } -anise = { version = "0.6", features = ["embed_ephem"] } +anise = { version = "0.5.3", features = ["embed_ephem"] } serde = { version = "1.0", default-features = false, features = ["derive"] } cggtts = { version = "4.3", features = ["serde", "scheduler", "tracker"], optional = true } diff --git a/README.md b/README.md index 9ecc66b..3489359 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ RINEX-Cli [![MRSV](https://img.shields.io/badge/MSRV-1.82.0-orange?style=for-the-badge)](https://github.com/rust-lang/rust/releases/tag/1.82.0) [![License](https://img.shields.io/badge/license-MPL_2.0-orange?style=for-the-badge&logo=mozilla)](https://github.com/rtk-rs/rinex-cli/blob/main/LICENSE) -`rinex-cli` is a command line tool to post process RINEX and SP3 files. +`rinex-cli` is a command line tool to post-process RINEX and SP3 files. Because RINEX and SP3 cover many applications, `rinex-cli` can be used for many applications. The most important being: @@ -22,8 +22,10 @@ The most important being: - generate high level reports - Synthesis - generate RINEX (and soon SP3) from provided products -- Post processed navigation (`ppp` mode) because it integrates a complete -PVT solver (on `ppp` feature only) +- post-processed navigation (on `ppp` feature only) because it integrates a complete + - RINEX-Cli integrates a complete [P.V.T solution solver](https://github.com/rtk-rs/gnss-rtk) + - absolute navigation with `ppp` command line + - differential navigation with `rtk` command line, using one ground station - CGGTTS solutions solver (`ppp --cggtts` mode) by combining the `ppp` **and** `cggtts` options
diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 9861ac3..3b1e35c 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -57,8 +57,8 @@ pub struct Context { pub workspace: Workspace, #[cfg(feature = "ppp")] - /// (RX) [Orbit] to use, whether is was automatically picked up, - /// or manually overwritten. + /// (RX) [Orbit] to be used in several proceeses. + /// Either automatically picked up, manually assigned or overwritten, or non-existing. pub rx_orbit: Option, } @@ -330,33 +330,6 @@ Otherwise it gets automatically picked up.")) } } - /// Returns individual input BASE STATION -d - pub fn base_station_directories(&self) -> Vec<&String> { - match self.matches.subcommand() { - Some(("rtk", submatches)) => { - if let Some(dir) = submatches.get_many::("dir") { - dir.collect() - } else { - Vec::new() - } - }, - _ => Vec::new(), - } - } - /// Returns individual input BASE STATION -fp - pub fn base_station_files(&self) -> Vec<&String> { - match self.matches.subcommand() { - Some(("rtk", submatches)) => { - if let Some(fp) = submatches.get_many::("fp") { - fp.collect() - } else { - Vec::new() - } - }, - _ => Vec::new(), - } - } - /// Returns list of preprocessing operations pub fn preprocessing(&self) -> Vec<&String> { if let Some(filters) = self.matches.get_many::("preprocessing") { diff --git a/src/cli/positioning.rs b/src/cli/positioning.rs index 1701498..629f46f 100644 --- a/src/cli/positioning.rs +++ b/src/cli/positioning.rs @@ -12,8 +12,8 @@ fn shared_args(cmd: Command) -> Command { .action(ArgAction::Append) .help("Position Solver configuration file (JSON). See --help.") .long_help(" -Refer to all our navigation demos (subfolder). -[https://docs.rs/gnss-rtk/latest/gnss_rtk/prelude/struct.Config.html] is the structure to represent in JSON. +Refer to all our navigation demos (subfolder) and example scripts. +[https://docs.rs/gnss-rtk/latest/gnss_rtk/prelude/struct.Config.html] is the structure to be descrined. ")); let cmd = cmd.next_help_heading("User / Rover Profile") @@ -21,15 +21,44 @@ Refer to all our navigation demos (subfolder). Arg::new("static") .long("static") .action(ArgAction::SetTrue) - .help("Define that the user (rover) is static. Antenna was held static for the entire session.")) - .arg( - Arg::new("clock-sigma") - .long("clock-sigma") - .action(ArgAction::Set) - .value_parser(value_parser!(f64)) - .required(false) - .help("Define the uncertainty/bias over next clock state prediction (in seconds). -Default value is 10ms (=low quality clock).") + .help("Define the rover as static, meaning, its antenna was held static for the entire session. +The default profile is \"pedestrian\" (very low velocity), which is not suited for very fast moving rovers.")) + .arg( + Arg::new("car") + .long("car") + .action(ArgAction::SetTrue) + .help("Define car profile (low velocity). +The default profile is \"pedestrian\" (very low velocity), which is not suited for very fast moving rovers.")) + .arg( + Arg::new("airplane") + .long("airplane") + .action(ArgAction::SetTrue) + .help("Define airplane profile (high velocity). +The default profile is \"pedestrian\" (very low velocity), which is not suited for very fast moving rovers.")) + .arg( + Arg::new("rocket") + .long("rocket") + .action(ArgAction::SetTrue) + .help("Define rocket profile (very high velocity). +The default profile is \"pedestrian\" (very low velocity), which is not suited for very fast moving rovers.")) + .arg( + Arg::new("quartz") + .long("quartz") + .action(ArgAction::SetTrue) + .help("Define quartz (rover clock) profile (very poor quality). +The default profile is Oscillator/OCXO.")) +.arg( + Arg::new("atomic") + .long("quartz") + .action(ArgAction::SetTrue) + .help("Define atomic (rover clock) profile (high quality, at the scale of a GNSS constellation). +The default profile is Oscillator/OCXO.")) +.arg( + Arg::new("h-maser") + .long("h-maser") + .action(ArgAction::SetTrue) + .help("Define Hydrogen MASER (rover clock) profile (ultra high quality, better than GNSS constellation). +The default profile is Oscillator/OCXO.") ); let cmd = cmd.next_help_heading("Solutions formating"); @@ -66,45 +95,6 @@ Default value is 10ms (=low quality clock).") ) }; - let cmd = cmd.next_help_heading("CGGTTS Post FIT"); - - let cmd = if cfg!(not(feature = "cggtts")) { - cmd.arg( - Arg::new("cggtts") - .long("cggtts") - .action(ArgAction::SetTrue) - .help("[NOT AVAILABLE] requires cggtts compilation option"), - ) - } else { - cmd - .arg(Arg::new("cggtts") - .long("cggtts") - .action(ArgAction::SetTrue) - .help("Activate CGGTTS special Post FIT")) - .arg(Arg::new("tracking") - .long("trk") - .value_parser(value_parser!(Duration)) - .action(ArgAction::Set) - .help("CGGTTS custom tracking duration. - Otherwise, the default tracking duration is used. Refer to [https://docs.rs/cggtts/latest/cggtts/track/struct.Scheduler.html].")) - .arg(Arg::new("lab") - .long("lab") - .action(ArgAction::Set) - .help("Define the name of your station or laboratory here.")) - .arg(Arg::new("utck") - .long("utck") - .action(ArgAction::Set) - .conflicts_with("clock") - .help("If the local clock tracks a local UTC replica, you can define the name - of this replica here.")) - .arg(Arg::new("clock") - .long("clk") - .action(ArgAction::Set) - .conflicts_with("utck") - .help("If the local clock is not a UTC replica and has a specific name, you - can define it here.")) - }; - cmd } @@ -112,7 +102,7 @@ pub fn ppp_subcommand() -> Command { let cmd = Command::new("ppp") .arg_required_else_help(false) .about( - "Post Processed Positioning. Use this mode to deploy the precise position solver. + "Post-processed PPP (absolute navigation). The solutions are added to the final report as an extra chapter. See --help", ) .long_about( @@ -120,7 +110,46 @@ The solutions are added to the final report as an extra chapter. See --help", PVT solutions from RINEX data sampled by a single receiver (! This is not RTK!). The solutions are presented in the analysis report (post processed results chapter). Use --cggtts to convert solutions to CGGTTS special format.", - ); + ) + .next_help_heading("CGGTTS Post FIT"); + + let cmd = if cfg!(not(feature = "cggtts")) { + cmd.arg( + Arg::new("cggtts") + .long("cggtts") + .action(ArgAction::SetTrue) + .help("[NOT AVAILABLE] requires cggtts compilation option"), + ) + } else { + cmd + .arg(Arg::new("cggtts") + .long("cggtts") + .action(ArgAction::SetTrue) + .help("Activate CGGTTS special Post FIT")) + .arg(Arg::new("tracking") + .long("trk") + .value_parser(value_parser!(Duration)) + .action(ArgAction::Set) + .help("CGGTTS custom tracking duration. + Otherwise, the default tracking duration is used. Refer to [https://docs.rs/cggtts/latest/cggtts/track/struct.Scheduler.html].")) + .arg(Arg::new("lab") + .long("lab") + .action(ArgAction::Set) + .help("Define the name of your station or laboratory here.")) + .arg(Arg::new("utck") + .long("utck") + .action(ArgAction::Set) + .conflicts_with("clock") + .help("If the local clock tracks a local UTC replica, you can define the name + of this replica here.")) + .arg(Arg::new("clock") + .long("clk") + .action(ArgAction::Set) + .conflicts_with("utck") + .help("If the local clock is not a UTC replica and has a specific name, you + can define it here.")) + }; + shared_args(cmd) } @@ -128,16 +157,17 @@ pub fn rtk_subcommand() -> Command { let cmd = Command::new("rtk") .arg_required_else_help(true) .about( - "Post Processed RTK. Use this mode to deploy the precise differential positioning. -The initial context describes the Rover context. rtk accepts `-f` and `-d` once again, to describe the remote Station. -Other positioning flags still apply (like -c). See --help.", + "Post-processed RTK (differential navigation). +The initial context describes the rover context. +Use rtk --fp,-f to describe the base station, with at least one RINEX file (mandatory). +The RINEX file must describe the station position. +RINEX-Cli is limited to static base! moving base is not feasible. +The RTK solutions are added to the final report, as an extra chapter. See --help.", ) .long_about( "RTK post opmode resolves PVT solutions by (post processed) differential navigation. The initial context (-f, -d) describes the ROVER. -`rtk` also accepts -f and -d and you need to use those to describe the BASE (mandatory). -Other than that, `rtk` is stricly identical to `ppp` and is presented similarly. -CGGTTS and other options still apply." +`rtk` also accepts -f and -d and you need to use those to describe the base station (mandatory).", ) .arg( Arg::new("fp") @@ -145,7 +175,7 @@ CGGTTS and other options still apply." .value_name("FILE") .action(ArgAction::Append) .required_unless_present("dir") - .help("Pass any RINEX file for remote base station"), + .help("Base station Observation RINEX file(s), one at a time, as many as needed."), ) .arg( Arg::new("dir") @@ -153,7 +183,9 @@ CGGTTS and other options still apply." .value_name("DIR") .action(ArgAction::Append) .required_unless_present("fp") - .help("Pass any directory for remote base station"), + .help( + "Base station Observation RINEX directory, one at a time, as many as needed.", + ), ); shared_args(cmd) } diff --git a/src/fops/filegen.rs b/src/fops/filegen.rs index 9164df2..41cf38d 100644 --- a/src/fops/filegen.rs +++ b/src/fops/filegen.rs @@ -31,13 +31,11 @@ pub fn filegen(ctx: &Context, matches: &ArgMatches, submatches: &ArgMatches) -> fn write(ctx: &Context, matches: &ArgMatches, submatches: &ArgMatches) -> Result<(), Error> { let ctx_data = &ctx.data; for (product, dir) in [ - (ProductType::DORIS, "DORIS"), (ProductType::Observation, "OBSERVATIONS"), (ProductType::MeteoObservation, "METEO"), (ProductType::BroadcastNavigation, "BRDC"), (ProductType::HighPrecisionClock, "CLOCK"), (ProductType::HighPrecisionOrbit, "SP3"), - (ProductType::IONEX, "IONEX"), (ProductType::ANTEX, "ANTEX"), ] { if let Some(rinex) = ctx_data.rinex(product) { diff --git a/src/fops/merge.rs b/src/fops/merge.rs index 8320191..8b91745 100644 --- a/src/fops/merge.rs +++ b/src/fops/merge.rs @@ -64,14 +64,6 @@ pub fn merge(ctx: &Context, cli: &Cli, submatches: &ArgMatches) -> Result<(), Er rinex_a.merge(&rinex_b)?, ) }, - RinexType::IonosphereMaps => { - let rinex_a = ctx_data.ionex().ok_or(Error::MissingIONEX)?; - - ( - rinex_a.standard_filename(short_v2_name, None, None), - rinex_a.merge(&rinex_b)?, - ) - }, RinexType::ClockData => { let rinex_a = ctx_data.clock().ok_or(Error::MissingClockRinex)?; diff --git a/src/fops/split.rs b/src/fops/split.rs index f42e62c..b032efe 100644 --- a/src/fops/split.rs +++ b/src/fops/split.rs @@ -21,7 +21,6 @@ pub fn split(ctx: &Context, submatches: &ArgMatches) -> Result<(), Error> { ProductType::MeteoObservation, ProductType::BroadcastNavigation, ProductType::HighPrecisionClock, - ProductType::IONEX, ] { if let Some(rinex) = ctx_data.rinex(product) { let (rinex_a, rinex_b) = rinex.split(*split_instant); diff --git a/src/fops/tbin.rs b/src/fops/tbin.rs index 3bdd004..672a33b 100644 --- a/src/fops/tbin.rs +++ b/src/fops/tbin.rs @@ -28,8 +28,6 @@ pub fn time_binning( ctx.workspace.create_subdir("BATCH"); for product in [ - ProductType::IONEX, - ProductType::DORIS, ProductType::Observation, ProductType::MeteoObservation, ProductType::BroadcastNavigation, diff --git a/src/main.rs b/src/main.rs index b3e1bbb..8e178e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,7 +73,6 @@ fn user_data_parsing( single_files: Vec<&String>, directories: Vec<&String>, max_depth: usize, - is_rover: bool, ) -> QcContext { let mut ctx = QcContext::new(); @@ -178,18 +177,7 @@ fn user_data_parsing( // Preprocessing context_preprocessing(&mut ctx, cli); - match cli.matches.subcommand() { - Some(("rtk", _)) => { - if is_rover { - debug!("ROVER Dataset: {:?}", ctx); - } else { - error!("BASE STATION Dataset: {:?}", ctx); - } - }, - _ => { - debug!("{:?}", ctx); - }, - } + debug!("{:?}", ctx); ctx } @@ -216,7 +204,6 @@ pub fn main() -> Result<(), Error> { cli.rover_files(), cli.rover_directories(), max_recursive_depth, - true, ); let ctx_stem = Context::context_stem(&mut data_ctx); @@ -316,36 +303,44 @@ pub fn main() -> Result<(), Error> { fops::filegen(&ctx, &cli.matches, submatches)?; return Ok(()); }, + Some(("merge", submatches)) => { fops::merge(&ctx, &cli, submatches)?; return Ok(()); }, + Some(("split", submatches)) => { fops::split(&ctx, submatches)?; return Ok(()); }, + Some(("tbin", submatches)) => { fops::time_binning(&ctx, &cli.matches, submatches)?; return Ok(()); }, + Some(("cbin", submatches)) => { fops::constell_timescale_binning(&ctx, submatches)?; return Ok(()); }, + Some(("diff", submatches)) => { fops::diff(&ctx, &cli, submatches)?; return Ok(()); }, + #[cfg(feature = "ppp")] Some(("ppp", submatches)) => { - let chapter = positioning::precise_positioning(&cli, &ctx, false, submatches)?; + let chapter = positioning::precise_positioning(&ctx, false, submatches)?; extra_pages.push(chapter); }, + #[cfg(feature = "ppp")] Some(("rtk", submatches)) => { - let chapter = positioning::precise_positioning(&cli, &ctx, true, submatches)?; + let chapter = positioning::precise_positioning(&ctx, true, submatches)?; extra_pages.push(chapter); }, + _ => {}, } diff --git a/src/positioning/biases/environment.rs b/src/positioning/biases/environment.rs new file mode 100644 index 0000000..72f165b --- /dev/null +++ b/src/positioning/biases/environment.rs @@ -0,0 +1,142 @@ +use log::info; + +use gnss_rtk::prelude::{BiasRuntime, EnvironmentalBias, TroposphereModel}; + +pub struct EnvironmentalBiases {} + +impl EnvironmentalBias for EnvironmentalBiases { + fn ionosphere_bias_m(&self, _: &BiasRuntime) -> f64 { + // TODO + 0.0 + } + + fn troposphere_bias_m(&self, rtm: &BiasRuntime) -> f64 { + TroposphereModel::Niel.bias_m(rtm) + } +} + +//pub fn tropo_components(meteo: Option<&Rinex>, t: Epoch, lat_ddeg: f64) -> Option<(f64, f64)> { +// const MAX_LATDDEG_DELTA: f64 = 15.0; +// let max_dt = Duration::from_hours(24.0); +// let rnx = meteo?; +// let meteo = rnx.header.meteo.as_ref().unwrap(); +// +// let delays: Vec<(Observable, f64)> = meteo +// .sensors +// .iter() +// .filter_map(|s| match s.observable { +// Observable::ZenithDryDelay => { +// let (x, y, z, _) = s.position?; +// let (lat, _, _) = ecef2geodetic(x, y, z, Ellipsoid::WGS84); +// let lat = rad2deg(lat); +// if (lat - lat_ddeg).abs() < MAX_LATDDEG_DELTA { +// let value = rnx +// .zenith_dry_delay() +// .filter(|(t_sens, _)| (*t_sens - t).abs() < max_dt) +// .min_by_key(|(t_sens, _)| (*t_sens - t).abs()); +// let (_, value) = value?; +// debug!("{:?} lat={} zdd {}", t, lat_ddeg, value); +// Some((s.observable.clone(), value)) +// } else { +// None +// } +// }, +// Observable::ZenithWetDelay => { +// let (x, y, z, _) = s.position?; +// let (mut lat, _, _) = ecef2geodetic(x, y, z, Ellipsoid::WGS84); +// lat = rad2deg(lat); +// if (lat - lat_ddeg).abs() < MAX_LATDDEG_DELTA { +// let value = rnx +// .zenith_wet_delay() +// .filter(|(t_sens, _)| (*t_sens - t).abs() < max_dt) +// .min_by_key(|(t_sens, _)| (*t_sens - t).abs()); +// let (_, value) = value?; +// debug!("{:?} lat={} zdd {}", t, lat_ddeg, value); +// Some((s.observable.clone(), value)) +// } else { +// None +// } +// }, +// _ => None, +// }) +// .collect(); +// +// if delays.len() < 2 { +// None +// } else { +// let zdd = delays +// .iter() +// .filter_map(|(obs, value)| { +// if obs == &Observable::ZenithDryDelay { +// Some(*value) +// } else { +// None +// } +// }) +// .reduce(|k, _| k) +// .unwrap(); +// +// let zwd = delays +// .iter() +// .filter_map(|(obs, value)| { +// if obs == &Observable::ZenithWetDelay { +// Some(*value) +// } else { +// None +// } +// }) +// .reduce(|k, _| k) +// .unwrap(); +// +// Some((zwd, zdd)) +// } +//} + +// /* +// * Grabs nearest BD model (in time) +// */ +// pub fn bd_model(nav: &Rinex, t: Epoch) -> Option { +// let (_, model) = nav +// .nav_bdgim_models_iter() +// .min_by_key(|(k_i, _)| (k_i.epoch - t).abs())?; + +// Some(BdModel { alpha: model.alpha }) +// } + +// /* +// * Grabs nearest NG model (in time) +// */ +// pub fn ng_model(nav: &Rinex, t: Epoch) -> Option { +// let (_, model) = nav +// .nav_nequickg_models_iter() +// .min_by_key(|(k_i, _)| (k_i.epoch - t).abs())?; + +// Some(NgModel { a: model.a }) +// } + +// /// Returns a [KbModel] +// pub fn kb_model(nav: &Rinex, t: Epoch) -> Option { +// let (nav_key, model) = nav +// .nav_klobuchar_models_iter() +// .min_by_key(|(k_i, _)| (k_i.epoch - t).abs())?; +// +// Some(KbModel { +// h_km: { +// match nav_key.sv.constellation { +// Constellation::BeiDou => 375.0, +// // we only expect GPS or BDS here, +// // badly formed RINEX will generate errors in the solutions +// _ => 350.0, +// } +// }, +// alpha: model.alpha, +// beta: model.beta, +// }) +// } + +impl EnvironmentalBiases { + pub fn new() -> Self { + info!("environemental biases model created & deployed"); + Self {} + } +} diff --git a/src/positioning/biases/mod.rs b/src/positioning/biases/mod.rs new file mode 100644 index 0000000..3ee58ca --- /dev/null +++ b/src/positioning/biases/mod.rs @@ -0,0 +1,2 @@ +pub mod environment; +pub mod space; diff --git a/src/positioning/biases/space.rs b/src/positioning/biases/space.rs new file mode 100644 index 0000000..6c3614a --- /dev/null +++ b/src/positioning/biases/space.rs @@ -0,0 +1,53 @@ +use log::info; + +use std::cell::RefCell; +use std::rc::Rc; + +use gnss_rtk::prelude::{BiasRuntime, Duration, Epoch, SatelliteClockCorrection, SpacebornBias}; + +use crate::positioning::EphemerisBuffer; + +pub struct SpacebornBiases<'a> { + buffer: Rc>>, +} + +impl<'a> SpacebornBias for SpacebornBiases<'a> { + fn clock_bias(&self, rtm: &BiasRuntime) -> SatelliteClockCorrection { + if let Some(frm) = self.buffer.borrow().select_sv_ephemeris(rtm.epoch, rtm.sv) { + if let Some(dt) = frm + .ephemeris + .clock_correction(frm.toc, rtm.epoch, rtm.sv, 10) + { + SatelliteClockCorrection::without_relativistic_correction(dt) + } else { + Default::default() + } + } else { + Default::default() + } + } + + fn group_delay(&self, rtm: &BiasRuntime) -> Duration { + if let Some(frame) = self.buffer.borrow().select_sv_ephemeris(rtm.epoch, rtm.sv) { + if let Some(tgd) = frame.ephemeris.tgd() { + tgd + } else { + Duration::ZERO + } + } else { + Duration::ZERO + } + } + + fn mw_bias(&self, _: &BiasRuntime) -> f64 { + // MW bias not supplied nor supported yet + Default::default() + } +} + +impl<'a> SpacebornBiases<'a> { + pub fn new(buffer: Rc>>) -> Self { + info!("spaceborn biases created & deployed"); + Self { buffer } + } +} diff --git a/src/positioning/cggtts/mod.rs b/src/positioning/cggtts/mod.rs index 91527ed..1d92920 100644 --- a/src/positioning/cggtts/mod.rs +++ b/src/positioning/cggtts/mod.rs @@ -1,5 +1,8 @@ //! CGGTTS special resolution opmode. -use std::{cell::RefCell, collections::HashMap}; +use std::collections::HashMap; + +use std::cell::{RefCell, RefMut}; +use std::rc::Rc; mod post_process; pub use post_process::post_process; @@ -13,8 +16,9 @@ use rinex::{ }; use gnss_rtk::prelude::{ - AbsoluteTime, Bias, Candidate, Carrier as RTKCarrier, Method, Observation, OrbitSource, User, - PPP, SPEED_OF_LIGHT_M_S, + AbsoluteTime, Candidate, Carrier as RTKCarrier, ClockProfile, EnvironmentalBias, + EphemerisSource, Method, Observation, OrbitSource, Solver, SpacebornBias, UserParameters, + UserProfile, SPEED_OF_LIGHT_M_S, }; use cggtts::prelude::{ @@ -23,9 +27,7 @@ use cggtts::prelude::{ use crate::{ cli::Context, - positioning::{ - cast_rtk_carrier, ClockStateProvider, EphemerisSource, Error as PositioningError, - }, + positioning::{cast_rtk_carrier, EphemerisBuffer, Error as PositioningError}, }; fn rinex_ref_observable( @@ -82,13 +84,19 @@ fn rinex_ref_observable( } /// Resolves CGGTTS tracks from input context -pub fn resolve<'a, 'b, CK: ClockStateProvider, O: OrbitSource, B: Bias, T: AbsoluteTime>( +pub fn resolve< + 'a, + EPH: EphemerisSource, + ORB: OrbitSource, + EB: EnvironmentalBias, + SB: SpacebornBias, + TIM: AbsoluteTime, +>( ctx: &Context, - eph: &'a RefCell>, - user_profile: User, - mut clock: CK, - mut solver: PPP, method: Method, + params: UserParameters, + mut solver: Solver, + ephemeris_buffer: Rc>>, ) -> Result, PositioningError> { let obs_data = ctx .data @@ -128,27 +136,15 @@ pub fn resolve<'a, 'b, CK: ClockStateProvider, O: OrbitSource, B: Bias, T: Absol let mut release = false; - for (index, (t, signal)) in obs_data.signal_observations_sampling_ok_iter().enumerate() { - if index > 0 && t > past_t { + for (index, (epoch, signal)) in obs_data.signal_observations_sampling_ok_iter().enumerate() { + ephemeris_buffer.borrow_mut().new_epoch(epoch); + + if index > 0 && epoch > past_t { if collecting { info!("{} - new epoch", past_t); for (sv, observations) in sv_observations.iter() { // create new candidate let mut cd = Candidate::new(*sv, past_t, observations.clone()); - - // fixup and customizations - match clock.next_clock_at(past_t, *sv) { - Some(dt) => cd.set_clock_correction(dt), - None => error!("{} ({}) - no clock correction available", past_t, *sv), - } - - if let Some((_, _, eph)) = eph.borrow_mut().select(past_t, *sv) { - if let Some(tgd) = eph.tgd() { - debug!("{} ({}) - tgd: {}", past_t, *sv, tgd); - cd.set_group_delay(tgd); - } - } - candidates.push(cd); } @@ -164,7 +160,7 @@ pub fn resolve<'a, 'b, CK: ClockStateProvider, O: OrbitSource, B: Bias, T: Absol } } - match solver.resolve(user_profile, past_t, &candidates) { + match solver.ppp(past_t, params, &candidates) { Ok(pvt) => { for sv_contrib in pvt.sv.iter() { let (azim_deg, elev_deg) = @@ -247,11 +243,12 @@ pub fn resolve<'a, 'b, CK: ClockStateProvider, O: OrbitSource, B: Bias, T: Absol if carrier.is_err() { error!( "{}({}/{}) - unknown signal {:?}", - t, + epoch, signal.sv.constellation, signal.observable, carrier.err().unwrap() ); + continue; } @@ -262,11 +259,12 @@ pub fn resolve<'a, 'b, CK: ClockStateProvider, O: OrbitSource, B: Bias, T: Absol if rtk_carrier.is_err() { error!( "{}({}/{}) - unknown frequency: {}", - t, + epoch, signal.sv.constellation, signal.observable, rtk_carrier.err().unwrap() ); + continue; } @@ -345,9 +343,9 @@ pub fn resolve<'a, 'b, CK: ClockStateProvider, O: OrbitSource, B: Bias, T: Absol } // update only on new epochs - if index > 0 && t > past_t { + if index > 0 && epoch > past_t { if collecting { - if t > next_period_start { + if epoch > next_period_start { release = true; // end of period: release attempt for ((sv, sv_ref), tracker) in trackers.iter_mut() { @@ -375,19 +373,31 @@ pub fn resolve<'a, 'b, CK: ClockStateProvider, O: OrbitSource, B: Bias, T: Absol } } } else { - info!("{} - {} until CGGTTS release", t, next_period_start - t); + info!( + "{} - {} until CGGTTS release", + epoch, + next_period_start - epoch + ); } } else { // not collecting - if t >= next_collection_start { - next_period_start = cv_calendar.next_period_start_after(t); + if epoch >= next_collection_start { + next_period_start = cv_calendar.next_period_start_after(epoch); collecting = true; - debug!("{} - CGGTTS tracking started", t); - info!("{} - {} until CGGTTS release", t, next_period_start - t); + debug!("{} - CGGTTS tracking started", epoch); + info!( + "{} - {} until CGGTTS release", + epoch, + next_period_start - epoch + ); } if !collecting { - debug!("{} - {} until next tracking", t, next_collection_start - t); + debug!( + "{} - {} until next tracking", + epoch, + next_collection_start - epoch + ); } } } @@ -395,20 +405,28 @@ pub fn resolve<'a, 'b, CK: ClockStateProvider, O: OrbitSource, B: Bias, T: Absol if release { // reset release = false; - next_period_start = cv_calendar.next_period_start_after(t); + next_period_start = cv_calendar.next_period_start_after(epoch); next_collection_start = cv_calendar.next_data_collection_after(past_t); - collecting = t > next_collection_start; + collecting = epoch > next_collection_start; if collecting { - debug!("{} - CGGTTS tracking started", t); + debug!("{} - CGGTTS tracking started", epoch); } else { - debug!("{} - {} until next tracking", t, next_collection_start - t); + debug!( + "{} - {} until next tracking", + epoch, + next_collection_start - epoch + ); } - info!("{} - {} until CGGTTS release", t, next_period_start - t); + info!( + "{} - {} until CGGTTS release", + epoch, + next_period_start - epoch + ); } - past_t = t; + past_t = epoch; } Ok(tracks) diff --git a/src/positioning/clock.rs b/src/positioning/clock.rs index ead25f5..c566fb9 100644 --- a/src/positioning/clock.rs +++ b/src/positioning/clock.rs @@ -5,11 +5,7 @@ use crate::{ use std::{cell::RefCell, collections::HashMap}; -use gnss_rtk::prelude::{ClockCorrection, Duration, Epoch, SV}; - -pub trait ClockStateProvider { - fn next_clock_at(&mut self, t: Epoch, sv: SV) -> Option; -} +use gnss_rtk::prelude::{Duration, Epoch, SatelliteClockCorrection, SV}; impl CenteredDataPoints for f64 { fn zero() -> f64 { diff --git a/src/positioning/eph.rs b/src/positioning/eph.rs deleted file mode 100644 index 7aafaf6..0000000 --- a/src/positioning/eph.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::cli::Context; -use rinex::navigation::Ephemeris; -use rinex::prelude::{Epoch, SV}; -use std::collections::HashMap; - -pub struct EphemerisSource<'a> { - sv: SV, - eos: bool, - toc: Epoch, - buffer: HashMap>, - iter: Box + 'a>, -} - -impl<'a> EphemerisSource<'a> { - /// Builds new [EphemerisSource] from [Context] - pub fn from_ctx(ctx: &'a Context) -> Self { - let brdc = ctx - .data - .brdc_navigation() - .expect("Navigation RINEX is currently mandatory.."); - - info!("Ephemeris data source created."); - - let mut s = Self { - eos: false, - sv: SV::default(), - toc: Epoch::default(), - buffer: HashMap::with_capacity(32), - iter: Box::new(brdc.nav_ephemeris_frames_iter().filter_map(|(k, v)| { - let toe = v.toe(k.sv)?; - Some((k.sv, k.epoch, toe, v)) - })), - }; - - s.consume_many(32); // fill in with some data - s - } - - /// Consume one entry from [Iterator] - fn consume_one(&mut self) { - if let Some((sv, toc, toe, eph)) = self.iter.next() { - if let Some(buffer) = self.buffer.get_mut(&sv) { - buffer.push((toc, toe, eph.clone())); - } else { - self.buffer.insert(sv, vec![(toc, toe, eph.clone())]); - } - self.sv = sv; - self.toc = toc; - } else { - if !self.eos { - info!("{}({}): consumed all epochs", self.toc, self.sv); - } - self.eos = true; - } - } - - /// Consume n entries from [Iterator] - fn consume_many(&mut self, n: usize) { - for _ in 0..n { - self.consume_one(); - } - } - - /// [Ephemeris] selection attempt, for [SV] at [Epoch] - fn try_select(&self, t: Epoch, sv: SV) -> Option<(Epoch, Epoch, &Ephemeris)> { - let buffer = self.buffer.get(&sv)?; - - if sv.constellation.is_sbas() { - buffer - .iter() - .filter_map(|(toc_i, toe_i, eph_i)| { - if t >= *toc_i { - Some((*toc_i, *toe_i, eph_i)) - } else { - None - } - }) - .min_by_key(|(toc_i, _, _)| (t - *toc_i).abs()) - } else { - buffer - .iter() - .filter_map(|(toc_i, toe_i, eph_i)| { - if eph_i.is_valid(sv, t) { - Some((*toc_i, *toe_i, eph_i)) - } else { - None - } - }) - .min_by_key(|(_, toe_i, _)| (t - *toe_i).abs()) - } - } - - /// [Ephemeris] selection at [Epoch] for [SV]. - pub fn select(&mut self, t: Epoch, sv: SV) -> Option<(Epoch, Epoch, Ephemeris)> { - loop { - if let Some((toc_i, toe_i, eph_i)) = self.try_select(t, sv) { - return Some((toc_i, toe_i, eph_i.clone())); - } else { - self.consume_one(); - if self.eos { - return None; - } - } - } - } -} diff --git a/src/positioning/ephemeris.rs b/src/positioning/ephemeris.rs new file mode 100644 index 0000000..4c5dee0 --- /dev/null +++ b/src/positioning/ephemeris.rs @@ -0,0 +1,122 @@ +use gnss_rtk::prelude::{Ephemeris as RawEphemeris, EphemerisSource, Epoch, SV}; + +use rinex::navigation::Ephemeris; + +use crate::cli::Context; + +pub struct NullEphemerisSource {} + +impl NullEphemerisSource { + pub fn new() -> Self { + Self {} + } +} + +impl EphemerisSource for NullEphemerisSource { + // Ephemeris interface is not used by this application + fn ephemeris_data(&self, _: Epoch, _: SV) -> Option { + None + } +} + +#[derive(Clone)] +pub struct EphemerisFrame { + /// [SV] + pub sv: SV, + + /// ToC as [Epoch] + pub toc: Epoch, + + /// ToE as [Epoch] + pub toe: Epoch, + + /// [Ephemeris] + pub ephemeris: Ephemeris, +} + +pub struct EphemerisBuffer<'a> { + /// End of ephemeris Stream + eos: bool, + + /// buffer + buffer: Vec, + + /// Iterator + iter: Box + 'a>, +} + +impl<'a> EphemerisBuffer<'a> { + pub fn new_epoch(&mut self, epoch: Epoch) { + // discard past invalid + self.buffer.retain(|frm| { + if frm.toe < epoch { + frm.ephemeris.is_valid(frm.sv, epoch) + } else { + true + } + }); + + // iterate until EoS or frame becomes future + invalid + loop { + if self.eos { + break; + } + + if let Some(frame) = self.iter.next() { + if frame.toe > epoch && !frame.ephemeris.is_valid(frame.sv, epoch) { + self.buffer.push(frame); + break; + } + + self.buffer.push(frame); + } else { + self.eos = true; + } + } + } + + pub fn new(ctx: &'a Context) -> Self { + let brdc = ctx + .data + .brdc_navigation() + .expect("Navigation RINEX is required by post-processed navigation"); + + let s = Self { + eos: false, + buffer: Vec::with_capacity(16), + iter: Box::new(brdc.nav_ephemeris_frames_iter().filter_map(|(k, v)| { + if let Some(toe) = v.toe(k.sv) { + Some(EphemerisFrame { + toe, + sv: k.sv, + toc: k.epoch, + ephemeris: v.clone(), + }) + } else { + error!( + "{}({}) - ephemeris error (non supported constellation?)", + k.epoch, k.sv + ); + None + } + })), + }; + + info!("Ephemeris buffer created"); + + s + } + + pub fn select_sv_ephemeris(&self, epoch: Epoch, sv: SV) -> Option { + if sv.constellation.is_sbas() { + error!("{} - sbas not supported yet!", epoch); + None + } else { + self.buffer + .iter() + .filter(|frm| frm.sv == sv && frm.ephemeris.is_valid(sv, epoch)) + .min_by_key(|frm| (epoch - frm.toe).abs()) + .cloned() + } + } +} diff --git a/src/positioning/mod.rs b/src/positioning/mod.rs index a098ddb..4d01083 100644 --- a/src/positioning/mod.rs +++ b/src/positioning/mod.rs @@ -1,73 +1,57 @@ -use crate::cli::{Cli, Context}; -use clap::ArgMatches; -use std::cell::RefCell; -use std::fs::read_to_string; - mod buffer; -pub use buffer::Buffer; - +mod coords; +mod orbit; +mod precise; mod snapshot; -pub use snapshot::{CenteredDataPoints, CenteredSnapshot}; +mod time; +// mod clock; +mod biases; +mod ephemeris; +mod ppp; // precise point positioning +mod rtk; // RTK positioning -mod eph; -use eph::EphemerisSource; +#[cfg(feature = "cggtts")] +mod cggtts; // CGGTTS special solver -mod precise; -use precise::PreciseOrbits; +pub use buffer::Buffer; +pub use coords::Coords3d; +pub use snapshot::{CenteredDataPoints, CenteredSnapshot}; -mod time; +use orbit::Orbits; +use precise::PreciseOrbits; use time::Time; -mod ppp; // precise point positioning +use biases::{environment::EnvironmentalBiases, space::SpacebornBiases}; + +use ephemeris::{EphemerisBuffer, NullEphemerisSource}; + use ppp::{ post_process::{post_process as ppp_post_process, Error as PPPPostError}, Report as PPPReport, }; -#[cfg(feature = "cggtts")] -mod cggtts; // CGGTTS special solver +use rtk::RTKBaseStation; #[cfg(feature = "cggtts")] use cggtts::{post_process as cggtts_post_process, Report as CggttsReport}; -// mod rtk; -// pub use rtk::RemoteRTKReference; +use log::error; -mod orbit; -use orbit::Orbits; - -mod coords; -pub use coords::Coords3d; - -mod clock; -use clock::Clock; -pub use clock::ClockStateProvider; - -use rinex::{ - carrier::Carrier, - prelude::{Constellation, Rinex}, -}; +use rinex::{carrier::Carrier, prelude::Rinex}; use gnss_qc::prelude::QcExtraPage; use gnss_rtk::prelude::{ - Bias, BiasRuntime, Carrier as RTKCarrier, Config, Duration, Epoch, Error as RTKError, KbModel, - Method, Profile, TroposphereModel, User, PPP, + Carrier as RTKCarrier, ClockProfile, Config, Duration, Error as RTKError, Method, Rc, Solver, + UserParameters, UserProfile, }; use thiserror::Error; -struct BiasModel {} - -impl Bias for BiasModel { - fn ionosphere_bias_m(&self, _: &BiasRuntime) -> f64 { - 0.0 - } - - fn troposphere_bias_m(&self, rtm: &BiasRuntime) -> f64 { - TroposphereModel::Niel.bias_m(rtm) - } -} +use crate::cli::{Cli, Context}; +use clap::ArgMatches; +use std::cell::RefCell; +use std::fs::read_to_string; #[derive(Debug, Error)] pub enum Error { @@ -88,158 +72,32 @@ pub fn rtk_carrier_cast(carrier: RTKCarrier) -> Carrier { RTKCarrier::B3 => Carrier::B3, RTKCarrier::E5a5b => Carrier::E5a5b, RTKCarrier::L1 => Carrier::L1, - RTKCarrier::G1 => Carrier::G1(None), - RTKCarrier::G2 => Carrier::G2(None), - RTKCarrier::G3 => Carrier::G3, RTKCarrier::E5b => Carrier::E5b, - RTKCarrier::E6Lex => Carrier::E6, - RTKCarrier::G1a => Carrier::G1a, - RTKCarrier::G2a => Carrier::G2a, RTKCarrier::L5 => Carrier::L5, RTKCarrier::L2 => Carrier::L2, - RTKCarrier::S => Carrier::S, } } /// Converts [Carrier] to [RTKCarrier] pub fn cast_rtk_carrier(carrier: Carrier) -> Result { - let freq_mhz = carrier.frequency_mega_hz(); - RTKCarrier::from_frequency_mega_hz(freq_mhz) -} - -// // helper in reference signal determination -// fn rtk_reference_carrier(carrier: RTKCarrier) -> bool { -// matches!( -// carrier, -// RTKCarrier::L1 | RTKCarrier::E1 | RTKCarrier::B1c | RTKCarrier::B1i -// ) -// } - -//use map_3d::{ecef2geodetic, rad2deg, Ellipsoid}; - -//pub fn tropo_components(meteo: Option<&Rinex>, t: Epoch, lat_ddeg: f64) -> Option<(f64, f64)> { -// const MAX_LATDDEG_DELTA: f64 = 15.0; -// let max_dt = Duration::from_hours(24.0); -// let rnx = meteo?; -// let meteo = rnx.header.meteo.as_ref().unwrap(); -// -// let delays: Vec<(Observable, f64)> = meteo -// .sensors -// .iter() -// .filter_map(|s| match s.observable { -// Observable::ZenithDryDelay => { -// let (x, y, z, _) = s.position?; -// let (lat, _, _) = ecef2geodetic(x, y, z, Ellipsoid::WGS84); -// let lat = rad2deg(lat); -// if (lat - lat_ddeg).abs() < MAX_LATDDEG_DELTA { -// let value = rnx -// .zenith_dry_delay() -// .filter(|(t_sens, _)| (*t_sens - t).abs() < max_dt) -// .min_by_key(|(t_sens, _)| (*t_sens - t).abs()); -// let (_, value) = value?; -// debug!("{:?} lat={} zdd {}", t, lat_ddeg, value); -// Some((s.observable.clone(), value)) -// } else { -// None -// } -// }, -// Observable::ZenithWetDelay => { -// let (x, y, z, _) = s.position?; -// let (mut lat, _, _) = ecef2geodetic(x, y, z, Ellipsoid::WGS84); -// lat = rad2deg(lat); -// if (lat - lat_ddeg).abs() < MAX_LATDDEG_DELTA { -// let value = rnx -// .zenith_wet_delay() -// .filter(|(t_sens, _)| (*t_sens - t).abs() < max_dt) -// .min_by_key(|(t_sens, _)| (*t_sens - t).abs()); -// let (_, value) = value?; -// debug!("{:?} lat={} zdd {}", t, lat_ddeg, value); -// Some((s.observable.clone(), value)) -// } else { -// None -// } -// }, -// _ => None, -// }) -// .collect(); -// -// if delays.len() < 2 { -// None -// } else { -// let zdd = delays -// .iter() -// .filter_map(|(obs, value)| { -// if obs == &Observable::ZenithDryDelay { -// Some(*value) -// } else { -// None -// } -// }) -// .reduce(|k, _| k) -// .unwrap(); -// -// let zwd = delays -// .iter() -// .filter_map(|(obs, value)| { -// if obs == &Observable::ZenithWetDelay { -// Some(*value) -// } else { -// None -// } -// }) -// .reduce(|k, _| k) -// .unwrap(); -// -// Some((zwd, zdd)) -// } -//} - -/// Returns a [KbModel] -pub fn kb_model(nav: &Rinex, t: Epoch) -> Option { - let (nav_key, model) = nav - .nav_klobuchar_models_iter() - .min_by_key(|(k_i, _)| (k_i.epoch - t).abs())?; - - Some(KbModel { - h_km: { - match nav_key.sv.constellation { - Constellation::BeiDou => 375.0, - // we only expect GPS or BDS here, - // badly formed RINEX will generate errors in the solutions - _ => 350.0, - } + match carrier { + Carrier::B1 => Ok(RTKCarrier::B1), + Carrier::B3 => Ok(RTKCarrier::B3), + Carrier::E5a5b => Ok(RTKCarrier::E5a5b), + Carrier::L2 => Ok(RTKCarrier::L2), + Carrier::L5 | Carrier::E5a => Ok(RTKCarrier::L5), + Carrier::L1 | Carrier::E1 => Ok(RTKCarrier::L1), + Carrier::E5b => Ok(RTKCarrier::E5b), + carrier => { + error!("{} - signal not supported", carrier); + Err(RTKError::UnknownCarrierFrequency) }, - alpha: model.alpha, - beta: model.beta, - }) + } } -// /* -// * Grabs nearest BD model (in time) -// */ -// pub fn bd_model(nav: &Rinex, t: Epoch) -> Option { -// let (_, model) = nav -// .nav_bdgim_models_iter() -// .min_by_key(|(k_i, _)| (k_i.epoch - t).abs())?; - -// Some(BdModel { alpha: model.alpha }) -// } - -// /* -// * Grabs nearest NG model (in time) -// */ -// pub fn ng_model(nav: &Rinex, t: Epoch) -> Option { -// let (_, model) = nav -// .nav_nequickg_models_iter() -// .min_by_key(|(k_i, _)| (k_i.epoch - t).abs())?; - -// Some(NgModel { a: model.a }) -// } - pub fn precise_positioning( - _cli: &Cli, ctx: &Context, - is_rtk: bool, + uses_rtk: bool, matches: &ArgMatches, ) -> Result { // Load custom configuration script, or Default @@ -251,31 +109,10 @@ pub fn precise_positioning( let cfg: Config = serde_json::from_str(&content) .unwrap_or_else(|e| panic!("failed to parse configuration: {}", e)); - /* - * CGGTTS special case - */ - #[cfg(not(feature = "cggtts"))] - if matches.get_flag("cggtts") { - panic!("--cggtts option not available: compile with cggtts option"); - } - - info!("Using custom solver configuration: {:#?}", cfg); cfg }, None => { - let method = Method::default(); - - let cfg = Config::default().with_navigation_method(method); - - /* - * CGGTTS special case - */ - #[cfg(not(feature = "cggtts"))] - if matches.get_flag("cggtts") { - panic!("--cggtts option not available: compile with cggtts option"); - } - - info!("Using {:?} default preset: {:#?}", method, cfg); + let cfg = Config::default(); cfg }, }; @@ -286,13 +123,21 @@ pub fn precise_positioning( "Positioning requires Observation RINEX" ); - if !is_rtk { - assert!( - ctx.data.brdc_navigation().is_some(), - "Positioning requires Navigation RINEX" - ); + assert!( + ctx.data.brdc_navigation().is_some(), + "Positioning requires Navigation RINEX" + ); + + /* + * CGGTTS special case + */ + #[cfg(not(feature = "cggtts"))] + if matches.get_flag("cggtts") { + panic!("--cggtts option not available: compile with cggtts option"); } + info!("Using custom solver configuration: {:#?}", cfg); + if let Some(obs_rinex) = ctx.data.observation() { if let Some(obs_header) = &obs_rinex.header.obs { if let Some(time_of_first_obs) = obs_header.timeof_first_obs { @@ -323,17 +168,30 @@ pub fn precise_positioning( } } - // print config to be used - info!("Using {:?} method", cfg.method); + let rtk_obs = if uses_rtk { + rtk::parse_rinex(&matches) + } else { + Rinex::basic_obs() + }; - // create data providers - let eph = RefCell::new(EphemerisSource::from_ctx(ctx)); + // Deploy base station if needed + let base_station = if uses_rtk { + Some(RTKBaseStation::new(&rtk_obs)) + } else { + None + }; - let clocks = Clock::new(&ctx, &eph); let time = Time::new(&ctx); - let orbits = Orbits::new(&ctx, &eph); - // let mut rtk_reference = RemoteRTKReference::from_ctx(&ctx); + let ephemeris_buffer = EphemerisBuffer::new(&ctx); + let ephemeris_buffer = Rc::new(RefCell::new(ephemeris_buffer)); + + let env_biases = EnvironmentalBiases::new(); + let orbits = Orbits::new(&ctx, Rc::clone(&ephemeris_buffer)); + let space_biases = SpacebornBiases::new(Rc::clone(&ephemeris_buffer)); + + // Ephemeris interface is not used by this application + let null_eph = NullEphemerisSource::new(); // reference point is mandatory to CGGTTS opmode #[cfg(feature = "cggtts")] @@ -346,8 +204,6 @@ If your dataset does not describe one, you can manually describe one, see --help } } - let bias_model = BiasModel {}; - let apriori = ctx.rx_orbit; let apriori_ecef_m = match apriori { @@ -358,35 +214,53 @@ If your dataset does not describe one, you can manually describe one, see --help None => None, }; - let solver = PPP::new( + let solver = Solver::new( ctx.data.almanac.clone(), ctx.data.earth_cef, cfg.clone(), + null_eph.into(), orbits.into(), + space_biases.into(), + env_biases.into(), time, - bias_model, apriori_ecef_m, ); - let user_profile = User { - profile: { - if matches.get_flag("static") { - Profile::Static - } else { - Profile::Pedestrian - } - }, - clock_sigma_s: if let Some(clock_sigma) = matches.get_one::("clock-sigma") { - *clock_sigma - } else { - 0.01 - }, + let user_profile = if matches.get_flag("static") { + UserProfile::Static + } else if matches.get_flag("car") { + UserProfile::Car + } else if matches.get_flag("airplane") { + UserProfile::Airplane + } else if matches.get_flag("rocket") { + UserProfile::Rocket + } else { + UserProfile::Pedestrian + }; + + let clock_profile = if matches.get_flag("quartz") { + ClockProfile::Quartz + } else if matches.get_flag("atomic") { + ClockProfile::Atomic + } else if matches.get_flag("h-maser") { + ClockProfile::HydrogenMaser + } else { + ClockProfile::Oscillator }; + info!( + "deployed with {} profile - clock profile: {:?}", + user_profile, clock_profile + ); + + let user_params = UserParameters::new(user_profile.clone(), clock_profile.clone()); + + // PPP+CGGTTS special case #[cfg(feature = "cggtts")] if matches.get_flag("cggtts") { //* CGGTTS special opmode */ - let tracks = cggtts::resolve(ctx, &eph, user_profile, clocks, solver, cfg.method)?; + let tracks = cggtts::resolve(ctx, cfg.method, user_params, solver, ephemeris_buffer)?; + if !tracks.is_empty() { cggtts_post_process(&ctx, &tracks, matches)?; let report = CggttsReport::new(&ctx, &tracks); @@ -398,11 +272,12 @@ If your dataset does not describe one, you can manually describe one, see --help } } - /* PPP */ - let solutions = ppp::resolve(ctx, &eph, user_profile, clocks, solver); + // PPP/RTK + let solutions = ppp::resolve(ctx, user_params, solver, ephemeris_buffer); + if !solutions.is_empty() { ppp_post_process(&ctx, &solutions, matches)?; - let report = PPPReport::new(&cfg, &ctx, user_profile, &solutions); + let report = PPPReport::new(&cfg, &ctx, user_profile, clock_profile, &solutions); Ok(report.formalize()) } else { error!("solver did not generate a single solution"); diff --git a/src/positioning/orbit.rs b/src/positioning/orbit.rs index e40a8c2..8fd1984 100644 --- a/src/positioning/orbit.rs +++ b/src/positioning/orbit.rs @@ -1,73 +1,85 @@ use crate::{ cli::Context, - positioning::{Buffer, CenteredSnapshot, Coords3d, EphemerisSource, PreciseOrbits}, + positioning::{Buffer, CenteredSnapshot, Coords3d, EphemerisBuffer, PreciseOrbits}, }; use anise::errors::AlmanacError; use rinex::carrier::Carrier; use gnss_rtk::prelude::{ - Almanac, Duration, Epoch, Frame, Orbit, OrbitSource, Vector3, EARTH_J2000, SUN_J2000, SV, + Almanac, Duration, Epoch, Frame, Orbit, OrbitSource, Rc, Vector3, EARTH_J2000, SUN_J2000, SV, }; use std::{cell::RefCell, collections::HashMap}; -pub struct Orbits<'a, 'b> { - eos: bool, +pub struct Orbits<'a> { + // eos_precise: bool, has_precise: bool, - eph: &'a RefCell>, - precise: RefCell>, + // has_precise: bool, + // eph: &'a RefCell>, + // precise: RefCell>, + ephemeris_buffer: Rc>>, } -impl<'a, 'b> Orbits<'a, 'b> { - pub fn new(ctx: &'a Context, eph: &'a RefCell>) -> Self { - let has_precise = ctx.data.has_sp3(); - let precise = RefCell::new(PreciseOrbits::new(ctx)); - +impl<'a> Orbits<'a> { + pub fn new(ctx: &'a Context, ephemeris_buffer: Rc>>) -> Self { + // let has_precise = ctx.data.has_sp3(); + // let precise = RefCell::new(PreciseOrbits::new(ctx)); Self { - eph, - precise, - eos: false, - has_precise, + ephemeris_buffer, + has_precise: false, } } } -impl OrbitSource for Orbits<'_, '_> { - fn next_at(&self, t: Epoch, sv: SV, frame: Frame) -> Option { +impl OrbitSource for Orbits<'_> { + fn state_at(&self, epoch: Epoch, sv: SV, frame: Frame) -> Option { if self.has_precise { - let mut precise_orbits = self.precise.borrow_mut(); - let orbit = precise_orbits.next_precise_at(t, sv, frame)?; - let state = orbit.to_cartesian_pos_vel(); - - let (x_km, y_km, z_km) = (state[0], state[1], state[2]); + panic!("not yet"); + } else { + if let Some(frm) = self + .ephemeris_buffer + .borrow() + .select_sv_ephemeris(epoch, sv) + { + if let Some(orbit) = frm.ephemeris.kepler2position(sv, epoch) { + let pos_vel_km = orbit.to_cartesian_pos_vel(); + debug!( + "{}({}) - kepler state: x={}km y={}km z={}km", + epoch.round(Duration::from_milliseconds(1.0)), + sv, + pos_vel_km[0], + pos_vel_km[1], + pos_vel_km[2], + ); - debug!( - "{} ({}) - precise state : x={}, y={}, z={} (km, ECEF)", - t.round(Duration::from_milliseconds(1.0)), - sv, - x_km, - y_km, - z_km - ); + Some(orbit) + } else { + error!("{}({}) - kepler solver failed", epoch, sv); + None + } + } else { + error!("{}({}) - no ephemeris available", epoch, sv); + None + } + } + // if self.has_precise { + // let mut precise_orbits = self.precise.borrow_mut(); + // let orbit = precise_orbits.next_precise_at(t, sv, frame)?; + // let state = orbit.to_cartesian_pos_vel(); - Some(orbit) - } else { - let (toc, _, eph) = self.eph.borrow_mut().select(t, sv)?; - let orbit = eph.kepler2position(sv, t)?; - let state = orbit.to_cartesian_pos_vel(); - let (x_km, y_km, z_km) = (state[0], state[1], state[2]); + // let (x_km, y_km, z_km) = (state[0], state[1], state[2]); - debug!( - "{} ({}) - keplerian state : x={}, y={}, z={} (km, ECEF)", - t.round(Duration::from_milliseconds(1.0)), - sv, - x_km, - y_km, - z_km - ); + // debug!( + // "{} ({}) - precise state : x={}, y={}, z={} (km, ECEF)", + // t.round(Duration::from_milliseconds(1.0)), + // sv, + // x_km, + // y_km, + // z_km + // ); - Some(orbit) - } + // Some(orbit) + // } else { } } diff --git a/src/positioning/ppp/mod.rs b/src/positioning/ppp/mod.rs index f4987cc..daec7c0 100644 --- a/src/positioning/ppp/mod.rs +++ b/src/positioning/ppp/mod.rs @@ -1,12 +1,13 @@ //! PPP solver use crate::{ cli::Context, - positioning::{cast_rtk_carrier, ClockStateProvider, EphemerisSource}, + positioning::{cast_rtk_carrier, EphemerisBuffer}, }; use std::{ - cell::RefCell, + cell::{RefCell, RefMut}, collections::{BTreeMap, HashMap}, + rc::Rc, }; use rinex::{ @@ -20,15 +21,22 @@ pub use report::Report; pub mod post_process; use gnss_rtk::prelude::{ - AbsoluteTime, Bias, Candidate, Epoch, Observation, OrbitSource, PVTSolution, User, PPP, + AbsoluteTime, Candidate, ClockProfile, EnvironmentalBias, EphemerisSource, Epoch, Observation, + OrbitSource, PVTSolution, Solver, SpacebornBias, UserParameters, UserProfile, }; -pub fn resolve<'a, 'b, CK: ClockStateProvider, O: OrbitSource, B: Bias, T: AbsoluteTime>( +pub fn resolve< + 'a, + EPH: EphemerisSource, + ORB: OrbitSource, + EB: EnvironmentalBias, + SB: SpacebornBias, + TIM: AbsoluteTime, +>( ctx: &Context, - eph: &'a RefCell>, - user_profile: User, - mut clock: CK, - mut solver: PPP, + user_params: UserParameters, + mut solver: Solver, + ephemeris_buffer: Rc>>, ) -> BTreeMap { let mut past_epoch = Option::::None; @@ -40,20 +48,20 @@ pub fn resolve<'a, 'b, CK: ClockStateProvider, O: OrbitSource, B: Bias, T: Absol let mut candidates = Vec::::with_capacity(4); let mut sv_observations = HashMap::>::new(); - // TODO: RTK - let mut remote_observations = Vec::::new(); + for (epoch, signal) in obs_data.signal_observations_sampling_ok_iter() { + ephemeris_buffer.borrow_mut().new_epoch(epoch); - for (t, signal) in obs_data.signal_observations_sampling_ok_iter() { let carrier = Carrier::from_observable(signal.sv.constellation, &signal.observable); if carrier.is_err() { error!( "{}({}/{}) - unknown signal {:?}", - t, + epoch, signal.sv.constellation, signal.observable, carrier.err().unwrap() ); + continue; } @@ -64,53 +72,40 @@ pub fn resolve<'a, 'b, CK: ClockStateProvider, O: OrbitSource, B: Bias, T: Absol if rtk_carrier.is_err() { error!( "{}({}/{}) - unknown frequency: {}", - t, + epoch, signal.sv.constellation, signal.observable, rtk_carrier.err().unwrap() ); + continue; } let rtk_carrier = rtk_carrier.unwrap(); if let Some(past_t) = past_epoch { - if t > past_t { + if epoch > past_t { // New epoch: solving attempt for (sv, observations) in sv_observations.iter() { // Create new candidate let mut cd = Candidate::new(*sv, past_t, observations.clone()); - - // candidate "fixup" or customizations - match clock.next_clock_at(past_t, *sv) { - Some(dt) => cd.set_clock_correction(dt), - None => error!("{} ({}) - no clock correction available", past_t, *sv), - } - - if let Some((_, _, eph)) = eph.borrow_mut().select(past_t, *sv) { - if let Some(tgd) = eph.tgd() { - debug!("{} ({}) - tgd: {}", past_t, *sv, tgd); - cd.set_group_delay(tgd); - } - } - candidates.push(cd); } - match solver.resolve(user_profile, past_t, &candidates) { + match solver.ppp(past_t, user_params, &candidates) { Ok(pvt) => { info!( - "{} : new pvt solution {:?} dt={}", - pvt.epoch, pvt.pos_m, pvt.clock_offset_s + "{} : new {:?} solution {:?} dt={}", + pvt.epoch, pvt.solution_type, pvt.pos_m, pvt.clock_offset_s ); + solutions.insert(pvt.epoch, pvt); }, - Err(e) => warn!("{} : pvt solver error \"{}\"", past_t, e), + Err(e) => warn!("{} : solver error \"{}\"", past_t, e), } candidates.clear(); sv_observations.clear(); - remote_observations.clear(); } } @@ -185,8 +180,7 @@ pub fn resolve<'a, 'b, CK: ClockStateProvider, O: OrbitSource, B: Bias, T: Absol _ => {}, } } - - past_epoch = Some(t); + past_epoch = Some(epoch); } solutions } diff --git a/src/positioning/ppp/post_process.rs b/src/positioning/ppp/post_process.rs index 2f7b4df..cd0ce62 100644 --- a/src/positioning/ppp/post_process.rs +++ b/src/positioning/ppp/post_process.rs @@ -61,7 +61,7 @@ pub fn post_process( for (epoch, solution) in solutions { let (x_m, y_m, z_m) = solution.pos_m; - let (vel_x_ms, vel_y_ms, vel_z_ms) = solution.vel_m_s; + let (vel_x_ms, vel_y_ms, vel_z_ms) = (0.0, 0.0, 0.0); //solution.vel_m_s; let (lat_deg, long_deg, alt_m) = solution.lat_long_alt_deg_deg_m; let (lat_rad, long_rad) = (lat_deg.to_radians(), long_deg.to_radians()); diff --git a/src/positioning/ppp/report.rs b/src/positioning/ppp/report.rs index a43f9b3..44054f7 100644 --- a/src/positioning/ppp/report.rs +++ b/src/positioning/ppp/report.rs @@ -2,8 +2,8 @@ use crate::cli::Context; use std::collections::BTreeMap; use gnss_rtk::prelude::{ - Config as NaviConfig, Duration, Epoch, Method as NaviMethod, PVTSolution, - Profile as NaviProfile, TimeScale, User as UserProfile, SV, + ClockProfile, Config as NaviConfig, Duration, Epoch, Method as NaviMethod, PVTSolution, + TimeScale, UserParameters, UserProfile, SV, }; use gnss_qc::{ @@ -30,13 +30,14 @@ impl Render for ReportTab { struct Summary { method: NaviMethod, - profile: NaviProfile, orbit: String, first_epoch: Epoch, last_epoch: Epoch, duration: Duration, satellites: Vec, timescale: TimeScale, + user_profile: UserProfile, + clock_profile: ClockProfile, final_err_m: Option<(f64, f64, f64)>, final_geo_ddeg_m: (f64, f64, f64), averaged_err_m: Option<(f64, f64, f64)>, @@ -52,10 +53,27 @@ impl Render for Summary { tbody { tr { th class="is-info" { - "Profile" + "Session" } - td { - (self.profile.to_string()) + @ if self.user_profile == UserProfile::Static { + td { + ("Static") + } + } @ else { + td { + ("Kinematic") + } + } + } + tr { + th class="is-info" { + "User Profile" + } + } + + tr { + th class="is-info" { + "Clock Profile" } } @@ -296,7 +314,8 @@ impl Summary { fn new( cfg: &NaviConfig, ctx: &Context, - user: &UserProfile, + user_profile: UserProfile, + clock_profile: ClockProfile, solutions: &BTreeMap, x0_y0_z0_m: Option<(f64, f64, f64)>, lat0_long0_alt0_ddeg_ddeg_km: Option<(f64, f64, f64)>, @@ -350,7 +369,7 @@ impl Summary { } } - if user.profile == NaviProfile::Static { + if user_profile == UserProfile::Static { // averaged coords if let Some(averaged_latlongalt_ddeg_m) = &mut averaged_latlongalt_ddeg_m { *averaged_latlongalt_ddeg_m = ( @@ -388,6 +407,8 @@ impl Summary { last_epoch, timescale, satellites, + user_profile, + clock_profile, final_err_m, final_geo_ddeg_m, averaged_err_m, @@ -400,8 +421,6 @@ impl Summary { } }, method: cfg.method, - profile: user.profile, - // filter: cfg.solver.filter, duration: last_epoch - first_epoch, surveyed_lat_long_alt_ddeg_ddeg_km: lat0_long0_alt0_ddeg_ddeg_km, } @@ -411,32 +430,45 @@ impl Summary { struct ReportContent { /// summary summary: Summary, + /// sv_plot sv_plot: Plot, + /// map_proj map_proj: Plot, + /// clk_plot clk_plot: Plot, + /// drift_plot drift_plot: Plot, + /// ddeg_plot ddeg_plot: Plot, + /// altitude_plot altitude_plot: Plot, + /// coords_err coords_err_plot: Option, + /// 3d_plot coords_err3d_plot: Option, + /// velocity_plot vel_plot: Plot, + /// DOP dop_plot: Plot, + /// TDOP tdop_plot: Plot, + // /// NAVI // navi_plot: Plot, /// tropod tropod_plot: Plot, + /// ionod ionod_plot: Plot, } @@ -446,6 +478,7 @@ impl ReportContent { cfg: &NaviConfig, ctx: &Context, user_profile: UserProfile, + clock_profile: ClockProfile, solutions: &BTreeMap, ) -> Self { let nb_solutions = solutions.len(); @@ -470,7 +503,8 @@ impl ReportContent { let summary = Summary::new( cfg, ctx, - &user_profile, + user_profile.clone(), + clock_profile, solutions, x0y0z0_m, lat0_long0_alt0_km, @@ -517,7 +551,7 @@ impl ReportContent { for (index, (_, sol_i)) in solutions.iter().enumerate() { let (lat_ddeg, long_ddeg, _) = sol_i.lat_long_alt_deg_deg_m; - let modulo = if user_profile.profile == NaviProfile::Static { + let modulo = if user_profile == UserProfile::Static { 10 } else { 1 @@ -645,53 +679,53 @@ impl ReportContent { let mut plot = Plot::timedomain_plot("vel_plot", "Velocity", "Velocity [m/s]", true); - let vel_x = solutions - .iter() - .map(|(_, sol)| sol.vel_m_s.0) - .collect::>(); - - let vel_y = solutions - .iter() - .map(|(_, sol)| sol.vel_m_s.1) - .collect::>(); - - let vel_z = solutions - .iter() - .map(|(_, sol)| sol.vel_m_s.2) - .collect::>(); - - let trace = Plot::timedomain_chart( - "vel_x", - Mode::Markers, - MarkerSymbol::Cross, - &epochs, - vel_x, - true, - ); - - plot.add_trace(trace); - - let trace = Plot::timedomain_chart( - "vel_y", - Mode::Markers, - MarkerSymbol::Cross, - &epochs, - vel_y, - true, - ); - - plot.add_trace(trace); - - let trace = Plot::timedomain_chart( - "vel_z", - Mode::Markers, - MarkerSymbol::Cross, - &epochs, - vel_z, - true, - ); - - plot.add_trace(trace); + // let vel_x = solutions + // .iter() + // .map(|(_, sol)| sol.vel_m_s.0) + // .collect::>(); + + // let vel_y = solutions + // .iter() + // .map(|(_, sol)| sol.vel_m_s.1) + // .collect::>(); + + // let vel_z = solutions + // .iter() + // .map(|(_, sol)| sol.vel_m_s.2) + // .collect::>(); + + // let trace = Plot::timedomain_chart( + // "vel_x", + // Mode::Markers, + // MarkerSymbol::Cross, + // &epochs, + // vel_x, + // true, + // ); + + // plot.add_trace(trace); + + // let trace = Plot::timedomain_chart( + // "vel_y", + // Mode::Markers, + // MarkerSymbol::Cross, + // &epochs, + // vel_y, + // true, + // ); + + // plot.add_trace(trace); + + // let trace = Plot::timedomain_chart( + // "vel_z", + // Mode::Markers, + // MarkerSymbol::Cross, + // &epochs, + // vel_z, + // true, + // ); + + // plot.add_trace(trace); plot }, tropod_plot: { @@ -884,20 +918,20 @@ impl ReportContent { let mut plot = Plot::timedomain_plot("clk_drift", "Clock Drift", "Drift [s/s]", true); - let clock_drift = solutions - .iter() - .map(|(_, sol)| sol.clock_drift_s_s) - .collect::>(); - - let trace = Plot::timedomain_chart( - "drift", - Mode::Markers, - MarkerSymbol::Cross, - &epochs, - clock_drift, - true, - ); - plot.add_trace(trace); + // let clock_drift = solutions + // .iter() + // .map(|(_, sol)| sol.clock_drift_s_s) + // .collect::>(); + + // let trace = Plot::timedomain_chart( + // "drift", + // Mode::Markers, + // MarkerSymbol::Cross, + // &epochs, + // clock_drift, + // true, + // ); + // plot.add_trace(trace); plot }, @@ -1172,11 +1206,12 @@ impl Report { cfg: &NaviConfig, ctx: &Context, user_profile: UserProfile, + clock_profile: ClockProfile, solutions: &BTreeMap, ) -> Self { Self { tab: ReportTab {}, - content: ReportContent::new(cfg, ctx, user_profile, solutions), + content: ReportContent::new(cfg, ctx, user_profile, clock_profile, solutions), } } } diff --git a/src/positioning/rtk.rs b/src/positioning/rtk.rs index 6a8379a..6241feb 100644 --- a/src/positioning/rtk.rs +++ b/src/positioning/rtk.rs @@ -1,99 +1,111 @@ -use crate::{cli::Context, positioning::cast_rtk_carrier}; +use log::info; -use gnss_rtk::prelude::{Epoch, Observation as RTKObservation, SV}; +use crate::{cli::Context, positioning::cast_rtk_carrier}; -use rinex::{ - prelude::{Carrier, Observable, obs::EpochFlag}, +use rinex::prelude::{ + obs::{EpochFlag, SignalObservation}, + Carrier, Epoch, Observable, Rinex, RinexType, }; +use clap::ArgMatches; + +use gnss_rtk::prelude::{Candidate, RTKBase}; + use std::collections::{BTreeMap, HashMap}; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -struct BufKey { - pub t: Epoch, - pub sv: SV, - pub carrier: Carrier, +pub fn parse_rinex(matches: &ArgMatches) -> Rinex { + let mut first_file = Option::::None; + + let file_path = matches + .get_one::("fp") + .expect("Base station observations required!"); + + let rinex = if file_path.ends_with(".gz") { + Rinex::from_gzip_file(file_path) + } else { + Rinex::from_file(file_path) + }; + + let mut rinex = rinex.unwrap_or_else(|e| { + panic!("Failed to parse base station observations: {}", e); + }); + + rinex } -pub struct RemoteRTKReference<'a> { - buffer: HashMap, - iter: Box< - dyn Iterator< - Item = ( - &'a (Epoch, EpochFlag), - &'a ( - Option, - BTreeMap>, - ), - ), - > + 'a, - >, +pub struct RTKBaseStation<'a> { + /// Name of this station + name: String, + + /// Reference position is mandatory + reference_position_ecef_m: (f64, f64, f64), + + /// End of observations stream + eos: bool, + + /// Iterator + iter: Box + 'a>, + + /// Buffer + buffer: Vec<(Epoch, SignalObservation)>, } -impl<'a> RemoteRTKReference<'a> { - pub fn from_ctx(ctx: &'a Context) -> Self { - if let Some(reference_site) = ctx.reference_site.as_ref() { - if let Some(remote_obs) = reference_site.data.observation() { - info!("Remote reference site context created"); - Self { - iter: remote_obs.observation(), - buffer: HashMap::with_capacity(16), +impl<'a> RTKBase for RTKBaseStation<'a> { + fn name(&self) -> String { + self.name.clone() + } + + fn new_epoch(&mut self, epoch: Epoch) { + // consume till EoS or new epoch + loop { + if self.eos { + break; + } + + if let Some((epoch, observation)) = self.iter.next() { + self.buffer.push((epoch, observation.clone())); + + if epoch > epoch { + break; } } else { - warn!("Not RTK compatible: missing remote observations"); - Self { - iter: Box::new([].into_iter()), - buffer: HashMap::with_capacity(16), - } - } - } else { - warn!("Not RTK compatible: no reference site definition"); - Self { - iter: Box::new([].into_iter()), - buffer: HashMap::with_capacity(16), + self.eos = true; } } } - pub fn observe(&mut self, t: Epoch, sv: SV, carrier: Carrier) -> Option { - let rtk_carrier = cast_rtk_carrier(carrier); - let mut ret = Option::::None; - let key = BufKey { t, sv, carrier }; - if let Some(value) = self.buffer.get(&key) { - // TODO (SNR) - ret = Some(RTKObservation::pseudo_range(rtk_carrier, *value, None)); - } - if ret.is_none() { - while let Some(((remote_t, remote_flag), (_, remote_svnn))) = self.iter.next() { - if remote_flag.is_ok() { - for (remote_sv, remote_obsnn) in remote_svnn.iter() { - for (remote_observable, remote_obs) in remote_obsnn.iter() { - if let Ok(remote_carrier) = - Carrier::from_observable(remote_sv.constellation, remote_observable) - { - let key = BufKey { - t: *remote_t, - sv: *remote_sv, - carrier: remote_carrier, - }; - let remote_rtk_carrier = cast_rtk_carrier(remote_carrier); - if *remote_t == t && *remote_sv == sv && remote_carrier == carrier { - // TODO (SNR) - return Some(RTKObservation::pseudo_range( - remote_rtk_carrier, - remote_obs.obs, - None, - )); - } else { - self.buffer.insert(key, remote_obs.obs); - } - } - } - } - } - } - } - self.buffer.retain(|k, _| k.t >= t); - ret + fn observe(&self, epoch: Epoch) -> Vec { + vec![] + } + + fn reference_position_ecef_m(&self, _: Epoch) -> (f64, f64, f64) { + self.reference_position_ecef_m + } +} + +impl<'a> RTKBaseStation<'a> { + pub fn new(rinex: &'a Rinex) -> Self { + assert_eq!( + rinex.header.rinex_type, + RinexType::ObservationData, + "base station file must be Observation RINEX!" + ); + + let reference_position_ecef_m = rinex + .header + .rx_position + .as_ref() + .expect("base station coordinates must be descrined in the RINEX header!"); + + let s = Self { + eos: false, + name: "Base".to_string(), + buffer: Vec::with_capacity(8), + iter: rinex.signal_observations_sampling_ok_iter(), + reference_position_ecef_m: *reference_position_ecef_m, + }; + + info!("{} - rtk base station deployed", s.name()); + s } } diff --git a/src/positioning/time.rs b/src/positioning/time.rs index 1e36c27..8bca815 100644 --- a/src/positioning/time.rs +++ b/src/positioning/time.rs @@ -1,8 +1,8 @@ -use crate::cli::Context; use gnss_qc::prelude::TimeCorrectionsDB; -use hifitime::Unit; - use gnss_rtk::prelude::{AbsoluteTime, Epoch, TimeScale}; +use log::info; + +use crate::cli::Context; pub struct Time { database: Option, @@ -34,6 +34,7 @@ impl AbsoluteTime for Time { impl Time { pub fn new(ctx: &Context) -> Self { + info!("time corrections database created"); Self { database: ctx.data.time_corrections_database(), }