From 418ae3b8ad714aeccbf982eb91d7cc5384471f7d Mon Sep 17 00:00:00 2001 From: gwbres Date: Tue, 12 Mar 2024 08:35:28 +0100 Subject: [PATCH] Several improvements (#210) * Improve context definitions and clock usage * run clippy * add more comments * Production environment improvements * make parsing smarter * propose one method to enhance self, for files that would not follow standard naming conventions * add more tests * introduce fops command line * unlock one more reciprocal test * fix help menu * fixed SSI plot augmentation with SV attitude * add example * handle batch # in filename --------- Signed-off-by: Guillaume W. Bres --- rinex-cli/Cargo.toml | 1 + rinex-cli/src/cli/{ => fops}/filegen.rs | 12 +- rinex-cli/src/cli/{ => fops}/merge.rs | 6 + rinex-cli/src/cli/fops/mod.rs | 82 + rinex-cli/src/cli/{ => fops}/split.rs | 6 + rinex-cli/src/cli/{ => fops}/substract.rs | 6 + rinex-cli/src/cli/{ => fops}/time_binning.rs | 6 + rinex-cli/src/cli/graph.rs | 2 +- rinex-cli/src/cli/mod.rs | 17 +- rinex-cli/src/fops.rs | 170 +- rinex-cli/src/graph/record/navigation.rs | 16 +- rinex-cli/src/graph/record/observation.rs | 48 +- rinex-cli/src/main.rs | 3 +- rinex-cli/src/positioning/cggtts/mod.rs | 4 +- rinex-cli/src/positioning/ppp/mod.rs | 51 +- rinex-cli/src/positioning/ppp/post_process.rs | 4 +- rinex-cli/src/preprocessing.rs | 527 +++- rinex/src/antex/record.rs | 38 +- rinex/src/clock/record.rs | 294 +-- rinex/src/context.rs | 6 +- rinex/src/hatanaka/compressor.rs | 1 + rinex/src/ionex/record.rs | 52 +- rinex/src/lib.rs | 180 +- rinex/src/meteo/record.rs | 76 +- rinex/src/navigation/record.rs | 2252 ++++++++--------- rinex/src/observation/record.rs | 73 +- rinex/src/production/ffu.rs | 93 +- rinex/src/production/mod.rs | 115 +- rinex/src/production/ppu.rs | 40 +- rinex/src/production/source.rs | 6 +- rinex/src/record.rs | 5 +- rinex/src/tests/clock.rs | 14 +- rinex/src/tests/decimation.rs | 10 +- rinex/src/tests/decompression.rs | 12 +- rinex/src/tests/filename.rs | 2 +- rinex/src/tests/obs.rs | 2 +- rinex/src/tests/production.rs | 2 - rinex/src/tests/toolkit/mod.rs | 2 +- sp3/src/lib.rs | 35 +- 39 files changed, 2672 insertions(+), 1599 deletions(-) rename rinex-cli/src/cli/{ => fops}/filegen.rs (57%) rename rinex-cli/src/cli/{ => fops}/merge.rs (71%) create mode 100644 rinex-cli/src/cli/fops/mod.rs rename rinex-cli/src/cli/{ => fops}/split.rs (71%) rename rinex-cli/src/cli/{ => fops}/substract.rs (75%) rename rinex-cli/src/cli/{ => fops}/time_binning.rs (70%) diff --git a/rinex-cli/Cargo.toml b/rinex-cli/Cargo.toml index ad242ca06..e88955c10 100644 --- a/rinex-cli/Cargo.toml +++ b/rinex-cli/Cargo.toml @@ -22,6 +22,7 @@ geo-types = "0.7.11" env_logger = "0.11" rand = "0.8.4" serde_json = "1" +lazy_static = "1.4" thiserror = "1" itertools = "0.12" map_3d = "0.1.5" diff --git a/rinex-cli/src/cli/filegen.rs b/rinex-cli/src/cli/fops/filegen.rs similarity index 57% rename from rinex-cli/src/cli/filegen.rs rename to rinex-cli/src/cli/fops/filegen.rs index ccc3bdc82..37b95143b 100644 --- a/rinex-cli/src/cli/filegen.rs +++ b/rinex-cli/src/cli/fops/filegen.rs @@ -1,5 +1,11 @@ // filegen opmode -use clap::Command; +use clap::{ + Command, + //ArgAction, + //value_parser, +}; + +use super::{SHARED_DATA_ARGS, SHARED_GENERAL_ARGS}; pub fn subcommand() -> Command { Command::new("filegen") @@ -10,4 +16,8 @@ pub fn subcommand() -> Command { modify and dump resulting context in preserved RINEX format. You can use this for example, to generate a decimated RINEX file from an input Observations file.", ) + .next_help_heading("Production Environment") + .args(SHARED_GENERAL_ARGS.iter()) + .next_help_heading("Data context") + .args(SHARED_DATA_ARGS.iter()) } diff --git a/rinex-cli/src/cli/merge.rs b/rinex-cli/src/cli/fops/merge.rs similarity index 71% rename from rinex-cli/src/cli/merge.rs rename to rinex-cli/src/cli/fops/merge.rs index 83c3daa78..c1e0189f5 100644 --- a/rinex-cli/src/cli/merge.rs +++ b/rinex-cli/src/cli/fops/merge.rs @@ -2,6 +2,8 @@ use clap::{value_parser, Arg, ArgAction, Command}; use std::path::PathBuf; +use super::{SHARED_DATA_ARGS, SHARED_GENERAL_ARGS}; + pub fn subcommand() -> Command { Command::new("merge") .short_flag('m') @@ -16,4 +18,8 @@ pub fn subcommand() -> Command { .required(true) .help("RINEX file to merge."), ) + .next_help_heading("Production Environment") + .args(SHARED_GENERAL_ARGS.iter()) + .next_help_heading("Data context") + .args(SHARED_DATA_ARGS.iter()) } diff --git a/rinex-cli/src/cli/fops/mod.rs b/rinex-cli/src/cli/fops/mod.rs new file mode 100644 index 000000000..fecc690fe --- /dev/null +++ b/rinex-cli/src/cli/fops/mod.rs @@ -0,0 +1,82 @@ +pub mod filegen; +pub mod merge; +pub mod split; +pub mod substract; +pub mod time_binning; + +use lazy_static::lazy_static; + +use ::clap::{value_parser, Arg, ArgAction}; + +use rinex::prod::{DataSource, FFU, PPU}; + +/* + * Arguments that are shared by all file operations. + * Mainly [ProductionAttributes] (re)definition opts + */ +lazy_static! { + pub static ref SHARED_GENERAL_ARGS : Vec = vec![ + Arg::new("batch") + .short('b') + .long("batch") + .required(false) + .value_parser(value_parser!(u8)) + .help("Set # (number ID) in case this file is part of a file serie"), + Arg::new("short") + .short('s') + .long("short") + .action(ArgAction::SetTrue) + .help("Prefer (deprecated) short filenames as historically used. +Otherwise, this ecosystem prefers modern (longer) filenames that contain more information."), + Arg::new("gzip") + .long("gzip") + .action(ArgAction::SetTrue) + .help("Append .gz suffix and perform seamless Gzip compression."), + Arg::new("agency") + .short('a') + .long("agency") + .required(false) + .help("Define a custom agency name, possibly overwriting +what the original filename did define (according to conventions)."), + Arg::new("country") + .short('c') + .long("country") + .required(false) + .help("Define a custom (3 letter) country code. +This code should represent where the Agency is located."), + Arg::new("source") + .long("src") + .required(false) + .value_name("[RCVR,STREAM]") + .value_parser(value_parser!(DataSource)) + .help("Define the data source. +In RINEX standards, we use \"RCVR\" when data was sampled from a hardware receiver. +Use \"STREAM\" for other stream data source, like RTCM for example.") + ]; + + pub static ref SHARED_DATA_ARGS : Vec = vec![ + Arg::new("PPU") + .long("ppu") + .required(false) + .value_name("[15M,01H,01D,01Y]") + .value_parser(value_parser!(PPU)) + .help("Define custom production periodicity (time between two batch/dataset). +\"15M\": 15' interval, \"01H\": 1 hr interval, \"01D\": 1 day interval, \"01Y\": 1 year interval"), + Arg::new("FFU") + .long("ffu") + .required(false) + .value_name("DDU") + .value_parser(value_parser!(FFU)) + .help("Define custom sampling interval. + Note that this only affects the filename to be generated, inner Record should match for consistency. + The sampling interval is the dominant time delta between two Epoch inside the record. + Format is \"DDU\" where DD must be (at most) two valid digits and U is the time Unit. + For example: \"30S\" for 30sec. interval, \"90S\" for 1'30s interval, \"20M\" for 20' interval, \"02H\" for 2hr interval, \"07D\" for weekly interval."), + Arg::new("region") + .long("region") + .value_name("[G]") + .help("Regional code, solely used in IONEX file name. +Use this to accurately (re)define your IONEX context that possibly did not follow standard naming conventions. +Use `G` for Global (World wide) TEC maps."), + ]; +} diff --git a/rinex-cli/src/cli/split.rs b/rinex-cli/src/cli/fops/split.rs similarity index 71% rename from rinex-cli/src/cli/split.rs rename to rinex-cli/src/cli/fops/split.rs index ac67794ab..c7bf2b434 100644 --- a/rinex-cli/src/cli/split.rs +++ b/rinex-cli/src/cli/fops/split.rs @@ -2,6 +2,8 @@ use clap::{value_parser, Arg, ArgAction, Command}; use rinex::prelude::Epoch; +use super::{SHARED_DATA_ARGS, SHARED_GENERAL_ARGS}; + pub fn subcommand() -> Command { Command::new("split") .short_flag('s') @@ -16,4 +18,8 @@ pub fn subcommand() -> Command { .required(true) .help("Epoch (instant) to split at."), ) + .next_help_heading("Production Environment") + .args(SHARED_GENERAL_ARGS.iter()) + .next_help_heading("Data context") + .args(SHARED_DATA_ARGS.iter()) } diff --git a/rinex-cli/src/cli/substract.rs b/rinex-cli/src/cli/fops/substract.rs similarity index 75% rename from rinex-cli/src/cli/substract.rs rename to rinex-cli/src/cli/fops/substract.rs index f34d4f523..8579835b0 100644 --- a/rinex-cli/src/cli/substract.rs +++ b/rinex-cli/src/cli/fops/substract.rs @@ -2,6 +2,8 @@ use clap::{value_parser, Arg, ArgAction, Command}; use std::path::PathBuf; +use super::{SHARED_DATA_ARGS, SHARED_GENERAL_ARGS}; + pub fn subcommand() -> Command { Command::new("sub") .long_flag("sub") @@ -20,4 +22,8 @@ This is typically used to compare two GNSS receivers together.", "RINEX(B) to substract to a single RINEX file (A), that was previously loaded.", ), ) + .next_help_heading("Production Environment") + .args(SHARED_GENERAL_ARGS.iter()) + .next_help_heading("Data context") + .args(SHARED_DATA_ARGS.iter()) } diff --git a/rinex-cli/src/cli/time_binning.rs b/rinex-cli/src/cli/fops/time_binning.rs similarity index 70% rename from rinex-cli/src/cli/time_binning.rs rename to rinex-cli/src/cli/fops/time_binning.rs index 93c371d18..043dfd590 100644 --- a/rinex-cli/src/cli/time_binning.rs +++ b/rinex-cli/src/cli/fops/time_binning.rs @@ -2,6 +2,8 @@ use clap::{value_parser, Arg, ArgAction, Command}; use rinex::prelude::Duration; +use super::{SHARED_DATA_ARGS, SHARED_GENERAL_ARGS}; + pub fn subcommand() -> Command { Command::new("tbin") .long_flag("tbin") @@ -15,4 +17,8 @@ pub fn subcommand() -> Command { .required(true) .help("Duration"), ) + .next_help_heading("Production Environment") + .args(SHARED_GENERAL_ARGS.iter()) + .next_help_heading("Data context") + .args(SHARED_DATA_ARGS.iter()) } diff --git a/rinex-cli/src/cli/graph.rs b/rinex-cli/src/cli/graph.rs index 44353a225..1bf6f5c79 100644 --- a/rinex-cli/src/cli/graph.rs +++ b/rinex-cli/src/cli/graph.rs @@ -98,7 +98,7 @@ or we post processed determined a CS.", Arg::new("orbit") .long("orbit") .action(ArgAction::SetTrue) - .help("SV position in the sky, on 2D cartesian plots."), + .help("3D projection of SV attitudes in the sky."), ) .arg( Arg::new("orbit-residual") diff --git a/rinex-cli/src/cli/mod.rs b/rinex-cli/src/cli/mod.rs index 838bb3eaa..33d7c47c3 100644 --- a/rinex-cli/src/cli/mod.rs +++ b/rinex-cli/src/cli/mod.rs @@ -9,7 +9,7 @@ use std::{ use clap::{value_parser, Arg, ArgAction, ArgMatches, ColorChoice, Command}; use rinex::prelude::*; -use crate::{fops::open_with_web_browser, Error}; +use crate::fops::open_with_web_browser; use map_3d::{geodetic2ecef, Ellipsoid}; @@ -17,20 +17,15 @@ use map_3d::{geodetic2ecef, Ellipsoid}; mod identify; // graph mode mod graph; -// merge mode -mod merge; -// split mode -mod split; -// tbin mode -mod time_binning; -// substraction mode -mod substract; // QC mode mod qc; // positioning mode mod positioning; -// filegen mode -mod filegen; + +// file operations +mod fops; + +use fops::{filegen, merge, split, substract, time_binning}; pub struct Cli { /// Arguments passed by user diff --git a/rinex-cli/src/fops.rs b/rinex-cli/src/fops.rs index b2cd6db04..6081439cb 100644 --- a/rinex-cli/src/fops.rs +++ b/rinex-cli/src/fops.rs @@ -9,15 +9,98 @@ use std::str::FromStr; use rinex::{ prelude::{Duration, Epoch, ProductType, Rinex, RinexType}, preprocessing::*, + prod::{DataSource, DetailedProductionAttributes, ProductionAttributes, FFU, PPU}, Merge, Split, }; +/* + * Parses share RINEX production attributes. + * This helps accurate file production, + * and also allows customization from files that did not originally follow + * standard naming conventions + */ +fn custom_prod_attributes(rinex: &Rinex, matches: &ArgMatches) -> ProductionAttributes { + // Start from smartly guessed attributes and replace + // manually customized fields + let mut opts = rinex.guess_production_attributes(); + if let Some(agency) = matches.get_one::("agency") { + opts.name = agency.to_string(); + } + if let Some(country) = matches.get_one::("country") { + if let Some(ref mut details) = opts.details { + details.country = country[..3].to_string(); + } else { + let mut default = DetailedProductionAttributes::default(); + default.country = country[..3].to_string(); + opts.details = Some(default); + } + } + if let Some(batch) = matches.get_one::("batch") { + if let Some(ref mut details) = opts.details { + details.batch = *batch; + } else { + let mut default = DetailedProductionAttributes::default(); + default.batch = *batch; + opts.details = Some(default); + } + } + if let Some(src) = matches.get_one::("source") { + if let Some(ref mut details) = opts.details { + details.data_src = *src; + } else { + let mut default = DetailedProductionAttributes::default(); + default.data_src = *src; + opts.details = Some(default); + } + } + if let Some(ppu) = matches.get_one::("ppu") { + if let Some(ref mut details) = opts.details { + details.ppu = *ppu; + } else { + let mut default = DetailedProductionAttributes::default(); + default.ppu = *ppu; + opts.details = Some(default); + } + } + if let Some(ffu) = matches.get_one::("ffu") { + if let Some(ref mut details) = opts.details { + details.ffu = Some(*ffu); + } else { + let mut default = DetailedProductionAttributes::default(); + default.ffu = Some(*ffu); + opts.details = Some(default); + } + } + opts +} + +/* + * Returns output filename to be generated, for this kind of Product + * TODO: some customization might impact the Header section + * that we should slightly rework, to be 100% correct + */ +fn output_filename(rinex: &Rinex, matches: &ArgMatches, prod: ProductionAttributes) -> String { + // Parse possible custom opts + let short = matches.get_flag("short"); + let gzip = if matches.get_flag("gzip") { + Some(".gz") + } else { + None + }; + + debug!("{:?}", prod); + + // Use smart determination + rinex.standard_filename(short, gzip, Some(prod)) +} + /* * Dumps current context (usually preprocessed) * into RINEX format maintaining consistent format */ -pub fn filegen(ctx: &Context, _matches: &ArgMatches) -> Result<(), Error> { +pub fn filegen(ctx: &Context, matches: &ArgMatches) -> Result<(), Error> { let ctx_data = &ctx.data; + for product in [ ProductType::Observation, ProductType::MeteoObservation, @@ -27,15 +110,8 @@ pub fn filegen(ctx: &Context, _matches: &ArgMatches) -> Result<(), Error> { ProductType::Antex, ] { if let Some(rinex) = ctx_data.rinex(product) { - let filename = ctx_data - .files(product) - .expect(&format!("failed to determine {} output", product)) - .get(0) - .expect(&format!("failed to determine {} output", product)) - .file_name() - .expect(&format!("failed to determine {} output", product)) - .to_string_lossy() - .to_string(); + let prod = custom_prod_attributes(rinex, matches); + let filename = output_filename(rinex, matches, prod); let output_path = ctx.workspace.join(filename).to_string_lossy().to_string(); @@ -113,7 +189,7 @@ pub fn split(ctx: &Context, matches: &ArgMatches) -> Result<(), Error> { let first_epoch = rinex_a .first_epoch() - .expect(&format!("failed to determine {} file suffix", product)); + .unwrap_or_else(|| panic!("failed to determine {} file suffix", product)); let (y, m, d, hh, mm, ss, _) = first_epoch.to_gregorian_utc(); let file_suffix = format!( @@ -123,13 +199,13 @@ pub fn split(ctx: &Context, matches: &ArgMatches) -> Result<(), Error> { let path = ctx_data .files(product) - .expect(&format!("failed to determine output {} filename", product)) - .get(0) + .unwrap_or_else(|| panic!("failed to determine output {} filename", product)) + .first() .unwrap(); let filename = path .file_stem() - .expect(&format!("failed to determine output {} filename", product)) + .unwrap_or_else(|| panic!("failed to determine output {} filename", product)) .to_string_lossy() .to_string(); @@ -178,8 +254,8 @@ pub fn split(ctx: &Context, matches: &ArgMatches) -> Result<(), Error> { let path = ctx_data .files(product) - .expect(&format!("failed to determine output {} filename", product)) - .get(0) + .unwrap_or_else(|| panic!("failed to determine output {} filename", product)) + .first() .unwrap(); let filename = path @@ -211,7 +287,7 @@ pub fn time_binning(ctx: &Context, matches: &ArgMatches) -> Result<(), Error> { .expect("duration is required"); if *duration == Duration::ZERO { - panic!("invalid duration"); + panic!("invalid (null) duration"); } for product in [ @@ -233,46 +309,17 @@ pub fn time_binning(ctx: &Context, matches: &ArgMatches) -> Result<(), Error> { let mut last = first + *duration; - // filename determination - let data_path = ctx_data - .files(product) - .unwrap() - .get(0) - .expect(&format!("failed to determine output {} file name", product)); - - let filename = data_path - .file_stem() - .expect(&format!("failed to determine output {} file name", product)) - .to_string_lossy() - .to_string(); - - let mut extension = String::new(); - - let filename = if filename.contains('.') { - /* .crx.gz case */ - let mut iter = filename.split('.'); - let filename = iter - .next() - .expect(&format!("failed to determine output {} file name", product)) - .to_string(); - extension.push_str( - iter.next() - .expect(&format!("failed to determine output {} file name", product)), - ); - extension.push('.'); - filename + // production attributes: initialize Batch counter + let mut batch = 0_u8; + let mut prod = custom_prod_attributes(rinex, matches); + if let Some(ref mut details) = prod.details { + details.batch = batch; } else { - filename.clone() + let mut details = DetailedProductionAttributes::default(); + details.batch = batch; + prod.details = Some(details); }; - let file_ext = data_path - .extension() - .expect(&format!("failed to determine output {} file name", product)) - .to_string_lossy() - .to_string(); - - extension.push_str(&file_ext); - // run time binning algorithm while last <= end { let rinex = rinex @@ -280,19 +327,20 @@ pub fn time_binning(ctx: &Context, matches: &ArgMatches) -> Result<(), Error> { .filter(Filter::from_str(&format!(">= {:?}", first)).unwrap()); let (y, m, d, hh, mm, ss, _) = first.to_gregorian_utc(); - let file_suffix = format!("{}{}{}_{}{}{}{}", y, m, d, hh, mm, ss, first.time_scale); - let output = ctx - .workspace - .join(&format!("{}-{}.{}", filename, file_suffix, extension)) - .to_string_lossy() - .to_string(); + // generate standardized name + let filename = output_filename(&rinex, matches, prod.clone()); + + let output = ctx.workspace.join(&filename).to_string_lossy().to_string(); rinex.to_file(&output)?; info!("{} RINEX \"{}\" has been generated", product, output); first += *duration; last += *duration; + if let Some(ref mut details) = prod.details { + details.batch += 1; + } } } } @@ -307,7 +355,7 @@ pub fn substract(ctx: &Context, matches: &ArgMatches) -> Result<(), Error> { let path_a = ctx_data .files(ProductType::Observation) .expect("failed to determine output file name") - .get(0) + .first() .unwrap(); let path_b = matches.get_one::("file").unwrap(); diff --git a/rinex-cli/src/graph/record/navigation.rs b/rinex-cli/src/graph/record/navigation.rs index efe4d45ba..b992ac614 100644 --- a/rinex-cli/src/graph/record/navigation.rs +++ b/rinex-cli/src/graph/record/navigation.rs @@ -130,7 +130,7 @@ fn plot_sv_clock_states(ctx: &CtxClockStates, nav_sv: &Vec, plot_ctx: &mut P }, }; for (index, (sv, results)) in vehicles.iter().enumerate() { - if !nav_sv.contains(&sv) { + if !nav_sv.contains(sv) { continue; } let sv_epochs = results.iter().map(|(t, _)| *t).collect::>(); @@ -192,7 +192,7 @@ fn ctx_sv_clock_corrections( obs: &Rinex, nav: &Rinex, clk: Option<&Rinex>, - sp3: Option<&SP3>, + _sp3: Option<&SP3>, ) -> CtxClockCorrections { let mut clock_corr = CtxClockCorrections::new(); for ((t, flag), (_, vehicles)) in obs.observation() { @@ -251,7 +251,7 @@ fn ctx_sv_clock_corrections( // insert result if let Some(inner) = clock_corr.get_mut(&product) { - if let Some(inner) = inner.get_mut(&sv) { + if let Some(inner) = inner.get_mut(sv) { inner.push((*t, correction)); } else { inner.insert(*sv, vec![(*t, correction)]); @@ -301,7 +301,7 @@ fn plot_sv_clock_corrections(ctx: &CtxClockCorrections, plot_ctx: &mut PlotConte } for (index, (sv, data)) in vehicles .iter() - .filter(|(sv, data)| sv.constellation.timescale().unwrap() == ts) + .filter(|(sv, _data)| sv.constellation.timescale().unwrap() == ts) .enumerate() { let epochs = data.iter().map(|(t, _)| *t).collect::>(); @@ -351,14 +351,14 @@ fn plot_system_time( }, } for (_, state_vehicles) in states.iter().filter(|(k, _)| *k == product) { - for (index, (sv, corrections)) in vehicles + for (_index, (sv, _corrections)) in vehicles .iter() - .filter(|(sv, data)| sv.constellation.timescale() == Some(ts)) + .filter(|(sv, _data)| sv.constellation.timescale() == Some(ts)) .enumerate() { - if let Some((_, data)) = state_vehicles + if let Some((_, _data)) = state_vehicles .iter() - .filter(|(state_sv, data)| *state_sv == sv) + .filter(|(state_sv, _data)| *state_sv == sv) .reduce(|k, _| k) { //FIXME: conclude this graph diff --git a/rinex-cli/src/graph/record/observation.rs b/rinex-cli/src/graph/record/observation.rs index 70c7abf08..b4ff23c55 100644 --- a/rinex-cli/src/graph/record/observation.rs +++ b/rinex-cli/src/graph/record/observation.rs @@ -1,9 +1,11 @@ use crate::cli::Context; -use crate::graph::{build_chart_epoch_axis, csv_export_timedomain, generate_markers, PlotContext}; use plotly::common::{Marker, MarkerSymbol, Mode, Visible}; -use rinex::{observation::*, prelude::*}; use std::collections::HashMap; +use rinex::{navigation::Ephemeris, observation::*, prelude::*}; + +use crate::graph::{build_chart_epoch_axis, csv_export_timedomain, generate_markers, PlotContext}; + fn observable_to_physics(observable: &Observable) -> String { if observable.is_phase_observable() { "Phase".to_string() @@ -122,7 +124,7 @@ pub fn plot_observations(ctx: &Context, plot_context: &mut PlotContext, csv_expo _ => unreachable!(), }; - if ctx.data.has_brdc_navigation() { + if ctx.data.has_brdc_navigation() && ctx.data.has_sp3() { // Augmented context, we plot data on two Y axes // one for physical observation, one for sat elevation plot_context.add_timedomain_2y_plot( @@ -174,25 +176,35 @@ pub fn plot_observations(ctx: &Context, plot_context: &mut PlotContext, csv_expo } if index == 0 && physics == "Signal Strength" { - // 1st Carrier encountered: plot SV only once - // we also only augment the SSI plot when NAV context is provided - if let Some(nav) = &ctx.data.brdc_navigation() { - // grab elevation angle - let data: Vec<(Epoch, f64)> = nav - .sv_elevation_azimuth(ctx.data.ground_position()) - .map(|(epoch, _sv, (elev, _a))| (epoch, elev)) - .collect(); - // plot (Epoch, Elev) - let epochs: Vec = data.iter().map(|(e, _)| *e).collect(); - let elev: Vec = data.iter().map(|(_, f)| *f).collect(); + // Draw SV elevation along SSI plot if that is feasible + if let Some(sp3) = ctx.data.sp3() { + // determine SV state + let rx_ecef = ctx.rx_ecef.unwrap(); + let data = data_x + .iter() + .filter_map(|t| { + sp3.sv_position_interpolate(*sv, *t, 5) + .map(|pos| (*t, Ephemeris::elevation_azimuth(pos, rx_ecef).0)) + }) + .collect::>(); + // plot + let data_x = data.iter().map(|(x, _)| *x).collect::>(); + let data_y = data.iter().map(|(_, y)| *y).collect::>(); let trace = build_chart_epoch_axis( &format!("Elev({:X})", sv), - Mode::LinesMarkers, - epochs, - elev, + Mode::Markers, + data_x, + data_y, ) + .y_axis("y2") .marker(Marker::new().symbol(markers[index].clone())) - .visible(Visible::LegendOnly); + .visible({ + if index < 1 { + Visible::True + } else { + Visible::LegendOnly + } + }); plot_context.add_trace(trace); } } diff --git a/rinex-cli/src/main.rs b/rinex-cli/src/main.rs index d3c9cdcc9..e3addc1d0 100644 --- a/rinex-cli/src/main.rs +++ b/rinex-cli/src/main.rs @@ -133,8 +133,7 @@ fn user_data_parsing(cli: &Cli) -> RnxContext { /* * Preprocess whole context */ - preprocess(&mut ctx, &cli); - + preprocess(&mut ctx, cli); debug!("{:?}", ctx); ctx } diff --git a/rinex-cli/src/positioning/cggtts/mod.rs b/rinex-cli/src/positioning/cggtts/mod.rs index 6e9404c67..4ec5c1bbf 100644 --- a/rinex-cli/src/positioning/cggtts/mod.rs +++ b/rinex-cli/src/positioning/cggtts/mod.rs @@ -85,9 +85,9 @@ where let meteo_data = ctx.data.meteo(); let clk_data = ctx.data.clock(); - let has_clk_data = clk_data.is_some(); + let _has_clk_data = clk_data.is_some(); - let sp3_data = ctx.data.sp3(); + let _sp3_data = ctx.data.sp3(); let sp3_has_clock = ctx.data.sp3_has_clock(); let dominant_sampling_period = obs_data diff --git a/rinex-cli/src/positioning/ppp/mod.rs b/rinex-cli/src/positioning/ppp/mod.rs index 8ea3a94f0..2770bb106 100644 --- a/rinex-cli/src/positioning/ppp/mod.rs +++ b/rinex-cli/src/positioning/ppp/mod.rs @@ -1,11 +1,14 @@ //! PPP solver use crate::cli::Context; use crate::positioning::{bd_model, kb_model, ng_model, tropo_components}; -use rinex::carrier::Carrier; -use rinex::navigation::Ephemeris; -use rinex::prelude::SV; use std::collections::BTreeMap; +use rinex::{ + carrier::Carrier, + navigation::Ephemeris, + prelude::{Duration, SV}, +}; + mod post_process; pub use post_process::{post_process, Error as PostProcessingError}; @@ -31,14 +34,24 @@ where let clk_data = ctx.data.clock(); let meteo_data = ctx.data.meteo(); + let sp3_data = ctx.data.sp3(); let sp3_has_clock = ctx.data.sp3_has_clock(); + if clk_data.is_none() && sp3_has_clock { + if let Some(sp3) = sp3_data { + warn!("Using clock states defined in SP3 file: CLK product should be prefered"); + if sp3.epoch_interval >= Duration::from_seconds(300.0) { + warn!("interpolating clock states from low sample rate SP3 will most likely introduce errors"); + } + } + } + for ((t, flag), (_clk, vehicles)) in obs_data.observation() { let mut candidates = Vec::::with_capacity(4); if !flag.is_ok() { - /* we only consider "OK" epochs" */ + /* we only consider _valid_ epochs" */ continue; } @@ -53,18 +66,18 @@ where for (sv, observations) in vehicles { let sv_eph = nav_data.sv_ephemeris(*sv, *t); if sv_eph.is_none() { - warn!("{:?} ({}) : undetermined ephemeris", t, sv); + debug!("{:?} ({}) : undetermined ephemeris", t, sv); continue; // can't proceed further } // determine TOE - let (toe, sv_eph) = sv_eph.unwrap(); + let (toe, _sv_eph) = sv_eph.unwrap(); /* * Clock state - * 1. Prefer CLK product - * 2. Prefer SP3 product - * 3. Radio last option: always feasible + * 1. Prefer CLK product: best quality + * 2. Prefer SP3 product: most likely incompatible with very precise PPP + * 3. BRDC Radio last option: always feasible but most likely very noisy/wrong */ let clock_state = if let Some(clk) = clk_data { if let Some((_, profile)) = clk.precise_sv_clock_interpolate(*t, *sv) { @@ -81,9 +94,25 @@ where continue; } } else if sp3_has_clock { - panic!("sp3 (clock) not ready yet: prefer broadcast or clk product"); + if let Some(sp3) = sp3_data { + if let Some(bias) = sp3.sv_clock_interpolate(*t, *sv) { + // FIXME: + // slightly rework SP3 to expose drift + driftr better + (bias, 0.0_f64, 0.0_f64) + } else { + /* + * interpolation failure. + * Do not interpolate other products: SV will not be presented. + */ + continue; + } + } else { + // FIXME: BRDC interpolation + continue; + } } else { - sv_eph.sv_clock() // BRDC case + // FIXME: BRDC interpolation + continue; }; // determine clock correction diff --git a/rinex-cli/src/positioning/ppp/post_process.rs b/rinex-cli/src/positioning/ppp/post_process.rs index 758fbe548..330930095 100644 --- a/rinex-cli/src/positioning/ppp/post_process.rs +++ b/rinex-cli/src/positioning/ppp/post_process.rs @@ -119,8 +119,8 @@ pub fn post_process( * largest error */ for error in results - .iter() - .map(|(_, pvt)| (pvt.pos.x.powi(2) + pvt.pos.y.powi(2) + pvt.pos.z.powi(2)).sqrt()) + .values() + .map(|pvt| (pvt.pos.x.powi(2) + pvt.pos.y.powi(2) + pvt.pos.z.powi(2)).sqrt()) { if error > worst_radius { worst_radius = error; diff --git a/rinex-cli/src/preprocessing.rs b/rinex-cli/src/preprocessing.rs index ca0024d70..9c9acfa26 100644 --- a/rinex-cli/src/preprocessing.rs +++ b/rinex-cli/src/preprocessing.rs @@ -1,10 +1,515 @@ +use itertools::Itertools; use log::error; use std::str::FromStr; use crate::Cli; -use rinex::prelude::RnxContext; +use rinex::prelude::{Epoch, RnxContext}; use rinex::preprocessing::*; +use sp3::prelude::{DataType as SP3DataType, SP3}; + +/* + * SP3 toolkit does not implement the Processing Traits + * since they're currently defined in RINEX.. + * Work around this by implementing the ""typical"" preprocessing ops + * manually here. This allows to shrink the SP3 context, which + * is quite heavy, and make future Epoch iterations much quicker + */ +fn sp3_filter_mut(filter: Filter, sp3: &mut SP3) { + match filter { + Filter::Mask(mask) => sp3_mask_mut(mask, sp3), + Filter::Decimation(decim) => sp3_decimate_mut(decim, sp3), + _ => {}, // does not apply + } +} + +fn sp3_mask_mut(mask: MaskFilter, sp3: &mut SP3) { + match mask.operand { + MaskOperand::Equals => match mask.item { + TargetItem::EpochItem(epoch) => { + sp3.clock.retain(|t, _| *t == epoch); + sp3.clock_rate.retain(|t, _| *t == epoch); + sp3.position.retain(|t, _| *t == epoch); + sp3.velocities.retain(|t, _| *t == epoch); + }, + TargetItem::ConstellationItem(constells) => { + sp3.clock.retain(|_t, data| { + data.retain(|sv, _| constells.contains(&sv.constellation)); + !data.is_empty() + }); + sp3.clock_rate.retain(|_t, data| { + data.retain(|sv, _| constells.contains(&sv.constellation)); + !data.is_empty() + }); + sp3.position.retain(|_t, data| { + data.retain(|sv, _| constells.contains(&sv.constellation)); + !data.is_empty() + }); + sp3.velocities.retain(|_t, data| { + data.retain(|sv, _| constells.contains(&sv.constellation)); + !data.is_empty() + }); + }, + TargetItem::SvItem(svs) => { + sp3.clock.retain(|_t, data| { + data.retain(|sv, _| svs.contains(sv)); + !data.is_empty() + }); + sp3.clock_rate.retain(|_t, data| { + data.retain(|sv, _| svs.contains(sv)); + !data.is_empty() + }); + sp3.position.retain(|_t, data| { + data.retain(|sv, _| svs.contains(sv)); + !data.is_empty() + }); + sp3.velocities.retain(|_t, data| { + data.retain(|sv, _| svs.contains(sv)); + !data.is_empty() + }); + }, + _ => {}, // does not apply + }, + MaskOperand::NotEquals => match mask.item { + TargetItem::EpochItem(epoch) => { + sp3.clock.retain(|t, _| *t != epoch); + sp3.clock_rate.retain(|t, _| *t != epoch); + sp3.position.retain(|t, _| *t != epoch); + sp3.velocities.retain(|t, _| *t != epoch); + }, + TargetItem::ConstellationItem(constells) => { + sp3.clock.retain(|_t, data| { + data.retain(|sv, _| !constells.contains(&sv.constellation)); + !data.is_empty() + }); + sp3.clock_rate.retain(|_t, data| { + data.retain(|sv, _| !constells.contains(&sv.constellation)); + !data.is_empty() + }); + sp3.position.retain(|_t, data| { + data.retain(|sv, _| !constells.contains(&sv.constellation)); + !data.is_empty() + }); + sp3.velocities.retain(|_t, data| { + data.retain(|sv, _| !constells.contains(&sv.constellation)); + !data.is_empty() + }); + }, + TargetItem::SvItem(svs) => { + sp3.clock.retain(|_t, data| { + data.retain(|sv, _| !svs.contains(sv)); + !data.is_empty() + }); + sp3.clock_rate.retain(|_t, data| { + data.retain(|sv, _| !svs.contains(sv)); + !data.is_empty() + }); + sp3.position.retain(|_t, data| { + data.retain(|sv, _| !svs.contains(sv)); + !data.is_empty() + }); + sp3.velocities.retain(|_t, data| { + data.retain(|sv, _| !svs.contains(sv)); + !data.is_empty() + }); + }, + _ => {}, // does not apply + }, + MaskOperand::GreaterEquals => match mask.item { + TargetItem::EpochItem(epoch) => { + sp3.clock.retain(|t, _| *t >= epoch); + sp3.clock_rate.retain(|t, _| *t >= epoch); + sp3.position.retain(|t, _| *t >= epoch); + sp3.velocities.retain(|t, _| *t >= epoch); + }, + TargetItem::SvItem(svs) => { + let constells = svs + .iter() + .map(|sv| sv.constellation) + .unique() + .collect::>(); + sp3.clock.retain(|_t, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + >= svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + sp3.clock_rate.retain(|_t, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + >= svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + sp3.position.retain(|_t, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + >= svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + sp3.velocities.retain(|_t, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + >= svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + }, + _ => {}, // does not apply + }, + MaskOperand::GreaterThan => match mask.item { + TargetItem::EpochItem(epoch) => { + sp3.clock.retain(|t, _| *t > epoch); + sp3.clock_rate.retain(|t, _| *t > epoch); + sp3.position.retain(|t, _| *t > epoch); + sp3.position.retain(|t, _| *t > epoch); + }, + TargetItem::SvItem(svs) => { + let constells = svs + .iter() + .map(|sv| sv.constellation) + .unique() + .collect::>(); + sp3.clock.retain(|_t, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + > svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + sp3.clock_rate.retain(|_t, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + > svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + sp3.position.retain(|_t, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + > svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + sp3.velocities.retain(|_t, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + > svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + }, + _ => {}, // does not apply + }, + MaskOperand::LowerThan => match mask.item { + TargetItem::EpochItem(epoch) => { + sp3.clock.retain(|t, _| *t < epoch); + sp3.clock_rate.retain(|t, _| *t < epoch); + sp3.position.retain(|t, _| *t < epoch); + sp3.velocities.retain(|t, _| *t < epoch); + }, + TargetItem::SvItem(svs) => { + let constells = svs + .iter() + .map(|sv| sv.constellation) + .unique() + .collect::>(); + sp3.clock.retain(|_t, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + < svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + sp3.clock_rate.retain(|_t, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + < svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + sp3.position.retain(|_t, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + < svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + sp3.velocities.retain(|_t, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + < svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + }, + _ => {}, // does not apply + }, + MaskOperand::LowerEquals => match mask.item { + TargetItem::EpochItem(epoch) => { + sp3.clock.retain(|t, _| *t <= epoch); + sp3.clock_rate.retain(|t, _| *t <= epoch); + sp3.position.retain(|t, _| *t <= epoch); + sp3.velocities.retain(|t, _| *t <= epoch); + }, + TargetItem::SvItem(svs) => { + let constells = svs + .iter() + .map(|sv| sv.constellation) + .unique() + .collect::>(); + sp3.clock.retain(|_t, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + <= svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + sp3.clock_rate.retain(|_t, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + <= svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + sp3.position.retain(|_, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + <= svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + sp3.velocities.retain(|_, data| { + data.retain(|sv, _| { + constells.contains(&sv.constellation) + && sv.prn + <= svs + .iter() + .filter(|svs| svs.constellation == sv.constellation) + .reduce(|k, _| k) + .unwrap() + .prn + }); + !data.is_empty() + }); + }, + _ => {}, // does not apply + }, + } +} + +fn sp3_decimate_mut(decim: DecimationFilter, sp3: &mut SP3) { + match decim.dtype { + DecimationType::DecimByRatio(r) => { + let mut i = 0; + sp3.clock.retain(|_, _| { + let retained = (i % r) == 0; + i += 1; + retained + }); + let mut i = 0; + sp3.clock_rate.retain(|_, _| { + let retained = (i % r) == 0; + i += 1; + retained + }); + let mut i = 0; + sp3.position.retain(|_, _| { + let retained = (i % r) == 0; + i += 1; + retained + }); + let mut i = 0; + sp3.velocities.retain(|_, _| { + let retained = (i % r) == 0; + i += 1; + retained + }); + }, + DecimationType::DecimByInterval(interval) => { + let mut last_retained = Option::::None; + sp3.clock.retain(|t, _| { + if let Some(last) = last_retained { + let dt = *t - last; + if dt >= interval { + last_retained = Some(*t); + true + } else { + false + } + } else { + last_retained = Some(*t); + true // always retain 1st Epoch + } + }); + let mut last_retained = Option::::None; + sp3.clock_rate.retain(|t, _| { + if let Some(last) = last_retained { + let dt = *t - last; + if dt >= interval { + last_retained = Some(*t); + true + } else { + false + } + } else { + last_retained = Some(*t); + true // always retain 1st Epoch + } + }); + let mut last_retained = Option::::None; + sp3.position.retain(|t, _| { + if let Some(last) = last_retained { + let dt = *t - last; + if dt >= interval { + last_retained = Some(*t); + true + } else { + false + } + } else { + last_retained = Some(*t); + true // always retain 1st Epoch + } + }); + let mut last_retained = Option::::None; + sp3.velocities.retain(|t, _| { + if let Some(last) = last_retained { + let dt = *t - last; + if dt >= interval { + last_retained = Some(*t); + true + } else { + false + } + } else { + last_retained = Some(*t); + true // always retain 1st Epoch + } + }); + }, + } +} + +/* + * Once SP3 payload has been reworked, + * we rework its header fields to the remaining payload. + * This keeps file header consistent and allows for example + * to generate a new SP3 that is consistent and correct. + */ +pub fn sp3_rework_mut(sp3: &mut SP3) { + let svs = sp3 + .sv_position() + .map(|(_, sv, _)| sv) + .unique() + .collect::>(); + sp3.sv.retain(|sv| svs.contains(sv)); + + let epochs = sp3 + .sv_position() + .map(|(t, _, _)| t) + .unique() + .collect::>(); + sp3.epoch.retain(|t| epochs.contains(t)); + + if sp3.data_type == SP3DataType::Velocity && sp3.sv_velocities().count() == 0 { + // dropped all Velocity information + sp3.data_type = SP3DataType::Position; + } +} + pub fn preprocess(ctx: &mut RnxContext, cli: &Cli) { // GNSS filters let mut gnss_filters = Vec::<&str>::new(); @@ -50,7 +555,7 @@ pub fn preprocess(ctx: &mut RnxContext, cli: &Cli) { inner.filter_mut(filter.clone()); } if let Some(inner) = ctx.sp3_mut() { - //TODO + sp3_filter_mut(filter, inner); } } @@ -58,7 +563,7 @@ pub fn preprocess(ctx: &mut RnxContext, cli: &Cli) { /* * Apply all preprocessing filters */ - if let Ok(filter) = Filter::from_str(&filt_str) { + if let Ok(filter) = Filter::from_str(filt_str) { if let Some(ref mut inner) = ctx.observation_mut() { inner.filter_mut(filter.clone()); } @@ -71,9 +576,25 @@ pub fn preprocess(ctx: &mut RnxContext, cli: &Cli) { if let Some(ref mut inner) = ctx.clock_mut() { inner.filter_mut(filter.clone()); } + if let Some(ref mut inner) = ctx.sp3_mut() { + sp3_filter_mut(filter.clone(), inner); + } + if let Some(ref mut _inner) = ctx.ionex_mut() { + // FIXME: conclude IONEX preprocessing. + // Time framing most importantly, will be useful + } trace!("applied filter \"{}\"", filt_str); } else { error!("invalid filter description \"{}\"", filt_str); } } + + /* + * see [sp3_rework_mut] + */ + if let Some(ref mut inner) = ctx.sp3_mut() { + if !cli.preprocessing().is_empty() { + sp3_rework_mut(inner); + } + } } diff --git a/rinex/src/antex/record.rs b/rinex/src/antex/record.rs index eeb56d0bf..174ca678d 100644 --- a/rinex/src/antex/record.rs +++ b/rinex/src/antex/record.rs @@ -385,25 +385,6 @@ pub(crate) fn parse_antenna( Ok((antenna, inner)) } -#[cfg(test)] -mod test { - use super::*; - #[test] - fn test_new_epoch() { - let content = " START OF ANTENNA"; - assert!(is_new_epoch(content)); - let content = - "TROSAR25.R4 LEIT727259 TYPE / SERIAL NO"; - assert!(!is_new_epoch(content)); - let content = - " 26 # OF FREQUENCIES"; - assert!(!is_new_epoch(content)); - let content = - " G01 START OF FREQUENCY"; - assert!(!is_new_epoch(content)); - } -} - impl Merge for Record { /// Merges `rhs` into `Self` without mutable access at the expense of more memcopies fn merge(&self, rhs: &Self) -> Result { @@ -444,3 +425,22 @@ impl Merge for Record { Ok(()) } } + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_new_epoch() { + let content = " START OF ANTENNA"; + assert!(is_new_epoch(content)); + let content = + "TROSAR25.R4 LEIT727259 TYPE / SERIAL NO"; + assert!(!is_new_epoch(content)); + let content = + " 26 # OF FREQUENCIES"; + assert!(!is_new_epoch(content)); + let content = + " G01 START OF FREQUENCY"; + assert!(!is_new_epoch(content)); + } +} diff --git a/rinex/src/clock/record.rs b/rinex/src/clock/record.rs index f32d760a9..7e76132af 100644 --- a/rinex/src/clock/record.rs +++ b/rinex/src/clock/record.rs @@ -309,6 +309,151 @@ pub(crate) fn fmt_epoch(epoch: &Epoch, key: &ClockKey, prof: &ClockProfile) -> S lines } +use crate::merge::merge_mut_option; + +impl Merge for Record { + /// Merges `rhs` into `Self` without mutable access at the expense of more memcopies + fn merge(&self, rhs: &Self) -> Result { + let mut lhs = self.clone(); + lhs.merge_mut(rhs)?; + Ok(lhs) + } + /// Merges `rhs` into `Self` + fn merge_mut(&mut self, rhs: &Self) -> Result<(), merge::Error> { + for (rhs_epoch, rhs_content) in rhs.iter() { + if let Some(lhs_content) = self.get_mut(rhs_epoch) { + for (rhs_key, rhs_prof) in rhs_content.iter() { + if let Some(lhs_prof) = lhs_content.get_mut(rhs_key) { + // enhance only, if possible + merge_mut_option(&mut lhs_prof.drift, &rhs_prof.drift); + merge_mut_option(&mut lhs_prof.drift_dev, &rhs_prof.drift_dev); + merge_mut_option(&mut lhs_prof.drift_change, &rhs_prof.drift_change); + merge_mut_option( + &mut lhs_prof.drift_change_dev, + &rhs_prof.drift_change_dev, + ); + } else { + lhs_content.insert(rhs_key.clone(), rhs_prof.clone()); + } + } + } else { + self.insert(*rhs_epoch, rhs_content.clone()); + } + } + Ok(()) + } +} + +impl Split for Record { + fn split(&self, epoch: Epoch) -> Result<(Self, Self), split::Error> { + let r0 = self + .iter() + .flat_map(|(k, v)| { + if k <= &epoch { + Some((*k, v.clone())) + } else { + None + } + }) + .collect(); + let r1 = self + .iter() + .flat_map(|(k, v)| { + if k > &epoch { + Some((*k, v.clone())) + } else { + None + } + }) + .collect(); + Ok((r0, r1)) + } + fn split_dt(&self, _duration: Duration) -> Result, split::Error> { + Ok(Vec::new()) + } +} + +#[cfg(feature = "processing")] +use crate::preprocessing::*; + +#[cfg(feature = "processing")] +impl Mask for Record { + fn mask(&self, mask: MaskFilter) -> Self { + let mut s = self.clone(); + s.mask_mut(mask); + s + } + fn mask_mut(&mut self, mask: MaskFilter) { + match mask.operand { + MaskOperand::Equals => match mask.item { + TargetItem::EpochItem(epoch) => self.retain(|e, _| *e == epoch), + TargetItem::ConstellationItem(mask) => { + self.retain(|_, data| { + data.retain(|sysclk, _| { + if let Some(sv) = sysclk.clock_type.as_sv() { + mask.contains(&sv.constellation) + } else { + false + } + }); + !data.is_empty() + }); + }, + _ => {}, // TargetItem:: + }, + MaskOperand::NotEquals => match mask.item { + TargetItem::EpochItem(epoch) => self.retain(|e, _| *e != epoch), + _ => {}, // TargetItem:: + }, + MaskOperand::GreaterEquals => match mask.item { + TargetItem::EpochItem(epoch) => self.retain(|e, _| *e >= epoch), + _ => {}, // TargetItem:: + }, + MaskOperand::GreaterThan => match mask.item { + TargetItem::EpochItem(epoch) => self.retain(|e, _| *e > epoch), + _ => {}, // TargetItem:: + }, + MaskOperand::LowerEquals => match mask.item { + TargetItem::EpochItem(epoch) => self.retain(|e, _| *e <= epoch), + _ => {}, // TargetItem:: + }, + MaskOperand::LowerThan => match mask.item { + TargetItem::EpochItem(epoch) => self.retain(|e, _| *e < epoch), + _ => {}, // TargetItem:: + }, + } + } +} + +#[cfg(feature = "processing")] +impl Preprocessing for Record { + fn filter(&self, f: Filter) -> Self { + let mut s = self.clone(); + s.filter_mut(f); + s + } + fn filter_mut(&mut self, f: Filter) { + match f { + Filter::Mask(mask) => self.mask_mut(mask), + Filter::Smoothing(_) => todo!(), + Filter::Decimation(_) => todo!(), + Filter::Interp(filter) => self.interpolate_mut(filter.series), + } + } +} + +#[cfg(feature = "processing")] +impl Interpolate for Record { + fn interpolate(&self, series: TimeSeries) -> Self { + let mut s = self.clone(); + s.interpolate_mut(series); + s + } + fn interpolate_mut(&mut self, _series: TimeSeries) { + unimplemented!("clocks:record:interpolate_mut()"); + } +} + #[cfg(test)] mod test { use super::*; @@ -406,7 +551,7 @@ mod test { ] { let (parsed_e, parsed_k, parsed_prof) = parse_epoch(Version { minor: 0, major: 2 }, descriptor) - .expect(&format!("failed to parse \"{}\"", descriptor)); + .unwrap_or_else(|_| panic!("failed to parse \"{}\"", descriptor)); assert_eq!(parsed_e, epoch, "parsed wrong epoch"); assert_eq!(parsed_k, key, "parsed wrong clock id"); @@ -485,7 +630,7 @@ mod test { ] { let (parsed_e, parsed_k, parsed_prof) = parse_epoch(Version { minor: 0, major: 2 }, descriptor) - .expect(&format!("failed to parse \"{}\"", descriptor)); + .unwrap_or_else(|_| panic!("failed to parse \"{}\"", descriptor)); assert_eq!(parsed_e, epoch, "parsed wrong epoch"); assert_eq!(parsed_k, key, "parsed wrong clock id"); @@ -493,148 +638,3 @@ mod test { } } } - -use crate::merge::merge_mut_option; - -impl Merge for Record { - /// Merges `rhs` into `Self` without mutable access at the expense of more memcopies - fn merge(&self, rhs: &Self) -> Result { - let mut lhs = self.clone(); - lhs.merge_mut(rhs)?; - Ok(lhs) - } - /// Merges `rhs` into `Self` - fn merge_mut(&mut self, rhs: &Self) -> Result<(), merge::Error> { - for (rhs_epoch, rhs_content) in rhs.iter() { - if let Some(lhs_content) = self.get_mut(&rhs_epoch) { - for (rhs_key, rhs_prof) in rhs_content.iter() { - if let Some(lhs_prof) = lhs_content.get_mut(&rhs_key) { - // enhance only, if possible - merge_mut_option(&mut lhs_prof.drift, &rhs_prof.drift); - merge_mut_option(&mut lhs_prof.drift_dev, &rhs_prof.drift_dev); - merge_mut_option(&mut lhs_prof.drift_change, &rhs_prof.drift_change); - merge_mut_option( - &mut lhs_prof.drift_change_dev, - &rhs_prof.drift_change_dev, - ); - } else { - lhs_content.insert(rhs_key.clone(), rhs_prof.clone()); - } - } - } else { - self.insert(*rhs_epoch, rhs_content.clone()); - } - } - Ok(()) - } -} - -impl Split for Record { - fn split(&self, epoch: Epoch) -> Result<(Self, Self), split::Error> { - let r0 = self - .iter() - .flat_map(|(k, v)| { - if k <= &epoch { - Some((*k, v.clone())) - } else { - None - } - }) - .collect(); - let r1 = self - .iter() - .flat_map(|(k, v)| { - if k > &epoch { - Some((*k, v.clone())) - } else { - None - } - }) - .collect(); - Ok((r0, r1)) - } - fn split_dt(&self, _duration: Duration) -> Result, split::Error> { - Ok(Vec::new()) - } -} - -#[cfg(feature = "processing")] -use crate::preprocessing::*; - -#[cfg(feature = "processing")] -impl Mask for Record { - fn mask(&self, mask: MaskFilter) -> Self { - let mut s = self.clone(); - s.mask_mut(mask); - s - } - fn mask_mut(&mut self, mask: MaskFilter) { - match mask.operand { - MaskOperand::Equals => match mask.item { - TargetItem::EpochItem(epoch) => self.retain(|e, _| *e == epoch), - TargetItem::ConstellationItem(mask) => { - self.retain(|_, data| { - data.retain(|sysclk, _| { - if let Some(sv) = sysclk.clock_type.as_sv() { - mask.contains(&sv.constellation) - } else { - false - } - }); - !data.is_empty() - }); - }, - _ => {}, // TargetItem:: - }, - MaskOperand::NotEquals => match mask.item { - TargetItem::EpochItem(epoch) => self.retain(|e, _| *e != epoch), - _ => {}, // TargetItem:: - }, - MaskOperand::GreaterEquals => match mask.item { - TargetItem::EpochItem(epoch) => self.retain(|e, _| *e >= epoch), - _ => {}, // TargetItem:: - }, - MaskOperand::GreaterThan => match mask.item { - TargetItem::EpochItem(epoch) => self.retain(|e, _| *e > epoch), - _ => {}, // TargetItem:: - }, - MaskOperand::LowerEquals => match mask.item { - TargetItem::EpochItem(epoch) => self.retain(|e, _| *e <= epoch), - _ => {}, // TargetItem:: - }, - MaskOperand::LowerThan => match mask.item { - TargetItem::EpochItem(epoch) => self.retain(|e, _| *e < epoch), - _ => {}, // TargetItem:: - }, - } - } -} - -#[cfg(feature = "processing")] -impl Preprocessing for Record { - fn filter(&self, f: Filter) -> Self { - let mut s = self.clone(); - s.filter_mut(f); - s - } - fn filter_mut(&mut self, f: Filter) { - match f { - Filter::Mask(mask) => self.mask_mut(mask), - Filter::Smoothing(_) => todo!(), - Filter::Decimation(_) => todo!(), - Filter::Interp(filter) => self.interpolate_mut(filter.series), - } - } -} - -#[cfg(feature = "processing")] -impl Interpolate for Record { - fn interpolate(&self, series: TimeSeries) -> Self { - let mut s = self.clone(); - s.interpolate_mut(series); - s - } - fn interpolate_mut(&mut self, _series: TimeSeries) { - unimplemented!("clocks:record:interpolate_mut()"); - } -} diff --git a/rinex/src/context.rs b/rinex/src/context.rs index f68418cd2..d33b4369e 100644 --- a/rinex/src/context.rs +++ b/rinex/src/context.rs @@ -151,7 +151,7 @@ impl RnxContext { /* * Returns Fist file loaded in this category */ - return paths.get(0); + return paths.first(); } } None @@ -299,6 +299,10 @@ impl RnxContext { pub fn has_brdc_navigation(&self) -> bool { self.brdc_navigation().is_some() } + /// Returns true if [ProductType::HighPrecisionOrbit] are present in Self + pub fn has_sp3(&self) -> bool { + self.sp3().is_some() + } /// Returns true if [ProductType::MeteoObservation] are present in Self pub fn has_meteo(&self) -> bool { self.meteo().is_some() diff --git a/rinex/src/hatanaka/compressor.rs b/rinex/src/hatanaka/compressor.rs index 19b9c19eb..209cd60b7 100644 --- a/rinex/src/hatanaka/compressor.rs +++ b/rinex/src/hatanaka/compressor.rs @@ -531,6 +531,7 @@ impl Compressor { }, } //match(state) } //main loop + result.push('\n'); Ok(result) } //notes: diff --git a/rinex/src/ionex/record.rs b/rinex/src/ionex/record.rs index 147d3ad36..d40ba1ea5 100644 --- a/rinex/src/ionex/record.rs +++ b/rinex/src/ionex/record.rs @@ -240,32 +240,6 @@ pub(crate) fn parse_plane( Ok((epoch, altitude, plane)) } -#[cfg(test)] -mod test { - use super::*; - #[test] - fn test_new_tec_map() { - assert!(is_new_tec_plane( - "1 START OF TEC MAP" - )); - assert!(!is_new_tec_plane( - "1 START OF RMS MAP" - )); - assert!(is_new_rms_plane( - "1 START OF RMS MAP" - )); - // assert_eq!( - // is_new_height_map( - // "1 START OF HEIGHT MAP" - // ), - // true - // ); - } - //#[test] - //fn test_merge_map2d() { - //} -} - impl Merge for Record { /// Merges `rhs` into `Self` without mutable access at the expense of more memcopies fn merge(&self, rhs: &Self) -> Result { @@ -393,3 +367,29 @@ impl Interpolate for Record { unimplemented!("ionex:record:interpolate()") } } + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_new_tec_map() { + assert!(is_new_tec_plane( + "1 START OF TEC MAP" + )); + assert!(!is_new_tec_plane( + "1 START OF RMS MAP" + )); + assert!(is_new_rms_plane( + "1 START OF RMS MAP" + )); + // assert_eq!( + // is_new_height_map( + // "1 START OF HEIGHT MAP" + // ), + // true + // ); + } + //#[test] + //fn test_merge_map2d() { + //} +} diff --git a/rinex/src/lib.rs b/rinex/src/lib.rs index 2a3e59ad4..d604f4e2d 100644 --- a/rinex/src/lib.rs +++ b/rinex/src/lib.rs @@ -65,7 +65,7 @@ use observable::Observable; use observation::Crinex; use version::Version; -use production::{ProductionAttributes, FFU, PPU}; +use production::{DataSource, DetailedProductionAttributes, ProductionAttributes, FFU, PPU}; use hifitime::Unit; //use hifitime::{efmt::Format as EpochFormat, efmt::Formatter as EpochFormatter, Duration, Unit}; @@ -90,6 +90,13 @@ pub mod prelude { pub use hifitime::{Duration, Epoch, TimeScale, TimeSeries}; } +/// Package dedicated to file production. +pub mod prod { + pub use crate::production::{ + DataSource, DetailedProductionAttributes, ProductionAttributes, FFU, PPU, + }; +} + #[cfg(feature = "processing")] mod algorithm; @@ -382,7 +389,7 @@ impl Rinex { }); } } - /// Returns a filename that would describe Self according to naming conventions. + /// Returns a filename that would describe Self according to standard naming conventions. /// For this information to be 100% complete, Self must come from a file /// that follows these conventions itself. /// Otherwise you must provide [ProductionAttributes] yourself with "custom". @@ -393,6 +400,11 @@ impl Rinex { /// Otherwse, we will prefer modern V3 like formats. /// Use "suffix" to append a custom suffix like ".gz" for example. /// NB this will only output uppercase filenames (as per standard specs). + /// ``` + /// use rinex::prelude::*; + /// // Parse a File that follows standard naming conventions + /// // and verify we generate something correct + /// ``` pub fn standard_filename( &self, short: bool, @@ -407,7 +419,9 @@ impl Rinex { let mut filename = match rinextype { RinexType::IonosphereMaps => { let name = match custom { - Some(ref custom) => custom.name.clone(), + Some(ref custom) => { + custom.name[..std::cmp::min(3, custom.name.len())].to_string() + }, None => { if let Some(attr) = &self.prod_attr { attr.name.clone() @@ -502,6 +516,28 @@ impl Rinex { ProductionAttributes::rinex_short_format(&name, &ddd, &yy, ext) } else { /* long /V3 like format */ + let batch = match &custom { + Some(ref custom) => { + if let Some(details) = &custom.details { + // details.batch + 0 + } else { + 0 + } + }, + None => { + if let Some(attr) = &self.prod_attr { + if let Some(details) = &attr.details { + // details.batch + 0 + } else { + 0 + } + } else { + 0 + } + }, + }; let country = match &custom { Some(ref custom) => { if let Some(details) = &custom.details { @@ -616,6 +652,7 @@ impl Rinex { let ext = if is_crinex { "crx" } else { "rnx" }; ProductionAttributes::rinex_long_format( &name, + batch, &country, src, &yyyy, @@ -636,6 +673,125 @@ impl Rinex { } filename } + + /// Guesses File [ProductionAttributes] from the actual Record content. + /// This is particularly useful when working with datasets we are confident about, + /// yet that do not follow standard naming conventions. + /// Here is an example of such use case: + /// ``` + /// use rinex::prelude::*; + /// + /// // Parse one file that does not follow naming conventions + /// let rinex = Rinex::from_file("../test_resources/MET/V4/example1.txt"); + /// assert!(rinex.is_ok()); // As previously stated, we totally accept that + /// let rinex = rinex.unwrap(); + /// + /// // The standard file name generator has no means to generate something correct. + /// let standard_name = rinex.standard_filename(true, None, None); + /// assert_eq!(standard_name, "XXXX0070.21M"); + /// + /// // We use the smart attributes detector as custom attributes + /// let guessed = rinex.guess_production_attributes(); + /// let standard_name = rinex.standard_filename(true, None, Some(guessed.clone())); + /// + /// // we get a perfect shortened name + /// assert_eq!(standard_name, "bako0070.21M"); + /// + /// // If we ask for a (modern) long standard filename, we mostly get it right, + /// // but some fields like the Country code can only be determined from the original filename, + /// // so we have no means to receover them. + /// let standard_name = rinex.standard_filename(false, None, Some(guessed.clone())); + /// assert_eq!(standard_name, "bako00XXX_U_20210070000_00U_MM.rnx"); + /// ``` + pub fn guess_production_attributes(&self) -> ProductionAttributes { + // start from content identified from the filename + let mut attributes = self.prod_attr.clone().unwrap_or_default(); + + let first_epoch = self.first_epoch(); + let last_epoch = self.last_epoch(); + let first_epoch_gregorian = first_epoch.map(|t0| t0.to_gregorian_utc()); + + match first_epoch_gregorian { + Some((y, _, _, _, _, _, _)) => attributes.year = y as u32, + _ => {}, + } + match first_epoch { + Some(t0) => attributes.doy = t0.day_of_year().round() as u32, + _ => {}, + } + // notes on attribute."name" + // - Non detailed OBS RINEX: this is usually the station name + // which can be named after a geodetic marker + // - Non detailed NAV RINEX: station name + // - CLK RINEX: name of the local clock + // - IONEX: agency + match self.header.rinex_type { + RinexType::ClockData => match &self.header.clock { + Some(clk) => match &clk.ref_clock { + Some(refclock) => attributes.name = refclock.to_string(), + _ => { + if let Some(site) = &clk.site { + attributes.name = site.to_string(); + } else { + attributes.name = self.header.agency.to_string(); + } + }, + }, + _ => attributes.name = self.header.agency.to_string(), + }, + RinexType::IonosphereMaps => { + attributes.name = self.header.agency.to_string(); + }, + _ => match &self.header.geodetic_marker { + Some(marker) => attributes.name = marker.name.to_string(), + _ => attributes.name = self.header.agency.to_string(), + }, + } + if let Some(ref mut details) = attributes.details { + if let Some((_, _, _, hh, mm, _, _)) = first_epoch_gregorian { + details.hh = hh; + details.mm = mm; + } + if let Some(first_epoch) = first_epoch { + if let Some(last_epoch) = last_epoch { + let total_dt = last_epoch - first_epoch; + details.ppu = PPU::from(total_dt); + } + } + } else { + attributes.details = Some(DetailedProductionAttributes { + batch: 0, // see notes down below + country: "XXX".to_string(), // see notes down below + data_src: DataSource::Unknown, // see notes down below + ppu: match (first_epoch, last_epoch) { + (Some(first), Some(last)) => { + let total_dt = last - first; + PPU::from(total_dt) + }, + _ => PPU::Unspecified, + }, + ffu: self.dominant_sample_rate().map(FFU::from), + hh: match first_epoch_gregorian { + Some((_, _, _, hh, _, _, _)) => hh, + _ => 0, + }, + mm: match first_epoch_gregorian { + Some((_, _, _, _, mm, _, _)) => mm, + _ => 0, + }, + }); + } + /* + * Several fields cannot be deduced from the actual + * Record content. If provided filename did not describe them, + * we have no means to recover them. + * Example of such fields would be: + * + Country Code: would require a worldwide country database + * + Data source: is only defined in the filename + */ + attributes + } + /// Builds a `RINEX` from given file fullpath. /// Header section must respect labelization standards, /// some are mandatory. @@ -2982,11 +3138,9 @@ impl Rinex { ) -> Box + '_> { Box::new(self.precise_clock().flat_map(|(epoch, rec)| { rec.iter().filter_map(|(key, profile)| { - if let Some(sv) = key.clock_type.as_sv() { - Some((*epoch, sv, key.profile_type.clone(), profile.clone())) - } else { - None - } + key.clock_type + .as_sv() + .map(|sv| (*epoch, sv, key.profile_type.clone(), profile.clone())) }) })) } @@ -3052,16 +3206,14 @@ impl Rinex { ) -> Box + '_> { Box::new(self.precise_clock().flat_map(|(epoch, rec)| { rec.iter().filter_map(|(key, profile)| { - if let Some(clk_name) = key.clock_type.as_station() { - Some(( + key.clock_type.as_station().map(|clk_name| { + ( *epoch, clk_name.clone(), key.profile_type.clone(), profile.clone(), - )) - } else { - None - } + ) + }) }) })) } diff --git a/rinex/src/meteo/record.rs b/rinex/src/meteo/record.rs index 2997f2b93..e3b3cd010 100644 --- a/rinex/src/meteo/record.rs +++ b/rinex/src/meteo/record.rs @@ -136,44 +136,6 @@ pub(crate) fn fmt_epoch( Ok(lines) } -#[cfg(test)] -mod test { - use super::*; - #[test] - fn test_new_epoch() { - let content = " 22 1 4 0 0 0 993.4 -6.8 52.9 1.6 337.0 0.0 0.0"; - assert!(is_new_epoch( - content, - version::Version { major: 2, minor: 0 } - )); - let content = " 22 1 4 0 0 0 993.4 -6.8 52.9 1.6 337.0 0.0 0.0"; - assert!(is_new_epoch( - content, - version::Version { major: 2, minor: 0 } - )); - let content = " 22 1 4 9 55 0 997.9 -6.4 54.2 2.9 342.0 0.0 0.0"; - assert!(is_new_epoch( - content, - version::Version { major: 2, minor: 0 } - )); - let content = " 22 1 4 10 0 0 997.9 -6.3 55.4 3.4 337.0 0.0 0.0"; - assert!(is_new_epoch( - content, - version::Version { major: 2, minor: 0 } - )); - let content = " 08 1 1 0 0 1 1018.0 25.1 75.9 1.4 95.0 0.0 0.0"; - assert!(is_new_epoch( - content, - version::Version { major: 2, minor: 0 } - )); - let content = " 2021 1 7 0 0 0 993.3 23.0 90.0"; - assert!(is_new_epoch( - content, - version::Version { major: 4, minor: 0 } - )); - } -} - impl Merge for Record { fn merge(&self, rhs: &Self) -> Result { let mut lhs = self.clone(); @@ -416,3 +378,41 @@ impl Interpolate for Record { todo!("meteo:record:interpolate_mut()") } } + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_new_epoch() { + let content = " 22 1 4 0 0 0 993.4 -6.8 52.9 1.6 337.0 0.0 0.0"; + assert!(is_new_epoch( + content, + version::Version { major: 2, minor: 0 } + )); + let content = " 22 1 4 0 0 0 993.4 -6.8 52.9 1.6 337.0 0.0 0.0"; + assert!(is_new_epoch( + content, + version::Version { major: 2, minor: 0 } + )); + let content = " 22 1 4 9 55 0 997.9 -6.4 54.2 2.9 342.0 0.0 0.0"; + assert!(is_new_epoch( + content, + version::Version { major: 2, minor: 0 } + )); + let content = " 22 1 4 10 0 0 997.9 -6.3 55.4 3.4 337.0 0.0 0.0"; + assert!(is_new_epoch( + content, + version::Version { major: 2, minor: 0 } + )); + let content = " 08 1 1 0 0 1 1018.0 25.1 75.9 1.4 95.0 0.0 0.0"; + assert!(is_new_epoch( + content, + version::Version { major: 2, minor: 0 } + )); + let content = " 2021 1 7 0 0 0 993.3 23.0 90.0"; + assert!(is_new_epoch( + content, + version::Version { major: 4, minor: 0 } + )); + } +} diff --git a/rinex/src/navigation/record.rs b/rinex/src/navigation/record.rs index 947d4e63c..2d8bfb212 100644 --- a/rinex/src/navigation/record.rs +++ b/rinex/src/navigation/record.rs @@ -538,657 +538,177 @@ fn fmt_epoch_v4(epoch: &Epoch, data: &Vec, header: &Header) -> Result< Ok(lines) } -#[cfg(test)] -mod test { - use super::*; - #[test] - fn new_epoch() { - // NAV V<3 - let line = - " 1 20 12 31 23 45 0.0 7.282570004460D-05 0.000000000000D+00 7.380000000000D+04"; - assert!(is_new_epoch(line, Version::new(1, 0))); - assert!(is_new_epoch(line, Version::new(2, 0))); - assert!(!is_new_epoch(line, Version::new(3, 0))); - assert!(!is_new_epoch(line, Version::new(4, 0))); - // NAV V<3 - let line = - " 2 21 1 1 11 45 0.0 4.610531032090D-04 1.818989403550D-12 4.245000000000D+04"; - assert!(is_new_epoch(line, Version::new(1, 0))); - assert!(is_new_epoch(line, Version::new(2, 0))); - assert!(!is_new_epoch(line, Version::new(3, 0))); - assert!(!is_new_epoch(line, Version::new(4, 0))); - // GPS NAV V<3 - let line = - " 3 17 1 13 23 59 44.0-1.057861372828D-04-9.094947017729D-13 0.000000000000D+00"; - assert!(is_new_epoch(line, Version::new(1, 0))); - assert!(is_new_epoch(line, Version::new(2, 0))); - assert!(!is_new_epoch(line, Version::new(3, 0))); - assert!(!is_new_epoch(line, Version::new(4, 0))); - // NAV V3 - let line = - "C05 2021 01 01 00 00 00-4.263372393325e-04-7.525180478751e-11 0.000000000000e+00"; - assert!(!is_new_epoch(line, Version::new(1, 0))); - assert!(!is_new_epoch(line, Version::new(2, 0))); - assert!(is_new_epoch(line, Version::new(3, 0))); - assert!(!is_new_epoch(line, Version::new(4, 0))); - // NAV V3 - let line = - "R21 2022 01 01 09 15 00-2.666609361768E-04-2.728484105319E-12 5.508000000000E+05"; - assert!(!is_new_epoch(line, Version::new(1, 0))); - assert!(!is_new_epoch(line, Version::new(2, 0))); - assert!(is_new_epoch(line, Version::new(3, 0))); - assert!(!is_new_epoch(line, Version::new(4, 0))); - // NAV V4 - let line = "> EPH G02 LNAV"; - assert!(!is_new_epoch(line, Version::new(2, 0))); - assert!(!is_new_epoch(line, Version::new(3, 0))); - assert!(is_new_epoch(line, Version::new(4, 0))); +impl Merge for Record { + /// Merges `rhs` into `Self` without mutable access at the expense of more memcopies + fn merge(&self, rhs: &Self) -> Result { + let mut lhs = self.clone(); + lhs.merge_mut(rhs)?; + Ok(lhs) } - #[test] - fn parse_glonass_v2() { - let content = - " 1 20 12 31 23 45 0.0 7.282570004460D-05 0.000000000000D+00 7.380000000000D+04 - -1.488799804690D+03-2.196182250980D+00 3.725290298460D-09 0.000000000000D+00 - 1.292880712890D+04-2.049269676210D+00 0.000000000000D+00 1.000000000000D+00 - 2.193169775390D+04 1.059645652770D+00-9.313225746150D-10 0.000000000000D+00"; - let version = Version::new(2, 0); - assert!(is_new_epoch(content, version)); - - let entry = parse_epoch(version, Constellation::Glonass, content); - assert!(entry.is_ok(), "failed to parse epoch {:?}", entry.err()); - - let (epoch, frame) = entry.unwrap(); - assert_eq!( - epoch, - Epoch::from_gregorian_utc(2020, 12, 31, 23, 45, 00, 00) - ); - - let fr = frame.as_eph(); - assert!(fr.is_some()); - - let (msg_type, sv, ephemeris) = fr.unwrap(); - assert_eq!(msg_type, NavMsgType::LNAV); - assert_eq!( - sv, - SV { - constellation: Constellation::Glonass, - prn: 1, - } - ); - assert_eq!(ephemeris.clock_bias, 7.282570004460E-05); - assert_eq!(ephemeris.clock_drift, 0.0); - assert_eq!(ephemeris.clock_drift_rate, 7.38E4); - let orbits = &ephemeris.orbits; - assert_eq!(orbits.len(), 12); - for (k, v) in orbits.iter() { - if k.eq("satPosX") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -1.488799804690E+03); - } else if k.eq("velX") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -2.196182250980E+00); - } else if k.eq("accelX") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 3.725290298460E-09); - } else if k.eq("health") { - let v = v.as_glo_health(); - assert!(v.is_some()); - } else if k.eq("satPosY") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 1.292880712890E+04); - } else if k.eq("velY") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -2.049269676210E+00); - } else if k.eq("accelY") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.0); - } else if k.eq("channel") { - let v = v.as_i8(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 1); - } else if k.eq("satPosZ") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 2.193169775390E+04); - } else if k.eq("velZ") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 1.059645652770E+00); - } else if k.eq("accelZ") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -9.313225746150E-10); - } else if k.eq("ageOp") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.0); + /// Merges `rhs` into `Self` + fn merge_mut(&mut self, rhs: &Self) -> Result<(), merge::Error> { + for (rhs_epoch, rhs_frames) in rhs { + if let Some(frames) = self.get_mut(rhs_epoch) { + // this epoch already exists + for fr in rhs_frames { + if !frames.contains(fr) { + frames.push(fr.clone()); // insert new NavFrame + } + } } else { - panic!("Got unexpected key \"{}\" for GLOV2 record", k); + // insert new epoch + self.insert(*rhs_epoch, rhs_frames.clone()); } } + Ok(()) } - #[test] - fn parse_beidou_v3() { - let content = - "C05 2021 01 01 00 00 00 -.426337239332e-03 -.752518047875e-10 .000000000000e+00 - .100000000000e+01 .118906250000e+02 .105325815814e-08 -.255139531119e+01 - .169500708580e-06 .401772442274e-03 .292365439236e-04 .649346986580e+04 - .432000000000e+06 .105705112219e-06 -.277512444499e+01 -.211410224438e-06 - .607169709798e-01 -.897671875000e+03 .154887266488e+00 -.871464871438e-10 - -.940753471872e-09 .000000000000e+00 .782000000000e+03 .000000000000e+00 - .200000000000e+01 .000000000000e+00 -.599999994133e-09 -.900000000000e-08 - .432000000000e+06 .000000000000e+00 0.000000000000e+00 0.000000000000e+00"; - let version = Version::new(3, 0); - let entry = parse_epoch(version, Constellation::Mixed, content); - assert!(entry.is_ok()); +} - let (epoch, frame) = entry.unwrap(); - assert_eq!(epoch, Epoch::from_str("2021-01-01T00:00:00 BDT").unwrap()); +impl Split for Record { + fn split(&self, epoch: Epoch) -> Result<(Self, Self), split::Error> { + let r0 = self + .iter() + .flat_map(|(k, v)| { + if k < &epoch { + Some((*k, v.clone())) + } else { + None + } + }) + .collect(); + let r1 = self + .iter() + .flat_map(|(k, v)| { + if k >= &epoch { + Some((*k, v.clone())) + } else { + None + } + }) + .collect(); + Ok((r0, r1)) + } + fn split_dt(&self, _duration: Duration) -> Result, split::Error> { + Ok(Vec::new()) + } +} - let fr = frame.as_eph(); - assert!(fr.is_some()); +#[cfg(feature = "processing")] +use crate::preprocessing::{ + Decimate, DecimationType, Filter, Interpolate, Mask, MaskFilter, MaskOperand, Preprocessing, + TargetItem, +}; - let (msg_type, sv, ephemeris) = fr.unwrap(); - assert_eq!(msg_type, NavMsgType::LNAV); - assert_eq!( - sv, - SV { - constellation: Constellation::BeiDou, - prn: 5, +#[cfg(feature = "processing")] +fn mask_mut_equal(rec: &mut Record, target: TargetItem) { + match target { + TargetItem::EpochItem(epoch) => rec.retain(|e, _| *e == epoch), + TargetItem::SvItem(filter) => { + rec.retain(|_, frames| { + frames.retain(|fr| { + if let Some((_, sv, _)) = fr.as_eph() { + filter.contains(&sv) + } else { + // Only applies to Ephemeris frames + false + } + }); + !frames.is_empty() + }); + }, + TargetItem::ConstellationItem(filter) => { + let mut broad_sbas_filter = false; + for c in &filter { + broad_sbas_filter |= *c == Constellation::SBAS; } - ); - assert_eq!(ephemeris.clock_bias, -0.426337239332E-03); - assert_eq!(ephemeris.clock_drift, -0.752518047875e-10); - assert_eq!(ephemeris.clock_drift_rate, 0.0); - let orbits = &ephemeris.orbits; - assert_eq!(orbits.len(), 24); - for (k, v) in orbits.iter() { - if k.eq("aode") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.100000000000e+01); - } else if k.eq("crs") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.118906250000e+02); - } else if k.eq("deltaN") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.105325815814e-08); - } else if k.eq("m0") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.255139531119e+01); - } else if k.eq("cuc") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.169500708580e-06); - } else if k.eq("e") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.401772442274e-03); - } else if k.eq("cus") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.292365439236e-04); - } else if k.eq("sqrta") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.649346986580e+04); - } else if k.eq("toe") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.432000000000e+06); - } else if k.eq("cic") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.105705112219e-06); - } else if k.eq("omega0") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.277512444499e+01); - } else if k.eq("cis") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.211410224438e-06); - } else if k.eq("i0") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.607169709798e-01); - } else if k.eq("crc") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.897671875000e+03); - } else if k.eq("omega") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.154887266488e+00); - } else if k.eq("omegaDot") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.871464871438e-10); - } else if k.eq("idot") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.940753471872e-09); - // SPARE - } else if k.eq("week") { - let v = v.as_u32(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 782); - //SPARE - } else if k.eq("svAccuracy") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.200000000000e+01); - } else if k.eq("satH1") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.000000000000e+00); - } else if k.eq("tgd1b1b3") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.599999994133e-09); - } else if k.eq("tgd2b2b3") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.900000000000e-08); - } else if k.eq("t_tm") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.432000000000e+06); - } else if k.eq("aodc") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.000000000000e+00); - } else { - panic!("Got unexpected key \"{}\" for BDSV3 record", k); - } - } - } - #[test] - fn parse_galileo_v3() { - let content = - "E01 2021 01 01 10 10 00 -.101553811692e-02 -.804334376880e-11 .000000000000e+00 - .130000000000e+02 .435937500000e+02 .261510892978e-08 -.142304064404e+00 - .201165676117e-05 .226471573114e-03 .109840184450e-04 .544061822701e+04 - .468600000000e+06 .111758708954e-07 -.313008275208e+01 .409781932831e-07 - .980287270202e+00 .113593750000e+03 -.276495796017e+00 -.518200156545e-08 - -.595381942905e-09 .258000000000e+03 .213800000000e+04 0.000000000000e+00 - .312000000000e+01 .000000000000e+00 .232830643654e-09 .000000000000e+00 - .469330000000e+06 0.000000000000e+00 0.000000000000e+00 0.000000000000e+00"; - let version = Version::new(3, 0); - let entry = parse_epoch(version, Constellation::Mixed, content); - assert!(entry.is_ok()); - - let (epoch, frame) = entry.unwrap(); - assert_eq!(epoch, Epoch::from_str("2021-01-01T10:10:00 GST").unwrap(),); - - let fr = frame.as_eph(); - assert!(fr.is_some()); - - let (msg_type, sv, ephemeris) = fr.unwrap(); - assert_eq!(msg_type, NavMsgType::LNAV); - assert_eq!( - sv, - SV { - constellation: Constellation::Galileo, - prn: 1, - } - ); - assert_eq!(ephemeris.clock_bias, -0.101553811692e-02); - assert_eq!(ephemeris.clock_drift, -0.804334376880e-11); - assert_eq!(ephemeris.clock_drift_rate, 0.0); - let orbits = &ephemeris.orbits; - assert_eq!(orbits.len(), 24); - for (k, v) in orbits.iter() { - if k.eq("iodnav") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.130000000000e+02); - } else if k.eq("crs") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.435937500000e+02); - } else if k.eq("deltaN") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.261510892978e-08); - } else if k.eq("m0") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.142304064404e+00); - } else if k.eq("cuc") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.201165676117e-05); - } else if k.eq("e") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.226471573114e-03); - } else if k.eq("cus") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.109840184450e-04); - } else if k.eq("sqrta") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.544061822701e+04); - } else if k.eq("toe") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.468600000000e+06); - } else if k.eq("cic") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.111758708954e-07); - } else if k.eq("omega0") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.313008275208e+01); - } else if k.eq("cis") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.409781932831e-07); - } else if k.eq("i0") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.980287270202e+00); - } else if k.eq("crc") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.113593750000e+03); - } else if k.eq("omega") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.276495796017e+00); - } else if k.eq("omegaDot") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.518200156545e-08); - } else if k.eq("idot") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.595381942905e-09); - } else if k.eq("dataSrc") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.258000000000e+03); - } else if k.eq("week") { - let v = v.as_u32(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 2138); - //SPARE - } else if k.eq("sisa") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.312000000000e+01); - } else if k.eq("health") { - let v = v.as_gal_health(); - assert!(v.is_some()); - } else if k.eq("bgdE5aE1") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.232830643654e-09); - } else if k.eq("bgdE5bE1") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.000000000000e+00); - } else if k.eq("t_tm") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.469330000000e+06); - } else { - panic!("Got unexpected key \"{}\" for GALV3 record", k); - } - } - } - #[test] - fn parse_glonass_v3() { - let content = - "R07 2021 01 01 09 45 00 -.420100986958e-04 .000000000000e+00 .342000000000e+05 - .124900639648e+05 .912527084351e+00 .000000000000e+00 .000000000000e+00 - .595546582031e+04 .278496932983e+01 .000000000000e+00 .500000000000e+01 - .214479208984e+05 -.131077289581e+01 -.279396772385e-08 .000000000000e+00"; - let version = Version::new(3, 0); - let entry = parse_epoch(version, Constellation::Mixed, content); - assert!(entry.is_ok()); - let (epoch, frame) = entry.unwrap(); - assert_eq!( - epoch, - Epoch::from_gregorian_utc(2021, 01, 01, 09, 45, 00, 00) - ); - - let fr = frame.as_eph(); - assert!(fr.is_some()); - let (msg_type, sv, ephemeris) = fr.unwrap(); - assert_eq!(msg_type, NavMsgType::LNAV); - assert_eq!( - sv, - SV { - constellation: Constellation::Glonass, - prn: 7, - } - ); - assert_eq!(ephemeris.clock_bias, -0.420100986958e-04); - assert_eq!(ephemeris.clock_drift, 0.000000000000e+00); - assert_eq!(ephemeris.clock_drift_rate, 0.342000000000e+05); - let orbits = &ephemeris.orbits; - assert_eq!(orbits.len(), 12); - for (k, v) in orbits.iter() { - if k.eq("satPosX") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.124900639648e+05); - } else if k.eq("velX") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.912527084351e+00); - } else if k.eq("accelX") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.000000000000e+00); - } else if k.eq("health") { - let v = v.as_glo_health(); - assert!(v.is_some()); - } else if k.eq("satPosY") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.595546582031e+04); - } else if k.eq("velY") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.278496932983e+01); - } else if k.eq("accelY") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.000000000000e+00); - } else if k.eq("channel") { - let v = v.as_i8(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 5); - } else if k.eq("satPosZ") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.214479208984e+05); - } else if k.eq("velZ") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.131077289581e+01); - } else if k.eq("accelZ") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, -0.279396772385e-08); - } else if k.eq("ageOp") { - let v = v.as_f64(); - assert!(v.is_some()); - let v = v.unwrap(); - assert_eq!(v, 0.000000000000e+00); - } else { - panic!("Got unexpected key \"{}\" for GLOV3 record", k); - } - } - } - #[test] - fn format_rework() { - let content = "1000123 -123123e-1 -1.23123123e0 -0.123123e-4"; - assert_eq!( - fmt_rework(2, content), - "1000123 -123123D-01 -1.23123123D+00 -0.123123D-04" - ); - assert_eq!( - fmt_rework(3, content), - "1000123 -123123E-01 -1.23123123E+00 -0.123123E-04" - ); - } -} - -impl Merge for Record { - /// Merges `rhs` into `Self` without mutable access at the expense of more memcopies - fn merge(&self, rhs: &Self) -> Result { - let mut lhs = self.clone(); - lhs.merge_mut(rhs)?; - Ok(lhs) - } - /// Merges `rhs` into `Self` - fn merge_mut(&mut self, rhs: &Self) -> Result<(), merge::Error> { - for (rhs_epoch, rhs_frames) in rhs { - if let Some(frames) = self.get_mut(rhs_epoch) { - // this epoch already exists - for fr in rhs_frames { - if !frames.contains(fr) { - frames.push(fr.clone()); // insert new NavFrame + rec.retain(|_, frames| { + frames.retain(|fr| { + if let Some((_, sv, _)) = fr.as_eph() { + if broad_sbas_filter { + sv.constellation.is_sbas() || filter.contains(&sv.constellation) + } else { + filter.contains(&sv.constellation) + } + } else { + // Only applies to Ephemeris frames + false + } + }); + !frames.is_empty() + }); + }, + TargetItem::OrbitItem(_filter) => { + unimplemented!("orbititem filter"); + //self.retain(|_, frames| { + // frames.retain(|fr| { + // if let Some((_, _, ephemeris)) = fr.as_mut_eph() { + // let orbits = &mut ephemeris.orbits; + // orbits.retain(|k, _| filter.contains(&k)); + // orbits.len() > 0 + // } else { // other frames do not have "orbits" + // false + // } + // }); + // frames.len() > 0 + //}); + }, + TargetItem::NavFrameItem(filter) => { + rec.retain(|_, frames| { + frames.retain(|fr| { + if fr.as_eph().is_some() { + filter.contains(&FrameClass::Ephemeris) + } else if fr.as_eop().is_some() { + filter.contains(&FrameClass::EarthOrientation) + } else if fr.as_ion().is_some() { + filter.contains(&FrameClass::IonosphericModel) + } else if fr.as_sto().is_some() { + filter.contains(&FrameClass::SystemTimeOffset) + } else { + false + } + }); + !frames.is_empty() + }); + }, + TargetItem::NavMsgItem(filter) => { + rec.retain(|_, frames| { + frames.retain(|fr| { + if let Some((msg, _, _)) = fr.as_eph() { + filter.contains(&msg) + } else if let Some((msg, _, _)) = fr.as_ion() { + filter.contains(&msg) + } else if let Some((msg, _, _)) = fr.as_eop() { + filter.contains(&msg) + } else if let Some((msg, _, _)) = fr.as_sto() { + filter.contains(&msg) + } else { + false } - } - } else { - // insert new epoch - self.insert(*rhs_epoch, rhs_frames.clone()); - } - } - Ok(()) - } -} - -impl Split for Record { - fn split(&self, epoch: Epoch) -> Result<(Self, Self), split::Error> { - let r0 = self - .iter() - .flat_map(|(k, v)| { - if k < &epoch { - Some((*k, v.clone())) - } else { - None - } - }) - .collect(); - let r1 = self - .iter() - .flat_map(|(k, v)| { - if k >= &epoch { - Some((*k, v.clone())) - } else { - None - } - }) - .collect(); - Ok((r0, r1)) - } - fn split_dt(&self, _duration: Duration) -> Result, split::Error> { - Ok(Vec::new()) + }); + !frames.is_empty() + }); + }, + _ => {}, // Other items: either not supported, or do not apply } } #[cfg(feature = "processing")] -use crate::preprocessing::{ - Decimate, DecimationType, Filter, Interpolate, Mask, MaskFilter, MaskOperand, Preprocessing, - TargetItem, -}; - -#[cfg(feature = "processing")] -fn mask_mut_equal(rec: &mut Record, target: TargetItem) { +fn mask_mut_ineq(rec: &mut Record, target: TargetItem) { match target { - TargetItem::EpochItem(epoch) => rec.retain(|e, _| *e == epoch), + TargetItem::EpochItem(epoch) => rec.retain(|e, _| *e != epoch), TargetItem::SvItem(filter) => { rec.retain(|_, frames| { frames.retain(|fr| { if let Some((_, sv, _)) = fr.as_eph() { - filter.contains(&sv) + !filter.contains(&sv) + } else if let Some((_, sv, _)) = fr.as_ion() { + !filter.contains(&sv) + } else if let Some((_, sv, _)) = fr.as_eop() { + !filter.contains(&sv) + } else if let Some((_, sv, _)) = fr.as_sto() { + !filter.contains(&sv) } else { - // Only applies to Ephemeris frames + // non existing false } }); @@ -1196,20 +716,18 @@ fn mask_mut_equal(rec: &mut Record, target: TargetItem) { }); }, TargetItem::ConstellationItem(filter) => { - let mut broad_sbas_filter = false; - for c in &filter { - broad_sbas_filter |= *c == Constellation::SBAS; - } rec.retain(|_, frames| { frames.retain(|fr| { if let Some((_, sv, _)) = fr.as_eph() { - if broad_sbas_filter { - sv.constellation.is_sbas() || filter.contains(&sv.constellation) - } else { - filter.contains(&sv.constellation) - } + !filter.contains(&sv.constellation) + } else if let Some((_, sv, _)) = fr.as_ion() { + !filter.contains(&sv.constellation) + } else if let Some((_, sv, _)) = fr.as_eop() { + !filter.contains(&sv.constellation) + } else if let Some((_, sv, _)) = fr.as_sto() { + !filter.contains(&sv.constellation) } else { - // Only applies to Ephemeris frames + // non existing false } }); @@ -1218,7 +736,7 @@ fn mask_mut_equal(rec: &mut Record, target: TargetItem) { }, TargetItem::OrbitItem(_filter) => { unimplemented!("orbititem filter"); - //self.retain(|_, frames| { + //rec.retain(|_, frames| { // frames.retain(|fr| { // if let Some((_, _, ephemeris)) = fr.as_mut_eph() { // let orbits = &mut ephemeris.orbits; @@ -1235,13 +753,13 @@ fn mask_mut_equal(rec: &mut Record, target: TargetItem) { rec.retain(|_, frames| { frames.retain(|fr| { if fr.as_eph().is_some() { - filter.contains(&FrameClass::Ephemeris) + !filter.contains(&FrameClass::Ephemeris) } else if fr.as_eop().is_some() { - filter.contains(&FrameClass::EarthOrientation) + !filter.contains(&FrameClass::EarthOrientation) } else if fr.as_ion().is_some() { - filter.contains(&FrameClass::IonosphericModel) + !filter.contains(&FrameClass::IonosphericModel) } else if fr.as_sto().is_some() { - filter.contains(&FrameClass::SystemTimeOffset) + !filter.contains(&FrameClass::SystemTimeOffset) } else { false } @@ -1253,14 +771,176 @@ fn mask_mut_equal(rec: &mut Record, target: TargetItem) { rec.retain(|_, frames| { frames.retain(|fr| { if let Some((msg, _, _)) = fr.as_eph() { - filter.contains(&msg) + !filter.contains(&msg) } else if let Some((msg, _, _)) = fr.as_ion() { - filter.contains(&msg) + !filter.contains(&msg) } else if let Some((msg, _, _)) = fr.as_eop() { - filter.contains(&msg) + !filter.contains(&msg) } else if let Some((msg, _, _)) = fr.as_sto() { - filter.contains(&msg) + !filter.contains(&msg) + } else { + false + } + }); + !frames.is_empty() + }); + }, + _ => {}, // Other items: either not supported, or do not apply + } +} + +#[cfg(feature = "processing")] +fn mask_mut_leq(rec: &mut Record, target: TargetItem) { + match target { + TargetItem::EpochItem(epoch) => rec.retain(|e, _| *e <= epoch), + TargetItem::SvItem(filter) => { + // for each constell, grab PRN# + let filter: Vec<_> = filter.iter().map(|sv| (sv.constellation, sv.prn)).collect(); + + rec.retain(|_, frames| { + frames.retain(|fr| { + if let Some((_, sv, _)) = fr.as_eph() { + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn <= *prn; + } + } + pass + } else if let Some((_, sv, _)) = fr.as_ion() { + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn <= *prn; + } + } + pass + } else if let Some((_, sv, _)) = fr.as_eop() { + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn <= *prn; + } + } + pass + } else if let Some((_, sv, _)) = fr.as_sto() { + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn <= *prn; + } + } + pass + } else { + // non existing + false + } + }); + !frames.is_empty() + }); + }, + _ => {}, // Other items: either not supported, or do not apply + } +} + +#[cfg(feature = "processing")] +fn mask_mut_lt(rec: &mut Record, target: TargetItem) { + match target { + TargetItem::EpochItem(epoch) => rec.retain(|e, _| *e < epoch), + TargetItem::SvItem(filter) => { + // for each constell, grab PRN# + let filter: Vec<_> = filter.iter().map(|sv| (sv.constellation, sv.prn)).collect(); + + rec.retain(|_, frames| { + frames.retain(|fr| { + if let Some((_, sv, _)) = fr.as_eph() { + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn < *prn; + } + } + pass + } else if let Some((_, sv, _)) = fr.as_ion() { + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn < *prn; + } + } + pass + } else if let Some((_, sv, _)) = fr.as_eop() { + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn < *prn; + } + } + pass + } else if let Some((_, sv, _)) = fr.as_sto() { + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn < *prn; + } + } + pass + } else { + // non existing + false + } + }); + !frames.is_empty() + }); + }, + _ => {}, // Other items: either not supported, or do not apply + } +} + +#[cfg(feature = "processing")] +fn mask_mut_gt(rec: &mut Record, target: TargetItem) { + match target { + TargetItem::EpochItem(epoch) => rec.retain(|e, _| *e > epoch), + TargetItem::SvItem(filter) => { + // for each constell, grab PRN# + let filter: Vec<_> = filter.iter().map(|sv| (sv.constellation, sv.prn)).collect(); + + rec.retain(|_, frames| { + frames.retain(|fr| { + if let Some((_, sv, _)) = fr.as_eph() { + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn > *prn; + } + } + pass + } else if let Some((_, sv, _)) = fr.as_ion() { + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn > *prn; + } + } + pass + } else if let Some((_, sv, _)) = fr.as_eop() { + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn > *prn; + } + } + pass + } else if let Some((_, sv, _)) = fr.as_sto() { + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn > *prn; + } + } + pass } else { + // non existing false } }); @@ -1272,20 +952,47 @@ fn mask_mut_equal(rec: &mut Record, target: TargetItem) { } #[cfg(feature = "processing")] -fn mask_mut_ineq(rec: &mut Record, target: TargetItem) { +fn mask_mut_geq(rec: &mut Record, target: TargetItem) { match target { - TargetItem::EpochItem(epoch) => rec.retain(|e, _| *e != epoch), + TargetItem::EpochItem(epoch) => rec.retain(|e, _| *e >= epoch), TargetItem::SvItem(filter) => { + // for each constell, grab PRN# + let filter: Vec<_> = filter.iter().map(|sv| (sv.constellation, sv.prn)).collect(); + rec.retain(|_, frames| { frames.retain(|fr| { if let Some((_, sv, _)) = fr.as_eph() { - !filter.contains(&sv) + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn >= *prn; + } + } + pass } else if let Some((_, sv, _)) = fr.as_ion() { - !filter.contains(&sv) + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn >= *prn; + } + } + pass } else if let Some((_, sv, _)) = fr.as_eop() { - !filter.contains(&sv) + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn >= *prn; + } + } + pass } else if let Some((_, sv, _)) = fr.as_sto() { - !filter.contains(&sv) + let mut pass = false; + for (constell, prn) in &filter { + if *constell == sv.constellation { + pass |= sv.prn >= *prn; + } + } + pass } else { // non existing false @@ -1294,503 +1001,796 @@ fn mask_mut_ineq(rec: &mut Record, target: TargetItem) { !frames.is_empty() }); }, - TargetItem::ConstellationItem(filter) => { - rec.retain(|_, frames| { - frames.retain(|fr| { - if let Some((_, sv, _)) = fr.as_eph() { - !filter.contains(&sv.constellation) - } else if let Some((_, sv, _)) = fr.as_ion() { - !filter.contains(&sv.constellation) - } else if let Some((_, sv, _)) = fr.as_eop() { - !filter.contains(&sv.constellation) - } else if let Some((_, sv, _)) = fr.as_sto() { - !filter.contains(&sv.constellation) - } else { - // non existing - false - } - }); - !frames.is_empty() - }); + _ => {}, // Other items: either not supported, or do not apply + } +} + +#[cfg(feature = "processing")] +impl Mask for Record { + fn mask(&self, mask: MaskFilter) -> Self { + let mut s = self.clone(); + s.mask_mut(mask); + s + } + fn mask_mut(&mut self, mask: MaskFilter) { + match mask.operand { + MaskOperand::Equals => mask_mut_equal(self, mask.item), + MaskOperand::NotEquals => mask_mut_ineq(self, mask.item), + MaskOperand::GreaterThan => mask_mut_gt(self, mask.item), + MaskOperand::GreaterEquals => mask_mut_geq(self, mask.item), + MaskOperand::LowerThan => mask_mut_lt(self, mask.item), + MaskOperand::LowerEquals => mask_mut_leq(self, mask.item), + } + } +} + +/* + * Decimates only a given record subset + */ +#[cfg(feature = "processing")] +fn decimate_data_subset(record: &mut Record, subset: &Record, target: &TargetItem) { + match target { + TargetItem::SvItem(svs) => { + /* + * remove Sv data that should now be missing + */ + for (epoch, frames) in record.iter_mut() { + if subset.get(epoch).is_none() { + // for specified targets, this should now be removed + frames.retain(|fr| { + if let Some((_, sv, _)) = fr.as_eph() { + svs.contains(&sv) + } else if let Some((_, sv, _)) = fr.as_ion() { + svs.contains(&sv) + } else if let Some((_, sv, _)) = fr.as_sto() { + svs.contains(&sv) + } else if let Some((_, sv, _)) = fr.as_eop() { + svs.contains(&sv) + } else { + false // all cases already covered + } + }); + } + } }, - TargetItem::OrbitItem(_filter) => { - unimplemented!("orbititem filter"); - //rec.retain(|_, frames| { - // frames.retain(|fr| { - // if let Some((_, _, ephemeris)) = fr.as_mut_eph() { - // let orbits = &mut ephemeris.orbits; - // orbits.retain(|k, _| filter.contains(&k)); - // orbits.len() > 0 - // } else { // other frames do not have "orbits" - // false - // } - // }); - // frames.len() > 0 - //}); + TargetItem::ConstellationItem(constells_list) => { + /* + * Removes ephemeris frames that should now be missing + */ + for (epoch, frames) in record.iter_mut() { + if subset.get(epoch).is_none() { + // for specified targets, this should now be removed + frames.retain(|fr| { + if let Some((_, sv, _)) = fr.as_eph() { + constells_list.contains(&sv.constellation) + } else if let Some((_, sv, _)) = fr.as_ion() { + constells_list.contains(&sv.constellation) + } else if let Some((_, sv, _)) = fr.as_sto() { + constells_list.contains(&sv.constellation) + } else if let Some((_, sv, _)) = fr.as_eop() { + constells_list.contains(&sv.constellation) + } else { + false // all cases already covered + } + }); + } + } }, - TargetItem::NavFrameItem(filter) => { - rec.retain(|_, frames| { - frames.retain(|fr| { - if fr.as_eph().is_some() { - !filter.contains(&FrameClass::Ephemeris) - } else if fr.as_eop().is_some() { - !filter.contains(&FrameClass::EarthOrientation) - } else if fr.as_ion().is_some() { - !filter.contains(&FrameClass::IonosphericModel) - } else if fr.as_sto().is_some() { - !filter.contains(&FrameClass::SystemTimeOffset) - } else { - false - } - }); - !frames.is_empty() - }); + TargetItem::OrbitItem(_orbit_fields) => { + /* + * Removes ephemeris frames that should now be missing + */ + unimplemented!("specific orbit field decimation"); }, - TargetItem::NavMsgItem(filter) => { - rec.retain(|_, frames| { - frames.retain(|fr| { - if let Some((msg, _, _)) = fr.as_eph() { - !filter.contains(&msg) - } else if let Some((msg, _, _)) = fr.as_ion() { - !filter.contains(&msg) - } else if let Some((msg, _, _)) = fr.as_eop() { - !filter.contains(&msg) - } else if let Some((msg, _, _)) = fr.as_sto() { - !filter.contains(&msg) - } else { - false - } - }); - !frames.is_empty() - }); + TargetItem::AzimuthItem(_azim) => { + unimplemented!("navigation:record:decimate_data_subset(azim)"); }, - _ => {}, // Other items: either not supported, or do not apply + TargetItem::ElevationItem(_elev) => { + unimplemented!("navigation:record:decimate_data_subset(elev)"); + }, + TargetItem::NavFrameItem(_frame_classes) => { + unimplemented!("navigation:record:decimate_data_subset(navframe)"); + }, + TargetItem::NavMsgItem(_msg_types) => { + unimplemented!("navigation:record:decimate_data_subset(navmsg)"); + }, + _ => {}, // does not apply + } +} + +#[cfg(feature = "processing")] +impl Preprocessing for Record { + fn filter(&self, f: Filter) -> Self { + let mut s = self.clone(); + s.filter_mut(f); + s + } + fn filter_mut(&mut self, filt: Filter) { + match filt { + Filter::Mask(mask) => self.mask_mut(mask), + Filter::Interp(filter) => self.interpolate_mut(filter.series), + Filter::Decimation(filter) => match filter.dtype { + DecimationType::DecimByRatio(r) => { + if filter.target.is_none() { + self.decimate_by_ratio_mut(r); + return; // no need to proceed further + } + + let item = filter.target.unwrap(); + + // apply mask to retain desired subset + let mask = MaskFilter { + item: item.clone(), + operand: MaskOperand::Equals, + }; + + // decimate + let subset = self.mask(mask).decimate_by_ratio(r); + // adapt self's subset to new data rate + decimate_data_subset(self, &subset, &item); + }, + DecimationType::DecimByInterval(dt) => { + if filter.target.is_none() { + self.decimate_by_interval_mut(dt); + return; // no need to proceed further + } + + let item = filter.target.unwrap(); + + // apply mask to retain desired subset + let mask = MaskFilter { + item: item.clone(), + operand: MaskOperand::Equals, + }; + + // decimate + let subset = self.mask(mask).decimate_by_interval(dt); + // adapt self's subset to new data rate + decimate_data_subset(self, &subset, &item); + }, + }, + Filter::Smoothing(_) => unimplemented!("navigation:record:smoothing"), + } } } #[cfg(feature = "processing")] -fn mask_mut_leq(rec: &mut Record, target: TargetItem) { - match target { - TargetItem::EpochItem(epoch) => rec.retain(|e, _| *e <= epoch), - TargetItem::SvItem(filter) => { - // for each constell, grab PRN# - let filter: Vec<_> = filter.iter().map(|sv| (sv.constellation, sv.prn)).collect(); - - rec.retain(|_, frames| { - frames.retain(|fr| { - if let Some((_, sv, _)) = fr.as_eph() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn <= *prn; - } - } - pass - } else if let Some((_, sv, _)) = fr.as_ion() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn <= *prn; - } - } - pass - } else if let Some((_, sv, _)) = fr.as_eop() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn <= *prn; - } - } - pass - } else if let Some((_, sv, _)) = fr.as_sto() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn <= *prn; - } - } - pass - } else { - // non existing - false - } - }); - !frames.is_empty() - }); - }, - _ => {}, // Other items: either not supported, or do not apply +impl Interpolate for Record { + fn interpolate(&self, series: TimeSeries) -> Self { + let mut s = self.clone(); + s.interpolate_mut(series); + s + } + fn interpolate_mut(&mut self, _series: TimeSeries) { + unimplemented!("navigation:record:interpolate_mut()") } } #[cfg(feature = "processing")] -fn mask_mut_lt(rec: &mut Record, target: TargetItem) { - match target { - TargetItem::EpochItem(epoch) => rec.retain(|e, _| *e < epoch), - TargetItem::SvItem(filter) => { - // for each constell, grab PRN# - let filter: Vec<_> = filter.iter().map(|sv| (sv.constellation, sv.prn)).collect(); +impl Decimate for Record { + /// Decimates Self by desired factor + fn decimate_by_ratio_mut(&mut self, r: u32) { + let mut i = 0; + self.retain(|_, _| { + let retained = (i % r) == 0; + i += 1; + retained + }); + } + /// Copies and Decimates Self by desired factor + fn decimate_by_ratio(&self, r: u32) -> Self { + let mut s = self.clone(); + s.decimate_by_ratio_mut(r); + s + } + /// Decimates Self to fit minimum epoch interval + fn decimate_by_interval_mut(&mut self, interval: Duration) { + let mut last_retained = Option::::None; + self.retain(|e, _| { + if let Some(last) = last_retained { + let dt = *e - last; + if dt >= interval { + last_retained = Some(*e); + true + } else { + false + } + } else { + last_retained = Some(*e); + true // always retain 1st epoch + } + }); + } + fn decimate_by_interval(&self, dt: Duration) -> Self { + let mut s = self.clone(); + s.decimate_by_interval_mut(dt); + s + } + fn decimate_match_mut(&mut self, rhs: &Self) { + self.retain(|e, _| rhs.get(e).is_some()); + } + fn decimate_match(&self, rhs: &Self) -> Self { + let mut s = self.clone(); + s.decimate_match_mut(rhs); + s + } +} - rec.retain(|_, frames| { - frames.retain(|fr| { - if let Some((_, sv, _)) = fr.as_eph() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn < *prn; - } - } - pass - } else if let Some((_, sv, _)) = fr.as_ion() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn < *prn; - } - } - pass - } else if let Some((_, sv, _)) = fr.as_eop() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn < *prn; - } - } - pass - } else if let Some((_, sv, _)) = fr.as_sto() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn < *prn; - } - } - pass - } else { - // non existing - false - } - }); - !frames.is_empty() - }); - }, - _ => {}, // Other items: either not supported, or do not apply +#[cfg(test)] +mod test { + use super::*; + #[test] + fn new_epoch() { + // NAV V<3 + let line = + " 1 20 12 31 23 45 0.0 7.282570004460D-05 0.000000000000D+00 7.380000000000D+04"; + assert!(is_new_epoch(line, Version::new(1, 0))); + assert!(is_new_epoch(line, Version::new(2, 0))); + assert!(!is_new_epoch(line, Version::new(3, 0))); + assert!(!is_new_epoch(line, Version::new(4, 0))); + // NAV V<3 + let line = + " 2 21 1 1 11 45 0.0 4.610531032090D-04 1.818989403550D-12 4.245000000000D+04"; + assert!(is_new_epoch(line, Version::new(1, 0))); + assert!(is_new_epoch(line, Version::new(2, 0))); + assert!(!is_new_epoch(line, Version::new(3, 0))); + assert!(!is_new_epoch(line, Version::new(4, 0))); + // GPS NAV V<3 + let line = + " 3 17 1 13 23 59 44.0-1.057861372828D-04-9.094947017729D-13 0.000000000000D+00"; + assert!(is_new_epoch(line, Version::new(1, 0))); + assert!(is_new_epoch(line, Version::new(2, 0))); + assert!(!is_new_epoch(line, Version::new(3, 0))); + assert!(!is_new_epoch(line, Version::new(4, 0))); + // NAV V3 + let line = + "C05 2021 01 01 00 00 00-4.263372393325e-04-7.525180478751e-11 0.000000000000e+00"; + assert!(!is_new_epoch(line, Version::new(1, 0))); + assert!(!is_new_epoch(line, Version::new(2, 0))); + assert!(is_new_epoch(line, Version::new(3, 0))); + assert!(!is_new_epoch(line, Version::new(4, 0))); + // NAV V3 + let line = + "R21 2022 01 01 09 15 00-2.666609361768E-04-2.728484105319E-12 5.508000000000E+05"; + assert!(!is_new_epoch(line, Version::new(1, 0))); + assert!(!is_new_epoch(line, Version::new(2, 0))); + assert!(is_new_epoch(line, Version::new(3, 0))); + assert!(!is_new_epoch(line, Version::new(4, 0))); + // NAV V4 + let line = "> EPH G02 LNAV"; + assert!(!is_new_epoch(line, Version::new(2, 0))); + assert!(!is_new_epoch(line, Version::new(3, 0))); + assert!(is_new_epoch(line, Version::new(4, 0))); + } + #[test] + fn parse_glonass_v2() { + let content = + " 1 20 12 31 23 45 0.0 7.282570004460D-05 0.000000000000D+00 7.380000000000D+04 + -1.488799804690D+03-2.196182250980D+00 3.725290298460D-09 0.000000000000D+00 + 1.292880712890D+04-2.049269676210D+00 0.000000000000D+00 1.000000000000D+00 + 2.193169775390D+04 1.059645652770D+00-9.313225746150D-10 0.000000000000D+00"; + let version = Version::new(2, 0); + assert!(is_new_epoch(content, version)); + + let entry = parse_epoch(version, Constellation::Glonass, content); + assert!(entry.is_ok(), "failed to parse epoch {:?}", entry.err()); + + let (epoch, frame) = entry.unwrap(); + assert_eq!( + epoch, + Epoch::from_gregorian_utc(2020, 12, 31, 23, 45, 00, 00) + ); + + let fr = frame.as_eph(); + assert!(fr.is_some()); + + let (msg_type, sv, ephemeris) = fr.unwrap(); + assert_eq!(msg_type, NavMsgType::LNAV); + assert_eq!( + sv, + SV { + constellation: Constellation::Glonass, + prn: 1, + } + ); + assert_eq!(ephemeris.clock_bias, 7.282570004460E-05); + assert_eq!(ephemeris.clock_drift, 0.0); + assert_eq!(ephemeris.clock_drift_rate, 7.38E4); + let orbits = &ephemeris.orbits; + assert_eq!(orbits.len(), 12); + for (k, v) in orbits.iter() { + if k.eq("satPosX") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -1.488799804690E+03); + } else if k.eq("velX") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -2.196182250980E+00); + } else if k.eq("accelX") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 3.725290298460E-09); + } else if k.eq("health") { + let v = v.as_glo_health(); + assert!(v.is_some()); + } else if k.eq("satPosY") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 1.292880712890E+04); + } else if k.eq("velY") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -2.049269676210E+00); + } else if k.eq("accelY") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.0); + } else if k.eq("channel") { + let v = v.as_i8(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 1); + } else if k.eq("satPosZ") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 2.193169775390E+04); + } else if k.eq("velZ") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 1.059645652770E+00); + } else if k.eq("accelZ") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -9.313225746150E-10); + } else if k.eq("ageOp") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.0); + } else { + panic!("Got unexpected key \"{}\" for GLOV2 record", k); + } + } } -} + #[test] + fn parse_beidou_v3() { + let content = + "C05 2021 01 01 00 00 00 -.426337239332e-03 -.752518047875e-10 .000000000000e+00 + .100000000000e+01 .118906250000e+02 .105325815814e-08 -.255139531119e+01 + .169500708580e-06 .401772442274e-03 .292365439236e-04 .649346986580e+04 + .432000000000e+06 .105705112219e-06 -.277512444499e+01 -.211410224438e-06 + .607169709798e-01 -.897671875000e+03 .154887266488e+00 -.871464871438e-10 + -.940753471872e-09 .000000000000e+00 .782000000000e+03 .000000000000e+00 + .200000000000e+01 .000000000000e+00 -.599999994133e-09 -.900000000000e-08 + .432000000000e+06 .000000000000e+00 0.000000000000e+00 0.000000000000e+00"; + let version = Version::new(3, 0); + let entry = parse_epoch(version, Constellation::Mixed, content); + assert!(entry.is_ok()); -#[cfg(feature = "processing")] -fn mask_mut_gt(rec: &mut Record, target: TargetItem) { - match target { - TargetItem::EpochItem(epoch) => rec.retain(|e, _| *e > epoch), - TargetItem::SvItem(filter) => { - // for each constell, grab PRN# - let filter: Vec<_> = filter.iter().map(|sv| (sv.constellation, sv.prn)).collect(); + let (epoch, frame) = entry.unwrap(); + assert_eq!(epoch, Epoch::from_str("2021-01-01T00:00:00 BDT").unwrap()); - rec.retain(|_, frames| { - frames.retain(|fr| { - if let Some((_, sv, _)) = fr.as_eph() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn > *prn; - } - } - pass - } else if let Some((_, sv, _)) = fr.as_ion() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn > *prn; - } - } - pass - } else if let Some((_, sv, _)) = fr.as_eop() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn > *prn; - } - } - pass - } else if let Some((_, sv, _)) = fr.as_sto() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn > *prn; - } - } - pass - } else { - // non existing - false - } - }); - !frames.is_empty() - }); - }, - _ => {}, // Other items: either not supported, or do not apply + let fr = frame.as_eph(); + assert!(fr.is_some()); + + let (msg_type, sv, ephemeris) = fr.unwrap(); + assert_eq!(msg_type, NavMsgType::LNAV); + assert_eq!( + sv, + SV { + constellation: Constellation::BeiDou, + prn: 5, + } + ); + assert_eq!(ephemeris.clock_bias, -0.426337239332E-03); + assert_eq!(ephemeris.clock_drift, -0.752518047875e-10); + assert_eq!(ephemeris.clock_drift_rate, 0.0); + let orbits = &ephemeris.orbits; + assert_eq!(orbits.len(), 24); + for (k, v) in orbits.iter() { + if k.eq("aode") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.100000000000e+01); + } else if k.eq("crs") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.118906250000e+02); + } else if k.eq("deltaN") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.105325815814e-08); + } else if k.eq("m0") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.255139531119e+01); + } else if k.eq("cuc") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.169500708580e-06); + } else if k.eq("e") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.401772442274e-03); + } else if k.eq("cus") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.292365439236e-04); + } else if k.eq("sqrta") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.649346986580e+04); + } else if k.eq("toe") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.432000000000e+06); + } else if k.eq("cic") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.105705112219e-06); + } else if k.eq("omega0") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.277512444499e+01); + } else if k.eq("cis") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.211410224438e-06); + } else if k.eq("i0") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.607169709798e-01); + } else if k.eq("crc") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.897671875000e+03); + } else if k.eq("omega") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.154887266488e+00); + } else if k.eq("omegaDot") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.871464871438e-10); + } else if k.eq("idot") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.940753471872e-09); + // SPARE + } else if k.eq("week") { + let v = v.as_u32(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 782); + //SPARE + } else if k.eq("svAccuracy") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.200000000000e+01); + } else if k.eq("satH1") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.000000000000e+00); + } else if k.eq("tgd1b1b3") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.599999994133e-09); + } else if k.eq("tgd2b2b3") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.900000000000e-08); + } else if k.eq("t_tm") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.432000000000e+06); + } else if k.eq("aodc") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.000000000000e+00); + } else { + panic!("Got unexpected key \"{}\" for BDSV3 record", k); + } + } } -} - -#[cfg(feature = "processing")] -fn mask_mut_geq(rec: &mut Record, target: TargetItem) { - match target { - TargetItem::EpochItem(epoch) => rec.retain(|e, _| *e >= epoch), - TargetItem::SvItem(filter) => { - // for each constell, grab PRN# - let filter: Vec<_> = filter.iter().map(|sv| (sv.constellation, sv.prn)).collect(); + #[test] + fn parse_galileo_v3() { + let content = + "E01 2021 01 01 10 10 00 -.101553811692e-02 -.804334376880e-11 .000000000000e+00 + .130000000000e+02 .435937500000e+02 .261510892978e-08 -.142304064404e+00 + .201165676117e-05 .226471573114e-03 .109840184450e-04 .544061822701e+04 + .468600000000e+06 .111758708954e-07 -.313008275208e+01 .409781932831e-07 + .980287270202e+00 .113593750000e+03 -.276495796017e+00 -.518200156545e-08 + -.595381942905e-09 .258000000000e+03 .213800000000e+04 0.000000000000e+00 + .312000000000e+01 .000000000000e+00 .232830643654e-09 .000000000000e+00 + .469330000000e+06 0.000000000000e+00 0.000000000000e+00 0.000000000000e+00"; + let version = Version::new(3, 0); + let entry = parse_epoch(version, Constellation::Mixed, content); + assert!(entry.is_ok()); - rec.retain(|_, frames| { - frames.retain(|fr| { - if let Some((_, sv, _)) = fr.as_eph() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn >= *prn; - } - } - pass - } else if let Some((_, sv, _)) = fr.as_ion() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn >= *prn; - } - } - pass - } else if let Some((_, sv, _)) = fr.as_eop() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn >= *prn; - } - } - pass - } else if let Some((_, sv, _)) = fr.as_sto() { - let mut pass = false; - for (constell, prn) in &filter { - if *constell == sv.constellation { - pass |= sv.prn >= *prn; - } - } - pass - } else { - // non existing - false - } - }); - !frames.is_empty() - }); - }, - _ => {}, // Other items: either not supported, or do not apply - } -} + let (epoch, frame) = entry.unwrap(); + assert_eq!(epoch, Epoch::from_str("2021-01-01T10:10:00 GST").unwrap(),); -#[cfg(feature = "processing")] -impl Mask for Record { - fn mask(&self, mask: MaskFilter) -> Self { - let mut s = self.clone(); - s.mask_mut(mask); - s - } - fn mask_mut(&mut self, mask: MaskFilter) { - match mask.operand { - MaskOperand::Equals => mask_mut_equal(self, mask.item), - MaskOperand::NotEquals => mask_mut_ineq(self, mask.item), - MaskOperand::GreaterThan => mask_mut_gt(self, mask.item), - MaskOperand::GreaterEquals => mask_mut_geq(self, mask.item), - MaskOperand::LowerThan => mask_mut_lt(self, mask.item), - MaskOperand::LowerEquals => mask_mut_leq(self, mask.item), - } - } -} + let fr = frame.as_eph(); + assert!(fr.is_some()); -/* - * Decimates only a given record subset - */ -#[cfg(feature = "processing")] -fn decimate_data_subset(record: &mut Record, subset: &Record, target: &TargetItem) { - match target { - TargetItem::SvItem(svs) => { - /* - * remove Sv data that should now be missing - */ - for (epoch, frames) in record.iter_mut() { - if subset.get(epoch).is_none() { - // for specified targets, this should now be removed - frames.retain(|fr| { - if let Some((_, sv, _)) = fr.as_eph() { - svs.contains(&sv) - } else if let Some((_, sv, _)) = fr.as_ion() { - svs.contains(&sv) - } else if let Some((_, sv, _)) = fr.as_sto() { - svs.contains(&sv) - } else if let Some((_, sv, _)) = fr.as_eop() { - svs.contains(&sv) - } else { - false // all cases already covered - } - }); - } + let (msg_type, sv, ephemeris) = fr.unwrap(); + assert_eq!(msg_type, NavMsgType::LNAV); + assert_eq!( + sv, + SV { + constellation: Constellation::Galileo, + prn: 1, } - }, - TargetItem::ConstellationItem(constells_list) => { - /* - * Removes ephemeris frames that should now be missing - */ - for (epoch, frames) in record.iter_mut() { - if subset.get(epoch).is_none() { - // for specified targets, this should now be removed - frames.retain(|fr| { - if let Some((_, sv, _)) = fr.as_eph() { - constells_list.contains(&sv.constellation) - } else if let Some((_, sv, _)) = fr.as_ion() { - constells_list.contains(&sv.constellation) - } else if let Some((_, sv, _)) = fr.as_sto() { - constells_list.contains(&sv.constellation) - } else if let Some((_, sv, _)) = fr.as_eop() { - constells_list.contains(&sv.constellation) - } else { - false // all cases already covered - } - }); - } + ); + assert_eq!(ephemeris.clock_bias, -0.101553811692e-02); + assert_eq!(ephemeris.clock_drift, -0.804334376880e-11); + assert_eq!(ephemeris.clock_drift_rate, 0.0); + let orbits = &ephemeris.orbits; + assert_eq!(orbits.len(), 24); + for (k, v) in orbits.iter() { + if k.eq("iodnav") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.130000000000e+02); + } else if k.eq("crs") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.435937500000e+02); + } else if k.eq("deltaN") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.261510892978e-08); + } else if k.eq("m0") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.142304064404e+00); + } else if k.eq("cuc") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.201165676117e-05); + } else if k.eq("e") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.226471573114e-03); + } else if k.eq("cus") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.109840184450e-04); + } else if k.eq("sqrta") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.544061822701e+04); + } else if k.eq("toe") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.468600000000e+06); + } else if k.eq("cic") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.111758708954e-07); + } else if k.eq("omega0") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.313008275208e+01); + } else if k.eq("cis") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.409781932831e-07); + } else if k.eq("i0") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.980287270202e+00); + } else if k.eq("crc") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.113593750000e+03); + } else if k.eq("omega") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.276495796017e+00); + } else if k.eq("omegaDot") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.518200156545e-08); + } else if k.eq("idot") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.595381942905e-09); + } else if k.eq("dataSrc") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.258000000000e+03); + } else if k.eq("week") { + let v = v.as_u32(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 2138); + //SPARE + } else if k.eq("sisa") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.312000000000e+01); + } else if k.eq("health") { + let v = v.as_gal_health(); + assert!(v.is_some()); + } else if k.eq("bgdE5aE1") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.232830643654e-09); + } else if k.eq("bgdE5bE1") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.000000000000e+00); + } else if k.eq("t_tm") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.469330000000e+06); + } else { + panic!("Got unexpected key \"{}\" for GALV3 record", k); } - }, - TargetItem::OrbitItem(_orbit_fields) => { - /* - * Removes ephemeris frames that should now be missing - */ - unimplemented!("specific orbit field decimation"); - }, - TargetItem::AzimuthItem(_azim) => { - unimplemented!("navigation:record:decimate_data_subset(azim)"); - }, - TargetItem::ElevationItem(_elev) => { - unimplemented!("navigation:record:decimate_data_subset(elev)"); - }, - TargetItem::NavFrameItem(_frame_classes) => { - unimplemented!("navigation:record:decimate_data_subset(navframe)"); - }, - TargetItem::NavMsgItem(_msg_types) => { - unimplemented!("navigation:record:decimate_data_subset(navmsg)"); - }, - _ => {}, // does not apply - } -} - -#[cfg(feature = "processing")] -impl Preprocessing for Record { - fn filter(&self, f: Filter) -> Self { - let mut s = self.clone(); - s.filter_mut(f); - s - } - fn filter_mut(&mut self, filt: Filter) { - match filt { - Filter::Mask(mask) => self.mask_mut(mask), - Filter::Interp(filter) => self.interpolate_mut(filter.series), - Filter::Decimation(filter) => match filter.dtype { - DecimationType::DecimByRatio(r) => { - if filter.target.is_none() { - self.decimate_by_ratio_mut(r); - return; // no need to proceed further - } - - let item = filter.target.unwrap(); - - // apply mask to retain desired subset - let mask = MaskFilter { - item: item.clone(), - operand: MaskOperand::Equals, - }; - - // decimate - let subset = self.mask(mask).decimate_by_ratio(r); - // adapt self's subset to new data rate - decimate_data_subset(self, &subset, &item); - }, - DecimationType::DecimByInterval(dt) => { - if filter.target.is_none() { - self.decimate_by_interval_mut(dt); - return; // no need to proceed further - } - - let item = filter.target.unwrap(); - - // apply mask to retain desired subset - let mask = MaskFilter { - item: item.clone(), - operand: MaskOperand::Equals, - }; - - // decimate - let subset = self.mask(mask).decimate_by_interval(dt); - // adapt self's subset to new data rate - decimate_data_subset(self, &subset, &item); - }, - }, - Filter::Smoothing(_) => unimplemented!("navigation:record:smoothing"), } } -} - -#[cfg(feature = "processing")] -impl Interpolate for Record { - fn interpolate(&self, series: TimeSeries) -> Self { - let mut s = self.clone(); - s.interpolate_mut(series); - s - } - fn interpolate_mut(&mut self, _series: TimeSeries) { - unimplemented!("navigation:record:interpolate_mut()") - } -} + #[test] + fn parse_glonass_v3() { + let content = + "R07 2021 01 01 09 45 00 -.420100986958e-04 .000000000000e+00 .342000000000e+05 + .124900639648e+05 .912527084351e+00 .000000000000e+00 .000000000000e+00 + .595546582031e+04 .278496932983e+01 .000000000000e+00 .500000000000e+01 + .214479208984e+05 -.131077289581e+01 -.279396772385e-08 .000000000000e+00"; + let version = Version::new(3, 0); + let entry = parse_epoch(version, Constellation::Mixed, content); + assert!(entry.is_ok()); + let (epoch, frame) = entry.unwrap(); + assert_eq!( + epoch, + Epoch::from_gregorian_utc(2021, 01, 01, 09, 45, 00, 00) + ); -#[cfg(feature = "processing")] -impl Decimate for Record { - /// Decimates Self by desired factor - fn decimate_by_ratio_mut(&mut self, r: u32) { - let mut i = 0; - self.retain(|_, _| { - let retained = (i % r) == 0; - i += 1; - retained - }); - } - /// Copies and Decimates Self by desired factor - fn decimate_by_ratio(&self, r: u32) -> Self { - let mut s = self.clone(); - s.decimate_by_ratio_mut(r); - s - } - /// Decimates Self to fit minimum epoch interval - fn decimate_by_interval_mut(&mut self, interval: Duration) { - let mut last_retained = Option::::None; - self.retain(|e, _| { - if let Some(last) = last_retained { - let dt = *e - last; - if dt >= interval { - last_retained = Some(*e); - true - } else { - false - } + let fr = frame.as_eph(); + assert!(fr.is_some()); + let (msg_type, sv, ephemeris) = fr.unwrap(); + assert_eq!(msg_type, NavMsgType::LNAV); + assert_eq!( + sv, + SV { + constellation: Constellation::Glonass, + prn: 7, + } + ); + assert_eq!(ephemeris.clock_bias, -0.420100986958e-04); + assert_eq!(ephemeris.clock_drift, 0.000000000000e+00); + assert_eq!(ephemeris.clock_drift_rate, 0.342000000000e+05); + let orbits = &ephemeris.orbits; + assert_eq!(orbits.len(), 12); + for (k, v) in orbits.iter() { + if k.eq("satPosX") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.124900639648e+05); + } else if k.eq("velX") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.912527084351e+00); + } else if k.eq("accelX") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.000000000000e+00); + } else if k.eq("health") { + let v = v.as_glo_health(); + assert!(v.is_some()); + } else if k.eq("satPosY") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.595546582031e+04); + } else if k.eq("velY") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.278496932983e+01); + } else if k.eq("accelY") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.000000000000e+00); + } else if k.eq("channel") { + let v = v.as_i8(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 5); + } else if k.eq("satPosZ") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.214479208984e+05); + } else if k.eq("velZ") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.131077289581e+01); + } else if k.eq("accelZ") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, -0.279396772385e-08); + } else if k.eq("ageOp") { + let v = v.as_f64(); + assert!(v.is_some()); + let v = v.unwrap(); + assert_eq!(v, 0.000000000000e+00); } else { - last_retained = Some(*e); - true // always retain 1st epoch + panic!("Got unexpected key \"{}\" for GLOV3 record", k); } - }); - } - fn decimate_by_interval(&self, dt: Duration) -> Self { - let mut s = self.clone(); - s.decimate_by_interval_mut(dt); - s - } - fn decimate_match_mut(&mut self, rhs: &Self) { - self.retain(|e, _| rhs.get(e).is_some()); + } } - fn decimate_match(&self, rhs: &Self) -> Self { - let mut s = self.clone(); - s.decimate_match_mut(rhs); - s + #[test] + fn format_rework() { + let content = "1000123 -123123e-1 -1.23123123e0 -0.123123e-4"; + assert_eq!( + fmt_rework(2, content), + "1000123 -123123D-01 -1.23123123D+00 -0.123123D-04" + ); + assert_eq!( + fmt_rework(3, content), + "1000123 -123123E-01 -1.23123123E+00 -0.123123E-04" + ); } } diff --git a/rinex/src/observation/record.rs b/rinex/src/observation/record.rs index f77f0f5e0..3642e6913 100644 --- a/rinex/src/observation/record.rs +++ b/rinex/src/observation/record.rs @@ -693,7 +693,6 @@ fn fmt_epoch_v2( } } } - lines.push('\n'); lines } @@ -1650,42 +1649,6 @@ impl Dcb for Record { } } -#[cfg(test)] -mod test { - use super::*; - #[test] - fn obs_record_is_new_epoch() { - assert!(is_new_epoch( - "95 01 01 00 00 00.0000000 0 7 06 17 21 22 23 28 31", - Version { major: 2, minor: 0 } - )); - assert!(!is_new_epoch( - "21700656.31447 16909599.97044 .00041 24479973.67844 24479975.23247", - Version { major: 2, minor: 0 } - )); - assert!(is_new_epoch( - "95 01 01 11 00 00.0000000 0 8 04 16 18 19 22 24 27 29", - Version { major: 2, minor: 0 } - )); - assert!(!is_new_epoch( - "95 01 01 11 00 00.0000000 0 8 04 16 18 19 22 24 27 29", - Version { major: 3, minor: 0 } - )); - assert!(is_new_epoch( - "> 2022 01 09 00 00 30.0000000 0 40", - Version { major: 3, minor: 0 } - )); - assert!(!is_new_epoch( - "> 2022 01 09 00 00 30.0000000 0 40", - Version { major: 2, minor: 0 } - )); - assert!(!is_new_epoch( - "G01 22331467.880 117352685.28208 48.950 22331469.28", - Version { major: 3, minor: 0 } - )); - } -} - /* * Code multipath bias */ @@ -1776,3 +1739,39 @@ pub(crate) fn code_multipath( } ret } + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn obs_record_is_new_epoch() { + assert!(is_new_epoch( + "95 01 01 00 00 00.0000000 0 7 06 17 21 22 23 28 31", + Version { major: 2, minor: 0 } + )); + assert!(!is_new_epoch( + "21700656.31447 16909599.97044 .00041 24479973.67844 24479975.23247", + Version { major: 2, minor: 0 } + )); + assert!(is_new_epoch( + "95 01 01 11 00 00.0000000 0 8 04 16 18 19 22 24 27 29", + Version { major: 2, minor: 0 } + )); + assert!(!is_new_epoch( + "95 01 01 11 00 00.0000000 0 8 04 16 18 19 22 24 27 29", + Version { major: 3, minor: 0 } + )); + assert!(is_new_epoch( + "> 2022 01 09 00 00 30.0000000 0 40", + Version { major: 3, minor: 0 } + )); + assert!(!is_new_epoch( + "> 2022 01 09 00 00 30.0000000 0 40", + Version { major: 2, minor: 0 } + )); + assert!(!is_new_epoch( + "G01 22331467.880 117352685.28208 48.950 22331469.28", + Version { major: 3, minor: 0 } + )); + } +} diff --git a/rinex/src/production/ffu.rs b/rinex/src/production/ffu.rs index ffd29235c..bb21fc809 100644 --- a/rinex/src/production/ffu.rs +++ b/rinex/src/production/ffu.rs @@ -1,5 +1,5 @@ use super::Error; -use hifitime::{Duration, Unit}; +use hifitime::{Duration, Unit, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE}; #[derive(Debug, Copy, Clone, PartialEq)] pub struct FFU { @@ -9,6 +9,33 @@ pub struct FFU { pub unit: Unit, } +impl From for FFU { + fn from(dt: Duration) -> Self { + let total_seconds = dt.to_seconds(); + if dt < SECONDS_PER_MINUTE * Unit::Second { + Self { + val: total_seconds.round() as u32, + unit: Unit::Second, + } + } else if dt < 1.0 * Unit::Hour { + Self { + val: (total_seconds / SECONDS_PER_MINUTE).round() as u32, + unit: Unit::Minute, + } + } else if dt < 1 * Unit::Day { + Self { + val: (total_seconds / SECONDS_PER_HOUR).round() as u32, + unit: Unit::Hour, + } + } else { + Self { + val: (total_seconds / SECONDS_PER_DAY).round() as u32, + unit: Unit::Day, + } + } + } +} + impl Default for FFU { fn default() -> Self { Self { @@ -47,37 +74,10 @@ impl std::str::FromStr for FFU { } } -impl From for FFU { - fn from(dur: Duration) -> Self { - let secs = dur.to_seconds().round() as u32; - if secs < 60 { - Self { - val: secs, - unit: Unit::Second, - } - } else if secs < 3_600 { - Self { - val: secs / 60, - unit: Unit::Minute, - } - } else if secs < 86_400 { - Self { - val: secs / 3_600, - unit: Unit::Hour, - } - } else { - Self { - val: secs / 86_400, - unit: Unit::Day, - } - } - } -} - #[cfg(test)] mod test { use super::FFU; - use hifitime::Unit; + use hifitime::{Duration, Unit, SECONDS_PER_DAY}; use std::str::FromStr; #[test] fn ffu_parsing() { @@ -150,4 +150,39 @@ mod test { assert_eq!(ffu, expected); } } + #[test] + fn ffu_cast() { + for (duration, expected) in [ + ( + Duration::from_seconds(30.0), + FFU { + val: 30, + unit: Unit::Second, + }, + ), + ( + Duration::from_seconds(60.0), + FFU { + val: 1, + unit: Unit::Minute, + }, + ), + ( + Duration::from_seconds(3600.0), + FFU { + val: 1, + unit: Unit::Hour, + }, + ), + ( + Duration::from_seconds(SECONDS_PER_DAY), + FFU { + val: 1, + unit: Unit::Day, + }, + ), + ] { + assert_eq!(FFU::from(duration), expected); + } + } } diff --git a/rinex/src/production/mod.rs b/rinex/src/production/mod.rs index 5733ab659..4878a6ced 100644 --- a/rinex/src/production/mod.rs +++ b/rinex/src/production/mod.rs @@ -19,7 +19,6 @@ use thiserror::Error; mod sequence; -pub use sequence::FileSequence; mod ppu; pub use ppu::PPU; @@ -44,7 +43,7 @@ pub enum Error { /// File production attributes. Used when generating /// RINEX data that follows standard naming conventions, /// or attached to data parsed from such files. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct ProductionAttributes { /// Name serves several roles which are type dependent. /// - Non detailed OBS RINEX: this is usually the station name @@ -70,6 +69,8 @@ pub struct ProductionAttributes { pub struct DetailedProductionAttributes { /// Agency Country Code pub country: String, + /// # in Batch if Self is part of a file serie + pub batch: u8, /// Data source pub data_src: DataSource, /// PPU gives information on file production periodicity. @@ -94,6 +95,7 @@ impl ProductionAttributes { /* filename generator */ pub(crate) fn rinex_long_format( name: &str, + batch: u8, country: &str, src: char, yyyy: &str, @@ -107,13 +109,34 @@ impl ProductionAttributes { ) -> String { if let Some(ffu) = ffu { format!( - "{}00{}_{}_{}{}{}{}_{}_{}_{}.{}", - name, country, src, yyyy, ddd, hh, mm, ppu, ffu, fmt, ext, + "{}{:02}{}_{}_{}{}{}{}_{}_{}_{}.{}", + name, + batch % 99, + country, + src, + yyyy, + ddd, + hh, + mm, + ppu, + ffu, + fmt, + ext, ) } else { format!( - "{}00{}_{}_{}{}{}{}_{}_{}.{}", - name, country, src, yyyy, ddd, hh, mm, ppu, fmt, ext, + "{}{:02}{}_{}_{}{}{}{}_{}_{}.{}", + name, + batch % 99, + country, + src, + yyyy, + ddd, + hh, + mm, + ppu, + fmt, + ext, ) } } @@ -161,12 +184,16 @@ impl std::str::FromStr for ProductionAttributes { return Err(Error::NonStandardFileName); }; - // determine type of RINEX first - // because it determines how to parse the "name" field let year = fname[12..16] .parse::() .map_err(|_| Error::NonStandardFileName)?; + let batch = fname[5..6] + .parse::() + .map_err(|_| Error::NonStandardFileName)?; + + // determine type of RINEX first + // because it determines how to parse the "name" field let rtype = &fname[offset + 3..offset + 4]; let name_offset = match rtype { "I" => 3usize, // only 3 digits on IONEX @@ -183,9 +210,10 @@ impl std::str::FromStr for ProductionAttributes { }, region: None, // IONEX files only use a short format details: Some(DetailedProductionAttributes { + batch, country: fname[6..9].to_string(), - data_src: DataSource::from_str(&fname[10..11])?, ppu: PPU::from_str(&fname[24..27])?, + data_src: DataSource::from_str(&fname[10..11])?, hh: { fname[19..21] .parse::() @@ -206,6 +234,33 @@ impl std::str::FromStr for ProductionAttributes { } } +use crate::merge::{merge_mut_option, Error as MergeError, Merge}; + +impl Merge for ProductionAttributes { + fn merge(&self, rhs: &Self) -> Result { + let mut lhs = self.clone(); + lhs.merge_mut(rhs)?; + Ok(lhs) + } + fn merge_mut(&mut self, rhs: &Self) -> Result<(), MergeError> { + merge_mut_option(&mut self.region, &rhs.region); + merge_mut_option(&mut self.details, &rhs.details); + if let Some(lhs) = &mut self.details { + if let Some(rhs) = &rhs.details { + merge_mut_option(&mut lhs.ffu, &rhs.ffu); + /* + * Data source is downgraded to "Unknown" + * in case we wind up cross mixing data sources + */ + if lhs.data_src != rhs.data_src { + lhs.data_src = DataSource::Unknown; + } + } + } + Ok(()) + } +} + #[cfg(test)] mod test { use super::DetailedProductionAttributes; @@ -241,6 +296,7 @@ mod test { 355, DetailedProductionAttributes { country: "ESP".to_string(), + batch: 0, data_src: DataSource::Receiver, ppu: PPU::Daily, hh: 0, @@ -258,6 +314,7 @@ mod test { 159, DetailedProductionAttributes { country: "DNK".to_string(), + batch: 0, data_src: DataSource::Receiver, ppu: PPU::Hourly, hh: 10, @@ -275,6 +332,7 @@ mod test { 1, DetailedProductionAttributes { country: "NLD".to_string(), + batch: 0, data_src: DataSource::Receiver, hh: 0, mm: 0, @@ -289,6 +347,7 @@ mod test { 177, DetailedProductionAttributes { country: "DNK".to_string(), + batch: 0, data_src: DataSource::Receiver, ppu: PPU::Daily, hh: 0, @@ -306,6 +365,7 @@ mod test { 177, DetailedProductionAttributes { country: "DNK".to_string(), + batch: 0, data_src: DataSource::Receiver, ppu: PPU::Daily, hh: 0, @@ -323,6 +383,43 @@ mod test { 177, DetailedProductionAttributes { country: "DNK".to_string(), + batch: 0, + data_src: DataSource::Receiver, + ppu: PPU::Daily, + hh: 22, + mm: 23, + ffu: Some(FFU { + val: 30, + unit: Unit::Second, + }), + }, + ), + ( + "ESBC01DNK_R_20201772223_01D_30S_MO.crx.gz", + "ESBC", + 2020, + 177, + DetailedProductionAttributes { + country: "DNK".to_string(), + batch: 1, + data_src: DataSource::Receiver, + ppu: PPU::Daily, + hh: 22, + mm: 23, + ffu: Some(FFU { + val: 30, + unit: Unit::Second, + }), + }, + ), + ( + "ESBC04DNK_R_20201772223_01D_30S_MO.crx.gz", + "ESBC", + 2020, + 177, + DetailedProductionAttributes { + country: "DNK".to_string(), + batch: 4, data_src: DataSource::Receiver, ppu: PPU::Daily, hh: 22, diff --git a/rinex/src/production/ppu.rs b/rinex/src/production/ppu.rs index 6af573a8b..2017d3a20 100644 --- a/rinex/src/production/ppu.rs +++ b/rinex/src/production/ppu.rs @@ -1,5 +1,5 @@ use super::Error; -use hifitime::{Duration, Unit}; +use hifitime::{Duration, Unit, DAYS_PER_YEAR}; #[cfg(feature = "serde")] use serde::Serialize; @@ -8,7 +8,7 @@ use serde::Serialize; #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize))] pub enum PPU { - /// A Daily file is the standard and will contain 24h of data + /// A Daily file is the standard and contains 24h of data #[default] Daily, /// Contains 15' of data @@ -21,6 +21,22 @@ pub enum PPU { Unspecified, } +impl From for PPU { + fn from(dt: Duration) -> Self { + if dt >= DAYS_PER_YEAR * Unit::Day { + Self::Yearly + } else if dt > 1.0 * Unit::Hour && dt <= 24.0 * Unit::Hour { + Self::Daily + } else if dt > 15.0 * Unit::Minute && dt <= 1.0 * Unit::Hour { + Self::Hourly + } else if dt > 5.0 * Unit::Minute && dt <= 15.0 * Unit::Minute { + Self::QuarterHour + } else { + Self::Unspecified + } + } +} + impl PPU { /// Returns this file periodicity as a [Duration] pub fn duration(&self) -> Option { @@ -28,7 +44,7 @@ impl PPU { Self::QuarterHour => Some(15 * Unit::Minute), Self::Hourly => Some(1 * Unit::Hour), Self::Daily => Some(1 * Unit::Day), - Self::Yearly => Some(365 * Unit::Day), + Self::Yearly => Some(DAYS_PER_YEAR * Unit::Day), _ => None, } } @@ -62,7 +78,7 @@ impl std::str::FromStr for PPU { #[cfg(test)] mod test { use super::PPU; - use hifitime::Unit; + use hifitime::{Unit, DAYS_PER_YEAR}; use std::str::FromStr; #[test] fn ppu_parsing() { @@ -70,13 +86,25 @@ mod test { ("15M", PPU::QuarterHour, Some(15 * Unit::Minute)), ("01H", PPU::Hourly, Some(1 * Unit::Hour)), ("01D", PPU::Daily, Some(1 * Unit::Day)), - ("01Y", PPU::Yearly, Some(365 * Unit::Day)), + ("01Y", PPU::Yearly, Some(DAYS_PER_YEAR * Unit::Day)), ("XX", PPU::Unspecified, None), ("01U", PPU::Unspecified, None), ] { let ppu = PPU::from_str(c).unwrap(); assert_eq!(ppu, expected); - assert_eq!(ppu.duration(), dur); + assert_eq!(ppu.duration(), dur, "test failed for {}", c); + } + } + #[test] + fn ppu_cast() { + for (duration, expected) in [ + (15.0 * Unit::Minute, PPU::QuarterHour), + (0.9 * Unit::Hour, PPU::Hourly), + (1.0 * Unit::Hour, PPU::Hourly), + (0.9 * Unit::Day, PPU::Daily), + (1.0 * Unit::Day, PPU::Daily), + ] { + assert_eq!(PPU::from(duration), expected); } } } diff --git a/rinex/src/production/source.rs b/rinex/src/production/source.rs index 96773aba3..cf5c59e34 100644 --- a/rinex/src/production/source.rs +++ b/rinex/src/production/source.rs @@ -3,7 +3,7 @@ use super::Error; #[cfg(feature = "serde")] use serde::Serialize; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize))] pub enum DataSource { /// Source of data is hardware (radio) receiver. @@ -19,9 +19,9 @@ pub enum DataSource { impl std::str::FromStr for DataSource { type Err = Error; fn from_str(content: &str) -> Result { - if content.eq("R") { + if content.eq("R") || content.eq("RCVR") { Ok(Self::Receiver) - } else if content.eq("S") { + } else if content.eq("S") || content.eq("STREAM") { Ok(Self::Stream) } else { Ok(Self::Unknown) diff --git a/rinex/src/record.rs b/rinex/src/record.rs index 1027f73e9..683a2c479 100644 --- a/rinex/src/record.rs +++ b/rinex/src/record.rs @@ -158,11 +158,12 @@ impl Record { if let Ok(compressed) = compressor.compress(major, &obs_fields.codes, constell, &line) { - write!(writer, "{}", compressed)?; + // println!("compressed \"{}\"", compressed); // DEBUG + writeln!(writer, "{}", compressed)?; } } } else { - write!(writer, "{}", epoch)?; + writeln!(writer, "{}", epoch)?; } } }, diff --git a/rinex/src/tests/clock.rs b/rinex/src/tests/clock.rs index 75dafa72f..60253c3e5 100644 --- a/rinex/src/tests/clock.rs +++ b/rinex/src/tests/clock.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod test { - use crate::clock; + use crate::prelude::*; use std::str::FromStr; #[test] @@ -293,7 +293,9 @@ mod test { let epoch = Epoch::from_str(epoch_str).unwrap(); let (prof_type, profile) = rinex .precise_sv_clock_interpolate(epoch, sv) - .expect(&format!("precise_sv_clock_interp failed @{}", epoch_str)); + .unwrap_or_else(|| { + panic!("precise_sv_clock_interp failed @{}", epoch_str) + }); assert_eq!(prof_type, ClockProfileType::AS); assert_eq!(profile.bias, expected, "invalid results @{}", epoch_str); } @@ -316,7 +318,9 @@ mod test { let epoch = Epoch::from_str(epoch_str).unwrap(); let (prof_type, profile) = rinex .precise_sv_clock_interpolate(epoch, sv) - .expect(&format!("precise_sv_clock_interp failed @{}", epoch_str)); + .unwrap_or_else(|| { + panic!("precise_sv_clock_interp failed @{}", epoch_str) + }); assert_eq!(prof_type, ClockProfileType::AS); assert_eq!(profile.bias, expected, "invalid results @{}", epoch_str); } @@ -339,7 +343,9 @@ mod test { let epoch = Epoch::from_str(epoch_str).unwrap(); let (prof_type, profile) = rinex .precise_sv_clock_interpolate(epoch, sv) - .expect(&format!("precise_sv_clock_interp failed @{}", epoch_str)); + .unwrap_or_else(|| { + panic!("precise_sv_clock_interp failed @{}", epoch_str) + }); assert_eq!(prof_type, ClockProfileType::AS); assert_eq!(profile.bias, expected, "invalid results @{}", epoch_str); } diff --git a/rinex/src/tests/decimation.rs b/rinex/src/tests/decimation.rs index ca4220aa3..bccf9d95a 100644 --- a/rinex/src/tests/decimation.rs +++ b/rinex/src/tests/decimation.rs @@ -5,7 +5,7 @@ mod decimation { use crate::preprocessing::*; //use itertools::Itertools; use std::path::Path; - use std::str::FromStr; + #[test] #[cfg(feature = "flate2")] fn obs_decimation() { @@ -17,7 +17,7 @@ mod decimation { .join("ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz"); let fullpath = path.to_string_lossy(); - let mut rinex = Rinex::from_file(fullpath.as_ref()); + let rinex = Rinex::from_file(fullpath.as_ref()); assert!(rinex.is_ok(), "failed to parse \"{}\"", fullpath); let mut rinex = rinex.unwrap(); @@ -46,7 +46,7 @@ mod decimation { .join("POTS00DEU_R_20232540000_01D_05M_MM.rnx.gz"); let fullpath = path.to_string_lossy(); - let mut rinex = Rinex::from_file(fullpath.as_ref()); + let rinex = Rinex::from_file(fullpath.as_ref()); assert!(rinex.is_ok(), "failed to parse \"{}\"", fullpath); let mut rinex = rinex.unwrap(); @@ -75,11 +75,11 @@ mod decimation { .join("ESBC00DNK_R_20201770000_01D_MN.rnx.gz"); let fullpath = path.to_string_lossy(); - let mut rinex = Rinex::from_file(fullpath.as_ref()); + let rinex = Rinex::from_file(fullpath.as_ref()); assert!(rinex.is_ok(), "failed to parse \"{}\"", fullpath); let mut rinex = rinex.unwrap(); - let len = rinex.epoch().count(); + let _len = rinex.epoch().count(); rinex.decimate_by_interval_mut(Duration::from_seconds(60.0)); let count = rinex.epoch().count(); diff --git a/rinex/src/tests/decompression.rs b/rinex/src/tests/decompression.rs index af791683e..a4ae84021 100644 --- a/rinex/src/tests/decompression.rs +++ b/rinex/src/tests/decompression.rs @@ -4,6 +4,7 @@ mod test { use crate::tests::toolkit::obsrinex_check_observables; use crate::tests::toolkit::random_name; use crate::tests::toolkit::test_observation_rinex; + // use crate::tests::toolkit::test_against_model; use crate::{erratic_time_frame, evenly_spaced_time_frame, tests::toolkit::TestTimeFrame}; use crate::{observable, prelude::*}; use itertools::Itertools; @@ -162,12 +163,10 @@ mod test { // parse plain RINEX and run reciprocity let path = format!("../test_resources/OBS/V2/{}", rnx_name); - let model = Rinex::from_file(&path); - assert!(model.is_ok(), "Failed to parse test model \"{}\"", path); + let _model = Rinex::from_file(&path).unwrap(); - //let model = model.unwrap(); // run testbench - // test_toolkit::test_against_model(&rnx, &model, &path); + // test_against_model(&rnx, &model, &path, 1.0E-6); // remove copy let _ = std::fs::remove_file(filename); @@ -220,11 +219,10 @@ mod test { // parse Model for testbench let path = format!("../test_resources/OBS/V3/{}", rnx_name); - let model = Rinex::from_file(&path); - assert!(model.is_ok(), "Failed to parse test model \"{}\"", path); + let _model = Rinex::from_file(&path).unwrap(); // run testbench - // test_toolkit::test_against_model(&rnx, &model, &path); + // test_against_model(&rnx, &model, &path, 1.0E-6); } } /* diff --git a/rinex/src/tests/filename.rs b/rinex/src/tests/filename.rs index 0d464c5b4..5f3b36efb 100644 --- a/rinex/src/tests/filename.rs +++ b/rinex/src/tests/filename.rs @@ -61,7 +61,7 @@ fn long_filename_conventions() { .join("test_resources") .join(testfile); - let rinex = Rinex::from_file(fp.to_string_lossy().as_ref()).unwrap(); + let rinex = Rinex::from_path(&fp).unwrap(); let output = rinex.standard_filename(false, custom_suffix, None); assert_eq!(output, expected, "bad filename generated"); } diff --git a/rinex/src/tests/obs.rs b/rinex/src/tests/obs.rs index 486c87968..1c9fd402e 100644 --- a/rinex/src/tests/obs.rs +++ b/rinex/src/tests/obs.rs @@ -840,7 +840,7 @@ mod test { let record = rnx.record.as_obs().unwrap(); - let epoch = epochs.get(0).unwrap(); + let epoch = epochs.first().unwrap(); let flag = EpochFlag::Ok; let (clk_offset, vehicles) = record.get(&(*epoch, flag)).unwrap(); assert!(clk_offset.is_none()); diff --git a/rinex/src/tests/production.rs b/rinex/src/tests/production.rs index 51b4b2d89..a7c42b939 100644 --- a/rinex/src/tests/production.rs +++ b/rinex/src/tests/production.rs @@ -21,7 +21,6 @@ mod test { } #[test] #[cfg(feature = "flate2")] - #[ignore] fn obs_v2() { let prefix = Path::new(env!("CARGO_MANIFEST_DIR")) .join("..") @@ -58,7 +57,6 @@ mod test { } #[test] #[cfg(feature = "flate2")] - //#[ignore] fn meteo_v2() { let folder = env!("CARGO_MANIFEST_DIR").to_owned() + "/../test_resources/MET/V2/"; for file in std::fs::read_dir(folder).unwrap() { diff --git a/rinex/src/tests/toolkit/mod.rs b/rinex/src/tests/toolkit/mod.rs index 9706dc642..0014e9715 100644 --- a/rinex/src/tests/toolkit/mod.rs +++ b/rinex/src/tests/toolkit/mod.rs @@ -10,7 +10,7 @@ pub use observation::check_observables as obsrinex_check_observables; /* ANY RINEX == constant (special ops) */ mod constant; -pub use constant::{is_constant_rinex, is_null_rinex}; +pub use constant::is_null_rinex; //#[macro_use] #[macro_export] diff --git a/sp3/src/lib.rs b/sp3/src/lib.rs index dd30eabf7..cb6f94477 100644 --- a/sp3/src/lib.rs +++ b/sp3/src/lib.rs @@ -475,7 +475,7 @@ impl SP3 { } /// Returns first epoch pub fn first_epoch(&self) -> Option { - self.epoch.get(0).copied() + self.epoch.first().copied() } /// Returns last epoch pub fn last_epoch(&self) -> Option { @@ -532,6 +532,39 @@ impl SP3 { .iter() .flat_map(|(e, sv)| sv.iter().map(|(sv, clk)| (*e, *sv, *clk))) } + /// Interpolate Clock state at desired "t" expressed in the timescale you want. + /// SP3 files usually have a 15' sampling interval which makes this operation + /// most likely incorrect. You should either use higher sample rate to reduce + /// the error generated by interpolation, or use different products like + /// high precision Clock RINEX files. + pub fn sv_clock_interpolate(&self, t: Epoch, sv: SV) -> Option { + let before = self + .sv_clock() + .filter_map(|(clk_t, clk_sv, value)| { + if clk_t <= t && clk_sv == sv { + Some((clk_t, value)) + } else { + None + } + }) + .last()?; + let after = self + .sv_clock() + .filter_map(|(clk_t, clk_sv, value)| { + if clk_t > t && clk_sv == sv { + Some((clk_t, value)) + } else { + None + } + }) + .reduce(|k, _| k)?; + let (before_t, before_clk) = before; + let (after_t, after_clk) = after; + let dt = (after_t - before_t).to_seconds(); + let mut bias = (after_t - t).to_seconds() / dt * before_clk; + bias += (t - before_t).to_seconds() / dt * after_clk; + Some(bias) + } /// Returns an Iterator over [`Comments`] contained in this file pub fn comments(&self) -> impl Iterator + '_ { self.comments.iter()