From 980cdf39480d3ff58f592920e6f6d1d68696f89c Mon Sep 17 00:00:00 2001 From: gwbres Date: Tue, 26 Dec 2023 16:45:31 +0100 Subject: [PATCH] Develop (#194) * plot brdc clock corrections * take sp3 into account in clock state too * bump rtk to v0.4.0 * run linter * bump cggtts to v4.1.0 * antex: defining support of ATX files * differentiate between SV and RX antennas * correct possible reference antenna * improved PCV description and API * introduce dedicated browsing and data extraction methods by means of a dedicated crate features * ignore patch files * improved rtk and positioning definitions * high level atx methods * working on apc coordinates * remove rinex from crate dependencies * calibration validity parsing * test new features * prepare rinex 0.15.2 * clippy * linter * Introduce Channel Number Pseudo Observable * although we do not propose functionnalities tied to this Pseudo Observable, this will allow tolerating their presence and not crash * run linter * RINEX3: IONOSPHERIC CORR header support * support the special header, in RINEX3 to describe the ionospheric correction to apply over a 24H time frame * advanced in iono corr visualization * iono corr visualization solution * post processing -p: fix default configuration to be used * post processing -p: possible ionod correction (RINEX3) case * simplify code combinations * update documentation * add new test resource --------- Signed-off-by: Guillaume W. Bres --- .gitignore | 1 + README.md | 67 ++-- crx2rnx/Cargo.toml | 4 +- crx2rnx/src/main.rs | 16 +- rinex-cli/Cargo.toml | 6 +- rinex-cli/config/rtk/gpst_10sv_basic.json | 3 + rinex-cli/src/analysis/sampling.rs | 2 +- rinex-cli/src/cli.rs | 57 +-- rinex-cli/src/identification.rs | 2 +- rinex-cli/src/main.rs | 23 +- rinex-cli/src/plot/combination.rs | 4 +- rinex-cli/src/plot/context.rs | 16 +- rinex-cli/src/plot/mod.rs | 59 ++- rinex-cli/src/plot/record/ionex.rs | 2 +- rinex-cli/src/plot/record/ionosphere.rs | 194 +++++----- rinex-cli/src/plot/record/meteo.rs | 2 +- rinex-cli/src/plot/record/navigation.rs | 23 +- rinex-cli/src/plot/record/observation.rs | 6 +- rinex-cli/src/positioning/post_process.rs | 49 ++- rinex-cli/src/positioning/solver.rs | 134 +++---- rinex-qc/Cargo.toml | 4 +- rinex/Cargo.toml | 4 +- rinex/build.rs | 2 +- rinex/src/antex/antenna.rs | 116 ------ rinex/src/antex/antenna/mod.rs | 163 +++++++++ rinex/src/antex/antenna/sv.rs | 61 ++++ rinex/src/antex/mod.rs | 30 +- rinex/src/antex/pcv.rs | 4 +- rinex/src/antex/record.rs | 419 ++++++++++++++++------ rinex/src/carrier.rs | 9 +- rinex/src/context/mod.rs | 26 +- rinex/src/hatanaka/decompressor.rs | 6 + rinex/src/hatanaka/textdiff.rs | 6 + rinex/src/header.rs | 67 +++- rinex/src/ionex/grid.rs | 79 +--- rinex/src/ionex/mod.rs | 9 +- rinex/src/ionex/record.rs | 4 +- rinex/src/lib.rs | 164 ++++++++- rinex/src/linspace.rs | 65 ++++ rinex/src/navigation/ionmessage.rs | 185 +++++++++- rinex/src/observable.rs | 15 +- rinex/src/observation/record.rs | 78 ++-- rinex/src/record.rs | 44 +-- rinex/src/tests/antex.rs | 134 +++++-- rinex/src/tests/merge.rs | 40 +++ rinex/src/tests/nav.rs | 60 +++- rinex/src/tests/parsing.rs | 2 +- rinex/src/types.rs | 2 +- rnx2cggtts/Cargo.toml | 8 +- rnx2cggtts/src/cli.rs | 67 ++-- rnx2cggtts/src/main.rs | 6 +- rnx2cggtts/src/solver.rs | 170 ++++----- rnx2crx/Cargo.toml | 4 +- sinex/Cargo.toml | 1 - test_resources/ATX/V1/igs14_small.atx.gz | Bin 0 -> 18465 bytes ublox-rnx/Cargo.toml | 4 +- 56 files changed, 1714 insertions(+), 1014 deletions(-) delete mode 100644 rinex/src/antex/antenna.rs create mode 100644 rinex/src/antex/antenna/mod.rs create mode 100644 rinex/src/antex/antenna/sv.rs create mode 100644 rinex/src/linspace.rs create mode 100644 test_resources/ATX/V1/igs14_small.atx.gz diff --git a/.gitignore b/.gitignore index af8d74f04..40ac12c68 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ Cargo.lock *.html *.swp *.swo +*.patch **/*.rs.bk .DS_Store diff --git a/README.md b/README.md index ff4a6c3b5..06201d249 100644 --- a/README.md +++ b/README.md @@ -11,45 +11,50 @@ RINEX Rust tool suites to parse, analyze and process [RINEX Data](https://en.wikipedia.org/wiki/RINEX). -Our Wiki contains [several tutorials and applications](https://github.com/georust/rinex/wiki): it will get you started quickly. +The [Wiki pages](https://github.com/georust/rinex/wiki) contain all the documentation of this project, including several examples spanning different applications of GNSS. -For any question or problems you may experience: - -- open a new issue -- drop us a message [on Discord](https://discord.gg/Fp2aape) +If you have any question or experience any problems, feel free to open an issue on Github. +You can also contact us [on our Discord channel](https://discord.gg/Fp2aape) ## Advantages :rocket: -- Fast +- Fast :crab: - Open sources -- Native Hatanaka decompression and compression -- Seamless .gzip decompression with `flate2` compilation feature -- RINEX V4 full support, that includes modern Navigation messages +- Seamless Hatanaka compression and decompression +- Seamless Gzip decompression with `flate2` build option +- RINEX V4 full support - Meteo RINEX full support -- IONEX (2D) support, partial 3D support +- IONEX 2D support. Partial IONEX 3D support. - Clock RINEX partial support: to be concluded soon -- File merging, splitting and pre processing -- Modern constellations like BeiDou, Galileo and IRNSS -- Supported time scales are GPST, BDT, GST, UTC +- Several pre processing operations: + - File merging + - Time beaning + - Filtering.. +- Several post processing operations +- All modern GNSS constellations +- Modern GNSS codes and signals +- Time scales: GPST, BDT, GST, UTC - Supports many SBAS, refer to online documentation -- Full support of Military codes : if you're working with such signals you can -at least run a -qc analysis, and possibly the position solver once it is merged -- Supports high precision RINEX (scaled phase data with micro cycle precision) -- RINEX post processing like SNR, DCB analysis, Broadcast ephemeris interpolation, -high precision orbit interpolation (SP3).. -- RINEX-qc: statistical analysis that you can request in the "cli" application directly. -Analysis can run on modern GNSS signals and SP3 high precision data. -Emulates "teqc" historical application. -- An SPP/PPP position solver (under development), in the form of the "gnss-rtk" library that you can -summon from the "cli" application directly. - -## Known weaknesses :warning: - -- QZNSST is represented as GPST at the moment -- GLONASST and IRNSST are not supported : calculations (mostly orbits) will not be accurate -- The command line tool does not accept BINEX or other proprietary formats -- File production is not fully concluded to this day, some formats are still not correctly supported -(mostly NAV). +- High precision RINEX (carrier phase micro cycle precision) +- High precision orbit support (SP3) +- Quality Check (QC): file quality and statistical analysis to help precise positioning +(historical `teqc` function). +- SPP: Single Point Positioning +- PPP: Precise Point Positioning is work in progress :warning: + +## Disadvantages :warning: + +- QZNSST is represented as GPST at the moment. +- We're waiting for Hifitime V4 to support GLONASST and IRNSST. +Until then, orbital calculations on these systems are not feasible. +In other term, positioning is not feasible and you're limited to basic analysis. +- These tools are oriented towards the latest revisions of the RINEX format. +RINEX4 is out and we already support it. +Some minor features in the RINEX2 or 3 revisions may not be supported. +- Our command line applications do not accept BINEX or other proprietary formats +- File production is not fully concluded to this day. We're currently focused +on RINEX post processing rather than RINEX data production. Do not hesitate to fork and submit +your improvements ## Architecture diff --git a/crx2rnx/Cargo.toml b/crx2rnx/Cargo.toml index 0f1e7bbf4..199ca0c13 100644 --- a/crx2rnx/Cargo.toml +++ b/crx2rnx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "crx2rnx" -version = "2.2.0" +version = "2.2.1" license = "MIT OR Apache-2.0" authors = ["Guillaume W. Bres "] description = "RINEX data decompressor" @@ -12,4 +12,4 @@ readme = "README.md" [dependencies] clap = { version = "4.4.10", features = ["derive", "color"] } -rinex = { path = "../rinex", version = "=0.15.1", features = ["serde"] } +rinex = { path = "../rinex", version = "=0.15.2", features = ["serde"] } diff --git a/crx2rnx/src/main.rs b/crx2rnx/src/main.rs index 201552a80..a96c1093a 100644 --- a/crx2rnx/src/main.rs +++ b/crx2rnx/src/main.rs @@ -14,7 +14,7 @@ fn workspace(cli: &Cli) -> PathBuf { } fn create_workspace(path: &PathBuf) { - std::fs::create_dir_all(&path).unwrap_or_else(|_| { + std::fs::create_dir_all(path).unwrap_or_else(|_| { panic!( "failed to create workspace \"{}\": permission denied", path.to_string_lossy(), @@ -39,7 +39,7 @@ fn input_name(path: &PathBuf) -> String { } // deduce output name, from input name -fn output_filename<'a>(stem: &'a str, path: &PathBuf) -> String { +fn output_filename(stem: &str, path: &PathBuf) -> String { let filename = path .file_name() .expect("failed to determine input file name") @@ -52,14 +52,12 @@ fn output_filename<'a>(stem: &'a str, path: &PathBuf) -> String { .expect("failed to determine output file name") .replace("crx", "rnx") .to_string() + } else if filename.ends_with('d') { + filename.replace('d', "o").to_string() + } else if filename.ends_with('D') { + filename.replace('D', "O").to_string() } else { - if filename.ends_with('d') { - filename.replace('d', "o").to_string() - } else if filename.ends_with('D') { - filename.replace('D', "O").to_string() - } else { - format!("{}.rnx", stem) - } + format!("{}.rnx", stem) } } diff --git a/rinex-cli/Cargo.toml b/rinex-cli/Cargo.toml index c90cd8bf9..eba65fb0a 100644 --- a/rinex-cli/Cargo.toml +++ b/rinex-cli/Cargo.toml @@ -31,8 +31,8 @@ horrorshow = "0.8" clap = { version = "4.4.10", features = ["derive", "color"] } hifitime = { version = "3.8.4", features = ["serde", "std"] } gnss-rs = { version = "2.1.2" , features = ["serde"] } -rinex = { path = "../rinex", version = "=0.15.1", features = ["full"] } -rinex-qc = { path = "../rinex-qc", version = "=0.1.6", features = ["serde"] } +rinex = { path = "../rinex", version = "=0.15.2", features = ["full"] } +rinex-qc = { path = "../rinex-qc", version = "=0.1.7", features = ["serde"] } sp3 = { path = "../sp3", version = "=1.0.6", features = ["serde", "flate2"] } serde = { version = "1.0", default-features = false, features = ["derive"] } @@ -41,6 +41,6 @@ plotly = "0.8.4" # plotly = { git = "https://github.com/gwbres/plotly", branch = "density-mapbox" } # solver -gnss-rtk = { version = "0.4.0", features = ["serde"] } +gnss-rtk = { version = "0.4.1", features = ["serde"] } # gnss-rtk = { git = "https://github.com/rtk-rs/gnss-rtk", branch = "develop", features = ["serde"] } # gnss-rtk = { path = "../../rtk-rs/gnss-rtk", features = ["serde"] } diff --git a/rinex-cli/config/rtk/gpst_10sv_basic.json b/rinex-cli/config/rtk/gpst_10sv_basic.json index 565fa07e0..e9f879fef 100644 --- a/rinex-cli/config/rtk/gpst_10sv_basic.json +++ b/rinex-cli/config/rtk/gpst_10sv_basic.json @@ -4,6 +4,9 @@ "max_sv": 10, "min_sv_elev": 20.0, "min_snr": 20.0, + "solver": { + "gdop_threshold": 3.0 + }, "modeling": { "sv_clock_bias": true, "sv_total_group_delay": true, diff --git a/rinex-cli/src/analysis/sampling.rs b/rinex-cli/src/analysis/sampling.rs index b5faa7167..3d3ea002a 100644 --- a/rinex-cli/src/analysis/sampling.rs +++ b/rinex-cli/src/analysis/sampling.rs @@ -7,7 +7,7 @@ use rinex::prelude::RnxContext; * Sampling histogram */ pub fn histogram(ctx: &RnxContext, plot_ctx: &mut PlotContext) { - plot_ctx.add_cartesian2d_plot("Sampling Histogram", "Count"); + plot_ctx.add_timedomain_plot("Sampling Histogram", "Count"); if let Some(data) = ctx.obs_data() { let histogram = data.sampling_histogram().sorted(); let durations: Vec<_> = histogram.clone().map(|(dt, _)| dt.to_string()).collect(); diff --git a/rinex-cli/src/cli.rs b/rinex-cli/src/cli.rs index c9c293c5a..e147cb2ea 100644 --- a/rinex-cli/src/cli.rs +++ b/rinex-cli/src/cli.rs @@ -7,13 +7,19 @@ use std::str::FromStr; use rinex::prelude::*; use rinex_qc::QcOpts; -use gnss_rtk::prelude::{Config, Mode as SolverMode}; +use gnss_rtk::prelude::Config; pub struct Cli { /// Arguments passed by user matches: ArgMatches, } +impl Default for Cli { + fn default() -> Self { + Self::new() + } +} + impl Cli { /// Build new command line interface pub fn new() -> Self { @@ -238,36 +244,12 @@ The summary report by default is integrated to the global HTML report.")) .action(ArgAction::SetTrue) .help("Activates QC mode and disables all other features: quickest qc rendition.")) .next_help_heading("Positioning") - .arg(Arg::new("spp") - .long("spp") - .conflicts_with("ppp") - .conflicts_with("lsqspp") - .action(ArgAction::SetTrue) - .help("Enable Single Point Positioning. -Use with ${RUST_LOG} env logger for more information. -Refer to the positioning documentation.")) - .arg(Arg::new("lsqspp") - .long("lsqspp") - .conflicts_with("ppp") - .conflicts_with("spp") - .action(ArgAction::SetTrue) - .help("Recursive Weighted Least Square SPP strategy. -Use with ${RUST_LOG} env logger for more information. -Refer to the positioning documentation.")) - .arg(Arg::new("ppp") - .long("ppp") - .conflicts_with("spp") - .conflicts_with("lsqspp") + .arg(Arg::new("positioning") + .short('p') .action(ArgAction::SetTrue) - .help("Enable Precise Point Positioning. + .help("Activate positioning mode. Disables all other modes. Use with ${RUST_LOG} env logger for more information. Refer to the positioning documentation.")) - .arg(Arg::new("pos-only") - .long("pos-only") - .short('p') - .action(ArgAction::SetTrue) - .help("Disable context analysis and run position solver only. -This is the most performant mode to solve a position.")) .arg(Arg::new("config") .long("cfg") .short('c') @@ -453,25 +435,8 @@ Primary RINEX was either loaded with `-f`, or is Observation RINEX loaded with ` pub fn quiet(&self) -> bool { self.matches.get_flag("quiet") } - /* returns RTK solver mode to implement */ - pub fn solver_mode(&self) -> Option { - if self.matches.get_flag("spp") { - Some(SolverMode::SPP) - } else if self.matches.get_flag("lsqspp") { - Some(SolverMode::LSQSPP) - } else if self.matches.get_flag("ppp") { - Some(SolverMode::PPP) - } else { - None - } - } pub fn positioning(&self) -> bool { - self.matches.get_flag("spp") - || self.matches.get_flag("lsqspp") - || self.matches.get_flag("ppp") - } - pub fn positioning_only(&self) -> bool { - self.matches.get_flag("pos-only") + self.matches.get_flag("positioning") } pub fn gpx(&self) -> bool { self.matches.get_flag("gpx") diff --git a/rinex-cli/src/identification.rs b/rinex-cli/src/identification.rs index 9460844c4..e1207f5fa 100644 --- a/rinex-cli/src/identification.rs +++ b/rinex-cli/src/identification.rs @@ -105,7 +105,7 @@ struct SSIReport { fn report_sampling_histogram(data: &Vec<(Duration, usize)>) { let data: HashMap = data - .into_iter() + .iter() .map(|(dt, pop)| (dt.to_string(), *pop)) .collect(); println!("{:#?}", data); diff --git a/rinex-cli/src/main.rs b/rinex-cli/src/main.rs index 3cd96a445..8876a40bc 100644 --- a/rinex-cli/src/main.rs +++ b/rinex-cli/src/main.rs @@ -77,11 +77,11 @@ pub(crate) fn context_stem(ctx: &RnxContext) -> String { */ pub fn workspace_path(ctx: &RnxContext, cli: &Cli) -> PathBuf { match cli.workspace() { - Some(w) => Path::new(w).join(&context_stem(ctx)), + Some(w) => Path::new(w).join(context_stem(ctx)), None => Path::new(env!("CARGO_MANIFEST_DIR")) .join("..") .join("WORKSPACE") - .join(&context_stem(ctx)), + .join(context_stem(ctx)), } } @@ -133,7 +133,7 @@ fn build_context(cli: &Cli) -> RnxContext { * Returns true if Skyplot view if feasible and allowed */ fn skyplot_allowed(ctx: &RnxContext, cli: &Cli) -> bool { - if cli.quality_check_only() || cli.positioning_only() { + if cli.quality_check_only() || cli.positioning() { /* * Special modes: no plots allowed */ @@ -240,12 +240,7 @@ pub fn main() -> Result<(), Error> { let qc_only = cli.quality_check_only(); let qc = cli.quality_check() || qc_only; - let positioning_only = cli.positioning_only(); - let positioning = cli.positioning() || positioning_only; - - if !positioning { - warn!("position solver currently turned off"); - } + let positioning = cli.positioning(); // Initiate plot context let mut plot_ctx = PlotContext::new(); @@ -337,7 +332,7 @@ pub fn main() -> Result<(), Error> { None => String::from("merged.rnx"), }; - let path = workspace.clone().join(&filename); + let path = workspace.clone().join(filename); let path = path .as_path() @@ -406,10 +401,8 @@ pub fn main() -> Result<(), Error> { let ground_pos = ctx.ground_position().unwrap(); // infaillible plot::skyplot(nav, ground_pos, &mut plot_ctx); info!("skyplot view generated"); - } else { - if !no_graph { - info!("skyplot view is not feasible"); - } + } else if !no_graph { + info!("skyplot view is not feasible"); } } /* @@ -431,7 +424,7 @@ pub fn main() -> Result<(), Error> { * Record analysis / visualization * analysis depends on the provided record type */ - if !qc_only && !positioning_only && !no_graph { + if !qc_only && !positioning && !no_graph { info!("entering record analysis"); plot::plot_record(&ctx, &mut plot_ctx); diff --git a/rinex-cli/src/plot/combination.rs b/rinex-cli/src/plot/combination.rs index d297f36fc..34b0b4115 100644 --- a/rinex-cli/src/plot/combination.rs +++ b/rinex-cli/src/plot/combination.rs @@ -10,7 +10,7 @@ pub fn plot_gnss_combination( y_title: &str, ) { // add a plot - plot_context.add_cartesian2d_plot(plot_title, y_title); + plot_context.add_timedomain_plot(plot_title, y_title); // generate 1 marker per OP let markers = generate_markers(data.len()); @@ -49,7 +49,7 @@ pub fn plot_gnss_dcb_mp( y_title: &str, ) { // add a plot - plot_context.add_cartesian2d_plot(plot_title, y_title); + plot_context.add_timedomain_plot(plot_title, y_title); // generate 1 marker per OP let markers = generate_markers(data.len()); // plot all ops diff --git a/rinex-cli/src/plot/context.rs b/rinex-cli/src/plot/context.rs index 35303d3d3..62e453f96 100644 --- a/rinex-cli/src/plot/context.rs +++ b/rinex-cli/src/plot/context.rs @@ -1,6 +1,6 @@ use super::{ - build_default_2y_plot, build_default_3d_plot, build_default_plot, build_default_polar_plot, - build_world_map, Plot, + build_default_3d_plot, build_default_polar_plot, build_timedomain_2y_plot, + build_timedomain_plot, build_world_map, Plot, }; //use log::trace; use plotly::{layout::MapboxStyle, Trace}; @@ -21,8 +21,12 @@ impl PlotContext { let len = self.plots.len() - 1; self.plots.get_mut(len) }*/ - pub fn add_cartesian2d_plot(&mut self, title: &str, y_label: &str) { - self.plots.push(build_default_plot(title, y_label)); + pub fn add_timedomain_plot(&mut self, title: &str, y_label: &str) { + self.plots.push(build_timedomain_plot(title, y_label)); + } + pub fn add_timedomain_2y_plot(&mut self, title: &str, y1_label: &str, y2_label: &str) { + self.plots + .push(build_timedomain_2y_plot(title, y1_label, y2_label)); } pub fn add_cartesian3d_plot( &mut self, @@ -34,10 +38,6 @@ impl PlotContext { self.plots .push(build_default_3d_plot(title, x_label, y_label, z_label)); } - pub fn add_cartesian2d_2y_plot(&mut self, title: &str, y1_label: &str, y2_label: &str) { - self.plots - .push(build_default_2y_plot(title, y1_label, y2_label)); - } pub fn add_polar2d_plot(&mut self, title: &str) { self.plots.push(build_default_polar_plot(title)); } diff --git a/rinex-cli/src/plot/mod.rs b/rinex-cli/src/plot/mod.rs index b40a64234..051e0bdc8 100644 --- a/rinex-cli/src/plot/mod.rs +++ b/rinex-cli/src/plot/mod.rs @@ -233,16 +233,19 @@ pub fn generate_markers(n: usize) -> Vec { * builds a standard 2D plot single Y scale, * ready to plot data against time (`Epoch`) */ -pub fn build_default_plot(title: &str, y_title: &str) -> Plot { +pub fn build_timedomain_plot(title: &str, y_title: &str) -> Plot { build_plot( title, Side::Top, Font::default(), - "Epoch", + "MJD", y_title, (true, true), // y=0 lines true, // show legend true, // autosize + true, // show tick labels + 0.25, // ticks dx + "{:05}", // ticks fmt ) } @@ -267,17 +270,20 @@ pub fn build_default_3d_plot(title: &str, x_title: &str, y_title: &str, z_title: * build a standard 2D plot dual Y axes, * to plot against `Epochs` */ -pub fn build_default_2y_plot(title: &str, y1_title: &str, y2_title: &str) -> Plot { +pub fn build_timedomain_2y_plot(title: &str, y1_title: &str, y2_title: &str) -> Plot { build_plot_2y( title, Side::Top, Font::default(), - "Epoch", + "MJD", y1_title, y2_title, (false, false), // y=0 lines true, // show legend true, // autosize + true, // show x tick label + 0.25, // dx tick + "{:05}", // x tick fmt ) } @@ -285,16 +291,25 @@ pub fn build_default_2y_plot(title: &str, y1_title: &str, y2_title: &str) -> Plo * Builds a default Polar2D plot */ pub fn build_default_polar_plot(title: &str) -> Plot { - build_plot( - title, - Side::Top, - Font::default(), - "Latitude [°]", - "Longitude [°]", - (true, true), - true, - true, - ) + let layout = Layout::new() + .title(Title::new(title)) + .x_axis( + Axis::new() + .title(Title::new("Latitude [°]").side(Side::Top)) + .zero_line(true), //.show_tick_labels(show_tick_labels) + //.dtick(dx_tick) + //.tick_format(tick_fmt) + ) + .y_axis( + Axis::new() + .title(Title::new("Longitude [°]")) + .zero_line(true), + ) + .show_legend(true) + .auto_size(true); + let mut p = Plot::new(); + p.set_layout(layout); + p } /* @@ -337,6 +352,9 @@ fn build_plot( zero_line: (bool, bool), // plots a bold line @ (x=0,y=0) show_legend: bool, auto_size: bool, + show_xtick_labels: bool, + dx_tick: f64, + x_tick_fmt: &str, ) -> Plot { let layout = Layout::new() .title(Title::new(title).font(title_font)) @@ -344,7 +362,9 @@ fn build_plot( Axis::new() .title(Title::new(x_axis_title).side(title_side)) .zero_line(zero_line.0) - .show_tick_labels(false), + .show_tick_labels(show_xtick_labels) + .dtick(dx_tick) + .tick_format(x_tick_fmt), ) .y_axis( Axis::new() @@ -368,6 +388,9 @@ fn build_plot_2y( zero_line: (bool, bool), // plots a bold line @ (x=0,y=0) show_legend: bool, auto_size: bool, + show_xtick_labels: bool, + dx_tick: f64, + xtick_fmt: &str, ) -> Plot { let layout = Layout::new() .title(Title::new(title).font(title_font)) @@ -375,7 +398,9 @@ fn build_plot_2y( Axis::new() .title(Title::new(x_title).side(title_side)) .zero_line(zero_line.0) - .show_tick_labels(false), + .show_tick_labels(show_xtick_labels) + .dtick(dx_tick) + .tick_format(xtick_fmt), ) .y_axis( Axis::new() @@ -442,7 +467,7 @@ pub fn build_chart_epoch_axis( data_y: Vec, ) -> Box> { let txt: Vec = epochs.iter().map(|e| e.to_string()).collect(); - Scatter::new(epochs.iter().map(|e| e.to_utc_seconds()).collect(), data_y) + Scatter::new(epochs.iter().map(|e| e.to_mjd_utc_days()).collect(), data_y) .mode(mode) //.web_gl_mode(true) .name(name) diff --git a/rinex-cli/src/plot/record/ionex.rs b/rinex-cli/src/plot/record/ionex.rs index 93af53b97..b1a9b59e9 100644 --- a/rinex-cli/src/plot/record/ionex.rs +++ b/rinex-cli/src/plot/record/ionex.rs @@ -41,7 +41,7 @@ pub fn plot_tec_map(data: &Rinex, _borders: ((f64, f64), (f64, f64)), plot_ctx: }, ) .collect(); - let tec: Vec<_> = data + let _tec: Vec<_> = data .tec() .filter_map( |(t, _, _, _, tec)| { diff --git a/rinex-cli/src/plot/record/ionosphere.rs b/rinex-cli/src/plot/record/ionosphere.rs index 9def95f95..d54aa9e3d 100644 --- a/rinex-cli/src/plot/record/ionosphere.rs +++ b/rinex-cli/src/plot/record/ionosphere.rs @@ -1,115 +1,85 @@ -use crate::plot::PlotContext; -use hifitime::{Epoch, TimeScale}; - -use rinex::navigation::KbModel; -use rinex::prelude::{Constellation, GroundPosition, RnxContext, SV}; -use std::f64::consts::PI; - -fn klob_ionospheric_delay( - t: Epoch, - sv: SV, - model: KbModel, - elev_azim_deg: (f64, f64), - lat_lon_ddeg: (f64, f64), -) -> f64 { - let t = t.to_duration_in_time_scale(TimeScale::GPST).to_seconds(); - - let alpha = model.alpha; - let beta = model.beta; - let re = 6371.0E3_f64; - let phi_p = map_3d::deg2rad(78.3); - let lambda_p = map_3d::deg2rad(291.0); - let phi_u = map_3d::deg2rad(lat_lon_ddeg.0); - let lambda_u = map_3d::deg2rad(lat_lon_ddeg.1); - let e = map_3d::deg2rad(elev_azim_deg.0); - let a = map_3d::deg2rad(elev_azim_deg.1); - - let h = match sv.constellation { - Constellation::BeiDou => 375.0E3, - _ => 350.0E3, - }; - - let psi = PI / 2.0_f64 - e - (re * e.cos() / (re + h)).asin(); - - let phi_ipp = phi_u.sin() * psi.cos() + phi_u.cos() * psi.sin() * a.cos(); - let lambda_i = lambda_u + psi * a.sin() / phi_ipp.cos(); - let phi_m = - phi_ipp.sin() * phi_p.sin() + phi_ipp.cos() * phi_p.cos() * (lambda_i - lambda_p).cos(); - - let mut t_ipp = 43.200E3 * lambda_i / PI + t; - if t_ipp > 86400.0 { - t_ipp -= 86400.0; - } else if t_ipp < 0.0 { - t_ipp += 86400.0; - } - - let mut a_i = alpha.0 - + alpha.1 * (phi_m / PI).powi(1) - + alpha.2 * (phi_m / PI).powi(2) - + alpha.3 * (phi_m / PI).powi(3); - - if a_i < 0.0 { - a_i = 0.0; - } - - let mut p_i = beta.0 - + beta.1 * (phi_m / PI).powi(1) - + beta.2 * (phi_m / PI).powi(2) - + beta.3 * (phi_m / PI).powi(3); - - if p_i < 72000.0 { - p_i = 72000.0; - } - - let x_i = 2.0_f64 * PI * (t_ipp - 50.400E3) / p_i; - - let f = 1.0 / (1.0 - (re * e.cos() / (re + h)).powi(2)).sqrt(); - - if x_i.abs() < PI / 2.0 { - (5.0E-9 + a_i * x_i.cos()) * f - } else { - 5.0E-9 * f - } -} - -pub fn plot_ionospheric_delay(ctx: &RnxContext, _plot_ctx: &mut PlotContext) { - let ref_pos = ctx.ground_position().unwrap_or(GroundPosition::default()); +use crate::plot::{build_chart_epoch_axis, PlotContext}; +// use hifitime::{Epoch, TimeScale}; +use plotly::common::{ + //Marker, + //MarkerSymbol, + Mode, + Visible, +}; + +use rinex::carrier::Carrier; +use rinex::navigation::Ephemeris; +// use rinex::navigation::KbModel; +use rinex::prelude::RnxContext; + +pub fn plot_ionospheric_delay(ctx: &RnxContext, plot_ctx: &mut PlotContext) { + let ref_pos = ctx.ground_position().unwrap_or_default(); let ref_geo = ref_pos.to_geodetic(); - let _lat_lon_ddeg = (ref_geo.0, ref_geo.1); - - // if let Some(nav) = ctx.nav_data() { - // let mut kb_delay: Vec<(Epoch, f64)> = Vec::new(); - // for (_index, (t, svnn)) in nav.sv_epoch().enumerate() { - // for sv in svnn { - // if let Some(t, (_, _, model)) = nav.ionosphere_models(t) { - // match model { - // IonMessage::KlobucharModel(model) => { - // let sv_elev_azim = nav - // .sv_elevation_azimuth(Some(ref_pos)) - // .find(|(epoch, svnn, _)| *epoch == t && *svnn == sv); - // if let Some(elev_azim) = sv_elev_azim { - // kb_delay.push(( - // t, - // klob_ionospheric_delay(t, sv, model, elev_azim.2, lat_lon_ddeg), - // )); - // } - // }, - // _ => {}, - // } - // } - // } - // } - // if !kb_delay.is_empty() { - // trace!("klobuchar ionospheric model"); - // plot_ctx.add_cartesian2d_plot("Ionospheric Delay", "Delay [s]"); - // let trace = build_chart_epoch_axis( - // "kb", - // Mode::LinesMarkers, - // kb_delay.iter().map(|(t, _)| *t).collect(), - // kb_delay.iter().map(|(_, dly)| *dly).collect(), - // ); - // plot_ctx.add_trace(trace); - // } - // } + let lat_lon_ddeg = (ref_geo.0, ref_geo.1); + let ref_ecef_wgs84 = ref_pos.to_ecef_wgs84(); + + if let Some(obs) = ctx.obs_data() { + if let Some(nav) = ctx.nav_data() { + for (sv_index, sv) in obs.sv().enumerate() { + if sv_index == 0 { + plot_ctx.add_timedomain_plot("Ionospheric Delay", "meters of delay"); + trace!("ionod corr plot"); + } + let codes = obs + .observable() + .filter(|obs| obs.is_pseudorange_observable()) + .collect::>(); + /* + * Plot the ionod corr for each code measurement, at every Epoch + */ + for (code_index, code) in codes.iter().enumerate() { + let x = obs + .pseudo_range() + .filter_map(|((t, t_flag), svnn, observable, _)| { + if t_flag.is_ok() && svnn == sv && &observable == code { + Some(t) + } else { + None + } + }) + .collect::>(); + + let y = x + .iter() + .filter_map(|t| { + /* + * prefer SP3 for the position determination + */ + let sv_position = match ctx.sp3_data() { + Some(sp3) => sp3.sv_position_interpolate(sv, *t, 11), + None => nav.sv_position_interpolate(sv, *t, 11), + }; + let sv_position = sv_position?; + let (elev, azim) = + Ephemeris::elevation_azimuth(sv_position, ref_ecef_wgs84); + let (lat, lon) = lat_lon_ddeg; + let freq = Carrier::from_observable(sv.constellation, code).ok()?; + let ionod_corr = + nav.ionod_correction(*t, elev, azim, lat, lon, freq)?; + Some(ionod_corr) + }) + .collect::>(); + + let chart = + build_chart_epoch_axis(&format!("{:X}({})", sv, code), Mode::Markers, x, y) + .visible({ + if sv_index < 2 && code_index < 2 { + Visible::True + //Visible::LegendOnly + } else { + Visible::True + //Visible::LegendOnly + } + }); + plot_ctx.add_trace(chart); + } + } + } + } } diff --git a/rinex-cli/src/plot/record/meteo.rs b/rinex-cli/src/plot/record/meteo.rs index e45c8ab88..24b3c85bf 100644 --- a/rinex-cli/src/plot/record/meteo.rs +++ b/rinex-cli/src/plot/record/meteo.rs @@ -22,7 +22,7 @@ pub fn plot_meteo(rnx: &Rinex, plot_context: &mut PlotContext) { Observable::HailIndicator => "", _ => unreachable!(), }; - plot_context.add_cartesian2d_plot( + plot_context.add_timedomain_plot( &format!("{} Observations", observable), &format!("{} [{}]", observable, unit), ); diff --git a/rinex-cli/src/plot/record/navigation.rs b/rinex-cli/src/plot/record/navigation.rs index be56b5842..9d534d877 100644 --- a/rinex-cli/src/plot/record/navigation.rs +++ b/rinex-cli/src/plot/record/navigation.rs @@ -13,7 +13,7 @@ pub fn plot_navigation(ctx: &RnxContext, plot_ctx: &mut PlotContext) { */ for (sv_index, sv) in nav.sv().enumerate() { if sv_index == 0 { - plot_ctx.add_cartesian2d_2y_plot( + plot_ctx.add_timedomain_2y_plot( "SV Clock Bias", "Clock Bias [s]", "Clock Drift [s/s]", @@ -105,7 +105,7 @@ pub fn plot_navigation(ctx: &RnxContext, plot_ctx: &mut PlotContext) { if let Some(sp3) = ctx.sp3_data() { for (sv_index, sv) in sp3.sv().enumerate() { if sv_index == 0 && !clock_plot_created { - plot_ctx.add_cartesian2d_2y_plot( + plot_ctx.add_timedomain_2y_plot( "SV Clock Bias", "Clock Bias [s]", "Clock Drift [s/s]", @@ -234,17 +234,10 @@ pub fn plot_navigation(ctx: &RnxContext, plot_ctx: &mut PlotContext) { */ if let Some(sp3) = ctx.sp3_data() { for (sv_index, sv) in sp3.sv().enumerate() { - if sv_index == 0 { - if !pos_plot_created { - plot_ctx.add_cartesian3d_plot( - "SV Orbit (broadcast)", - "x [km]", - "y [km]", - "z [km]", - ); - trace!("broadcast orbit plot"); - pos_plot_created = true; - } + if sv_index == 0 && !pos_plot_created { + plot_ctx.add_cartesian3d_plot("SV Orbit (broadcast)", "x [km]", "y [km]", "z [km]"); + trace!("broadcast orbit plot"); + pos_plot_created = true; } let epochs: Vec<_> = sp3 .sv_position() @@ -319,7 +312,7 @@ pub fn plot_navigation(ctx: &RnxContext, plot_ctx: &mut PlotContext) { if let Some(obsdata) = ctx.obs_data() { for (sv_index, sv) in obsdata.sv().enumerate() { if sv_index == 0 { - plot_ctx.add_cartesian2d_plot("SV Clock Correction", "Correction [s]"); + plot_ctx.add_timedomain_plot("SV Clock Correction", "Correction [s]"); trace!("sv clock correction plot"); } let epochs: Vec<_> = obsdata @@ -334,7 +327,7 @@ pub fn plot_navigation(ctx: &RnxContext, plot_ctx: &mut PlotContext) { .collect(); let clock_corr: Vec<_> = obsdata .observation() - .filter_map(|((t, flag), (_, vehicles))| { + .filter_map(|((t, flag), (_, _vehicles))| { if flag.is_ok() { let (toe, sv_eph) = navdata.sv_ephemeris(sv, *t)?; /* diff --git a/rinex-cli/src/plot/record/observation.rs b/rinex-cli/src/plot/record/observation.rs index cc0ef8ac6..5de7eb8b3 100644 --- a/rinex-cli/src/plot/record/observation.rs +++ b/rinex-cli/src/plot/record/observation.rs @@ -78,7 +78,7 @@ pub fn plot_observation(ctx: &RnxContext, plot_context: &mut PlotContext) { } if !clk_offset.is_empty() { - plot_context.add_cartesian2d_plot("Receiver Clock Offset", "Clock Offset [s]"); + plot_context.add_timedomain_plot("Receiver Clock Offset", "Clock Offset [s]"); let data_x: Vec = clk_offset.iter().map(|(k, _)| *k).collect(); let data_y: Vec = clk_offset.iter().map(|(_, v)| *v).collect(); let trace = build_chart_epoch_axis("Clk Offset", Mode::LinesMarkers, data_x, data_y) @@ -101,14 +101,14 @@ pub fn plot_observation(ctx: &RnxContext, plot_context: &mut PlotContext) { if ctx.has_navigation_data() { // Augmented context, we plot data on two Y axes // one for physical observation, one for sat elevation - plot_context.add_cartesian2d_2y_plot( + plot_context.add_timedomain_2y_plot( &format!("{} Observations", physics), y_label, "Elevation Angle [°]", ); } else { // standard mode: one axis - plot_context.add_cartesian2d_plot(&format!("{} Observations", physics), y_label); + plot_context.add_timedomain_plot(&format!("{} Observations", physics), y_label); } let markers = generate_markers(carriers.len()); // one symbol per carrier diff --git a/rinex-cli/src/positioning/post_process.rs b/rinex-cli/src/positioning/post_process.rs index 680997d45..bd50992cb 100644 --- a/rinex-cli/src/positioning/post_process.rs +++ b/rinex-cli/src/positioning/post_process.rs @@ -28,10 +28,9 @@ use plotly::common::{Marker, MarkerSymbol}; use plotly::layout::MapboxStyle; use plotly::ScatterMapbox; -use map_3d::{ecef2geodetic, rad2deg, Ellipsoid}; - use crate::fops::open_with_web_browser; use crate::plot::{build_3d_chart_epoch_label, build_chart_epoch_axis, PlotContext}; +use map_3d::{ecef2geodetic, rad2deg, Ellipsoid}; #[derive(Debug, Error)] pub enum Error { @@ -68,9 +67,9 @@ pub fn post_process( let (mut lat, mut lon) = (Vec::::new(), Vec::::new()); for result in results.values() { - let px = x + result.p.x; - let py = y + result.p.y; - let pz = z + result.p.z; + let px = x + result.pos.x; + let py = y + result.pos.y; + let pz = z + result.pos.z; let (lat_ddeg, lon_ddeg, _) = ecef2geodetic(px, py, pz, Ellipsoid::WGS84); lat.push(rad2deg(lat_ddeg)); lon.push(rad2deg(lon_ddeg)); @@ -108,9 +107,9 @@ pub fn post_process( "error", Mode::Markers, epochs.clone(), - results.values().map(|e| e.p.x).collect::>(), - results.values().map(|e| e.p.y).collect::>(), - results.values().map(|e| e.p.z).collect::>(), + results.values().map(|e| e.pos.x).collect::>(), + results.values().map(|e| e.pos.y).collect::>(), + results.values().map(|e| e.pos.z).collect::>(), ); /* @@ -128,12 +127,12 @@ pub fn post_process( ); plot_ctx.add_trace(trace); - plot_ctx.add_cartesian2d_2y_plot("Velocity (X & Y)", "Speed [m/s]", "Speed [m/s]"); + plot_ctx.add_timedomain_2y_plot("Velocity (X & Y)", "Speed [m/s]", "Speed [m/s]"); let trace = build_chart_epoch_axis( "velocity (x)", Mode::Markers, epochs.clone(), - results.values().map(|p| p.v.x).collect::>(), + results.values().map(|p| p.vel.x).collect::>(), ); plot_ctx.add_trace(trace); @@ -141,21 +140,21 @@ pub fn post_process( "velocity (y)", Mode::Markers, epochs.clone(), - results.values().map(|p| p.v.y).collect::>(), + results.values().map(|p| p.vel.y).collect::>(), ) .y_axis("y2"); plot_ctx.add_trace(trace); - plot_ctx.add_cartesian2d_plot("Velocity (Z)", "Speed [m/s]"); + plot_ctx.add_timedomain_plot("Velocity (Z)", "Speed [m/s]"); let trace = build_chart_epoch_axis( "velocity (z)", Mode::Markers, epochs.clone(), - results.values().map(|p| p.v.z).collect::>(), + results.values().map(|p| p.vel.z).collect::>(), ); plot_ctx.add_trace(trace); - plot_ctx.add_cartesian2d_plot("GDOP", "GDOP [m]"); + plot_ctx.add_timedomain_plot("GDOP", "GDOP [m]"); let trace = build_chart_epoch_axis( "gdop", Mode::Markers, @@ -164,7 +163,7 @@ pub fn post_process( ); plot_ctx.add_trace(trace); - plot_ctx.add_cartesian2d_2y_plot("HDOP, VDOP", "HDOP [m]", "VDOP [m]"); + plot_ctx.add_timedomain_2y_plot("HDOP, VDOP", "HDOP [m]", "VDOP [m]"); let trace = build_chart_epoch_axis( "hdop", Mode::Markers, @@ -188,7 +187,7 @@ pub fn post_process( .y_axis("y2"); plot_ctx.add_trace(trace); - plot_ctx.add_cartesian2d_2y_plot("Clock offset", "dt [s]", "TDOP [s]"); + plot_ctx.add_timedomain_2y_plot("Clock offset", "dt [s]", "TDOP [s]"); let trace = build_chart_epoch_axis( "dt", Mode::Markers, @@ -231,7 +230,7 @@ pub fn post_process( )?; for (epoch, solution) in results { - let (px, py, pz) = (x + solution.p.x, y + solution.p.y, z + solution.p.z); + let (px, py, pz) = (x + solution.pos.x, y + solution.pos.y, z + solution.pos.z); let (lat, lon, alt) = map_3d::ecef2geodetic(px, py, pz, map_3d::Ellipsoid::WGS84); let (hdop, vdop, tdop) = ( solution.hdop(lat_ddeg, lon_ddeg), @@ -242,15 +241,15 @@ pub fn post_process( fd, "{:?}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}, {:.6E}", epoch, - solution.p.x, - solution.p.y, - solution.p.z, + solution.pos.x, + solution.pos.y, + solution.pos.z, px, py, pz, - solution.v.x, - solution.v.y, - solution.v.z, + solution.vel.x, + solution.vel.y, + solution.vel.z, hdop, vdop, solution.dt, @@ -298,7 +297,7 @@ pub fn post_process( } info!("\"{}\" generated", txtfile); if cli.gpx() { - let gpxpath = workspace.join(&format!("{}.gpx", context_stem(&ctx))); + let gpxpath = workspace.join(format!("{}.gpx", context_stem(ctx))); let gpxfile = gpxpath.to_string_lossy().to_string(); let fd = File::create(&gpxfile)?; @@ -312,7 +311,7 @@ pub fn post_process( info!("{} gpx track generated", gpxfile); } if cli.kml() { - let kmlpath = workspace.join(&format!("{}.kml", context_stem(&ctx))); + let kmlpath = workspace.join(format!("{}.kml", context_stem(ctx))); let kmlfile = kmlpath.to_string_lossy().to_string(); let mut fd = File::create(&kmlfile)?; diff --git a/rinex-cli/src/positioning/solver.rs b/rinex-cli/src/positioning/solver.rs index de202750a..8fe45347a 100644 --- a/rinex-cli/src/positioning/solver.rs +++ b/rinex-cli/src/positioning/solver.rs @@ -7,9 +7,9 @@ use rinex::navigation::Ephemeris; use rinex::prelude::{Observable, Rinex, RnxContext}; use rtk::prelude::{ - AprioriPosition, BdModel, Candidate, Config, Duration, Epoch, InterpolatedPosition, - InterpolationResult, IonosphericBias, KbModel, Mode, NgModel, Observation, PVTSolution, - PVTSolutionType, Solver, TroposphericBias, Vector3, + AprioriPosition, BdModel, Candidate, Config, Duration, Epoch, InterpolationResult, + IonosphericBias, KbModel, Method, NgModel, Observation, PVTSolution, PVTSolutionType, Solver, + TroposphericBias, Vector3, }; use map_3d::{ecef2geodetic, Ellipsoid}; @@ -110,22 +110,31 @@ fn kb_model(nav: &Rinex, t: Epoch) -> Option { .klobuchar_models() .min_by_key(|(t_i, _, _)| (t - *t_i).abs()); - match kb_model { - Some((_, sv, kb_model)) => { + if let Some((_, sv, kb_model)) = kb_model { + Some(KbModel { + h_km: { + match sv.constellation { + Constellation::BeiDou => 375.0, + // we only expect GPS or BDS here, + // badly formed RINEX will generate errors in the solutions + _ => 350.0, + } + }, + alpha: kb_model.alpha, + beta: kb_model.beta, + }) + } else { + /* RINEX 3 case */ + let iono_corr = nav.header.ionod_correction?; + if let Some(kb_model) = iono_corr.as_klobuchar() { Some(KbModel { - h_km: { - match sv.constellation { - Constellation::BeiDou => 375.0, - // we only expect GPS or BDS here, - // badly formed RINEX will generate errors in the solutions - _ => 350.0, - } - }, + h_km: 350.0, //TODO improve this alpha: kb_model.alpha, beta: kb_model.beta, }) - }, - None => None, + } else { + None + } } } @@ -142,19 +151,18 @@ fn ng_model(nav: &Rinex, t: Epoch) -> Option { } pub fn solver(ctx: &mut RnxContext, cli: &Cli) -> Result, Error> { - // custom strategy - let mode = cli.solver_mode().unwrap(); // infaillible - - match mode { - Mode::SPP => info!("single point positioning"), - Mode::LSQSPP => info!("recursive lsq single point positioning"), - Mode::PPP => info!("precise point positioning"), - }; - // parse custom config, if any let cfg = match cli.config() { Some(cfg) => cfg, - None => Config::default(mode), + None => { + /* no manual config: we use the optimal known to this day */ + Config::preset(Method::SPP) + }, + }; + + match cfg.method { + Method::SPP => info!("single point positioning"), + Method::PPP => info!("precise point positioning"), }; let pos = match cli.manual_position() { @@ -196,9 +204,8 @@ pub fn solver(ctx: &mut RnxContext, cli: &Cli) -> Result Result Result { let sp3 = sp3_data.unwrap(); - if let Some(clk) = sp3 + if let Some(_clk) = sp3 .sv_clock() .filter_map(|(sp3_t, sp3_sv, clk)| { if sp3_t == *t && sp3_sv == *sv { @@ -334,37 +332,19 @@ pub fn solver(ctx: &mut RnxContext, cli: &Cli) -> Result"] description = "RINEX data analysis" @@ -28,7 +28,7 @@ itertools = "0.12.0" statrs = "0.16" sp3 = { path = "../sp3", version = "=1.0.6", features = ["serde"] } rinex-qc-traits = { path = "../qc-traits", version = "=0.1.1" } -rinex = { path = "../rinex", version = "=0.15.1", features = ["full"] } +rinex = { path = "../rinex", version = "=0.15.2", features = ["full"] } gnss-rs = { version = "2.1.2", features = ["serde"] } [dev-dependencies] diff --git a/rinex/Cargo.toml b/rinex/Cargo.toml index e19bdbd67..a3d6b2d8b 100644 --- a/rinex/Cargo.toml +++ b/rinex/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rinex" -version = "0.15.1" +version = "0.15.2" license = "MIT OR Apache-2.0" authors = ["Guillaume W. Bres "] description = "Package to parse and analyze RINEX data" @@ -19,6 +19,7 @@ obs = [] meteo = [] nav = [] ionex = [] +antex = [] processing = [] # rinex Quality Check (mainly OBS RINEX) @@ -29,6 +30,7 @@ sp3 = ["dep:sp3", "walkdir"] # enable everything full = [ + "antex", "flate2", "horrorshow", "ionex", diff --git a/rinex/build.rs b/rinex/build.rs index 75ea9bd63..02a2c2320 100644 --- a/rinex/build.rs +++ b/rinex/build.rs @@ -5,7 +5,7 @@ use std::path::Path; fn build_nav_database() { let outdir = env::var("OUT_DIR").unwrap(); let nav_path = Path::new(&outdir).join("nav_orbits.rs"); - let mut nav_file = std::fs::File::create(&nav_path).unwrap(); + let mut nav_file = std::fs::File::create(nav_path).unwrap(); // read helper descriptor let nav_descriptor = std::fs::read_to_string("db/NAV/orbits.json").unwrap(); diff --git a/rinex/src/antex/antenna.rs b/rinex/src/antex/antenna.rs deleted file mode 100644 index 4b39e0ad6..000000000 --- a/rinex/src/antex/antenna.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::Epoch; -use strum_macros::EnumString; - -/// Known Calibration Methods -#[derive(Default, Clone, Debug, PartialEq, PartialOrd, EnumString)] -#[cfg_attr(feature = "serde", derive(Serialize))] -pub enum CalibrationMethod { - #[strum(serialize = "")] - Unknown, - #[default] - #[strum(serialize = "CHAMBER")] - Chamber, - #[strum(serialize = "FIELD")] - Field, - #[strum(serialize = "ROBOT")] - Robot, - /// Copied from other antenna - #[strum(serialize = "COPIED")] - Copied, - /// Converted from igs_01.pcv or blank - #[strum(serialize = "CONVERTED")] - Converted, -} - -/// Calibration information -#[derive(Default, Clone, Debug, PartialEq, PartialOrd)] -#[cfg_attr(feature = "serde", derive(Serialize))] -pub struct Calibration { - /// Calibration method - pub method: CalibrationMethod, - /// Agency who performed the calibration - pub agency: String, - /// Date of calibration - pub date: String, -} - -/// Describes an Antenna section inside the ATX record -#[derive(Clone, Debug, PartialEq, PartialOrd)] -#[cfg_attr(feature = "serde", derive(Serialize))] -pub struct Antenna { - pub ant_type: String, - pub sn: String, - /// Calibration informations - pub calibration: Calibration, - /// Increment of the azimuth, in degrees - pub dazi: f64, - pub zen: (f64, f64), - pub dzen: f64, - /// Optionnal SNX, standard IGS/SNX format, - /// used when referencing this model - pub sinex_code: Option, - /// Optionnal validity: start date - pub valid_from: Option, - /// Optionnal end of validity - pub valid_until: Option, -} - -impl Default for Antenna { - fn default() -> Self { - Self { - ant_type: String::from("?"), - sn: String::from("?"), - calibration: Calibration::default(), - dazi: 0.0_f64, - zen: (0.0_f64, 0.0_f64), - dzen: 0.0_f64, - sinex_code: None, - valid_from: None, - valid_until: None, - } - } -} - -impl Antenna { - pub fn with_type(&self, ant_type: &str) -> Self { - let mut a = self.clone(); - a.ant_type = ant_type.to_string(); - a - } - pub fn with_serial_num(&self, sn: &str) -> Self { - let mut a = self.clone(); - a.sn = sn.to_string(); - a - } - pub fn with_calibration(&self, c: Calibration) -> Self { - let mut a = self.clone(); - a.calibration = c.clone(); - a - } - pub fn with_dazi(&self, dazi: f64) -> Self { - let mut a = self.clone(); - a.dazi = dazi; - a - } - pub fn with_zenith(&self, zen1: f64, zen2: f64, dzen: f64) -> Self { - let mut a = self.clone(); - a.zen = (zen1, zen2); - a.dzen = dzen; - a - } - pub fn with_valid_from(&self, e: Epoch) -> Self { - let mut a = self.clone(); - a.valid_from = Some(e); - a - } - pub fn with_valid_until(&self, e: Epoch) -> Self { - let mut a = self.clone(); - a.valid_until = Some(e); - a - } - pub fn with_sinex_code(&self, code: &str) -> Self { - let mut a = self.clone(); - a.sinex_code = Some(code.to_string()); - a - } -} diff --git a/rinex/src/antex/antenna/mod.rs b/rinex/src/antex/antenna/mod.rs new file mode 100644 index 000000000..3b3fb50ae --- /dev/null +++ b/rinex/src/antex/antenna/mod.rs @@ -0,0 +1,163 @@ +use crate::linspace::Linspace; +use crate::Epoch; +use strum_macros::EnumString; + +#[cfg(feature = "serde")] +use serde::Serialize; + +mod sv; +pub use sv::{Cospar, SvAntenna, SvAntennaParsingError}; + +/// Known Calibration Methods +#[derive(Default, Clone, Debug, PartialEq, PartialOrd, EnumString)] +#[cfg_attr(feature = "serde", derive(Serialize))] +pub enum CalibrationMethod { + #[strum(serialize = "")] + #[default] + Unknown, + #[strum(serialize = "CHAMBER")] + Chamber, + #[strum(serialize = "FIELD")] + Field, + #[strum(serialize = "ROBOT")] + Robot, + /// Copied from other antenna + #[strum(serialize = "COPIED")] + Copied, + /// Converted from igs_01.pcv or blank + #[strum(serialize = "CONVERTED")] + Converted, +} + +/// Calibration information +#[derive(Default, Clone, Debug, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize))] +pub struct Calibration { + /// Calibration method + pub method: CalibrationMethod, + /// Agency who performed this calibration + pub agency: String, + /// Date of calibration + pub date: Epoch, + /// Number of calibrated antennas + pub number: u16, + /// Calibration Validity Period: (Start, End) inclusive + pub validity_period: Option<(Epoch, Epoch)>, +} + +/// Antenna description, as contained in ATX records +#[derive(Default, Clone, Debug, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize))] +pub struct Antenna { + /// Antenna specific field, either a + /// spacecraft antenna or a receiver antenna + pub specific: AntennaSpecific, + /// Information on the calibration process. + pub calibration: Calibration, + /// Zenith grid definition. + /// The grid is expressed in zenith angles for RxAntenneas, + /// or in nadir Angle for SvAntennas. + pub zenith_grid: Linspace, + /// Azmiuth increment + pub azi_inc: f64, + /// SINEX code normalization + pub sinex_code: String, +} + +impl Antenna { + /// Returns whether this calibration is valid at the current Epoch + /// or not. Note that specs that did not come with a calibration + /// certificate validity are always considered valid. + /// You then need to refer to the date of that calibration (always given) + /// and should only consider recently calibrated data. + pub fn is_valid(&self, now: Epoch) -> bool { + if let Some((from, until)) = self.calibration.validity_period { + now > from && now < until + } else { + true + } + } + // /// Returns the mean phase center position. + // /// If Self is a Receiver Antenna ([`RxAntenna`]), + // /// the returned position is expressed as an offset to the + // /// Antenna Reference Position (ARP). + // /// If Self is a Spacecraft Antenna ([`SvAntenna`]), + // /// the returned position is expressed as an offset to the Spacecraft + // /// Mass Center. + // fn mean_phase_center(&self, _reference: (f64, f64, f64)) -> (f64, f64, f64) { + // (0.0_f64, 0.0_f64, 0.0_f64) + // } + /// Builds an Antenna with given Calibration infos + pub fn with_calibration(&self, calib: Calibration) -> Self { + let mut a = self.clone(); + a.calibration = calib.clone(); + a + } + /// Builds an Antenna with given Zenith Grid + pub fn with_zenith_grid(&self, grid: Linspace) -> Self { + let mut a = self.clone(); + a.zenith_grid = grid.clone(); + a + } + /// Builds an Antenna with given Validity period + pub fn with_validity_period(&self, start: Epoch, end: Epoch) -> Self { + let mut a = self.clone(); + a.calibration.validity_period = Some((start, end)); + a + } + /// Builds an Antenna with given Azimuth increment + pub fn with_dazi(&self, dazi: f64) -> Self { + let mut a = self.clone(); + a.azi_inc = dazi; + a + } + /// Add custom specificities + pub fn with_specificities(&self, specs: AntennaSpecific) -> Self { + let mut a = self.clone(); + a.specific = specs.clone(); + a + } +} + +#[derive(Clone, Debug, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize))] +pub enum AntennaSpecific { + /// Attributes of a receiver antenna + RxAntenna(RxAntenna), + /// Attributes of a spacecraft antenna + SvAntenna(sv::SvAntenna), +} + +impl Default for AntennaSpecific { + fn default() -> Self { + Self::RxAntenna(RxAntenna::default()) + } +} + +/// Antenna Matcher is used to easily locate RX antennas +/// contained in ATX records. AntennaMatcher is case insensitive. +#[derive(Clone, Debug)] +pub enum AntennaMatcher { + /// Identify an (RX) antenna model by its IGS code + IGSCode(String), + /// Identify an (RX) antenna model by its serial number + SerialNumber(String), +} + +impl AntennaMatcher { + pub(crate) fn to_lowercase(&self) -> Self { + match self { + Self::IGSCode(code) => Self::IGSCode(code.to_lowercase()), + Self::SerialNumber(sn) => Self::SerialNumber(sn.to_lowercase()), + } + } +} + +#[derive(Default, Clone, Debug, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize))] +pub struct RxAntenna { + /// IGS antenna code + pub igs_type: String, + /// Antenna serial number + pub serial_number: Option, +} diff --git a/rinex/src/antex/antenna/sv.rs b/rinex/src/antex/antenna/sv.rs new file mode 100644 index 000000000..a4e5e5863 --- /dev/null +++ b/rinex/src/antex/antenna/sv.rs @@ -0,0 +1,61 @@ +use gnss_rs::prelude::SV; +use thiserror::Error; + +#[cfg(feature = "serde")] +use serde::Serialize; + +#[derive(Debug, Clone, Error)] +pub enum SvAntennaParsingError { + #[error("cospar bad length")] + CosparBadLength, + #[error("failed to parse cospar launch year")] + CosparLaunchYearParsing, + #[error("failed to parse cospar launch code")] + CosparLaunchCodeParsing, +} + +#[derive(Default, Clone, Debug, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize))] +pub struct SvAntenna { + /// IGS antenna code + pub igs_type: String, + /// Spacecraft to which this antenna is attached to + pub sv: SV, + /// Cospar information + pub cospar: Cospar, +} + +#[derive(Default, Clone, Debug, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize))] +pub struct Cospar { + /// Vehicle launch year + pub launch_year: u16, + /// Launcher ID + pub launch_vehicle: String, + /// Launch code + pub launch_code: char, +} + +impl std::str::FromStr for Cospar { + type Err = SvAntennaParsingError; + fn from_str(content: &str) -> Result { + let s = content.trim(); + if s.len() != 9 { + return Err(SvAntennaParsingError::CosparBadLength); + } + let year = s[0..4] + .parse::() + .map_err(|_| SvAntennaParsingError::CosparLaunchYearParsing)?; + + let _launch_code = s[8..9] + .chars() + .next() + .ok_or(SvAntennaParsingError::CosparLaunchCodeParsing)?; + + Ok(Self { + launch_year: year, + launch_vehicle: s[4..6].to_string(), + launch_code: s[8..9].chars().next().unwrap(), + }) + } +} diff --git a/rinex/src/antex/mod.rs b/rinex/src/antex/mod.rs index 79913def6..82160454c 100644 --- a/rinex/src/antex/mod.rs +++ b/rinex/src/antex/mod.rs @@ -4,32 +4,40 @@ pub mod frequency; pub mod pcv; pub mod record; -pub use antenna::{Antenna, Calibration, CalibrationMethod}; -pub use frequency::{Frequency, Pattern}; pub use pcv::Pcv; -pub use record::Record; +// pub use frequency::{Frequency, Pattern}; + +pub use antenna::{ + Antenna, AntennaMatcher, AntennaSpecific, Calibration, CalibrationMethod, Cospar, RxAntenna, + SvAntenna, +}; + +pub use record::{FrequencyDependentData, Record}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct HeaderFields { - /// Phase Center Variations - pub pcv: pcv::Pcv, + /// Type of Phase Center Variation in use + pub pcv_type: pcv::Pcv, /// Optionnal reference antenna Serial Number /// used to produce this calibration file - pub reference_sn: Option, + pub reference_ant_sn: Option, } impl HeaderFields { - /// Sets Phase Center Variations - pub fn with_pcv(&self, pcv: Pcv) -> Self { + /// Set Phase Center Variations type + pub fn with_pcv_type(&self, pcv: Pcv) -> Self { let mut s = self.clone(); - s.pcv = pcv; + s.pcv_type = pcv; s } /// Sets Reference Antenna serial number - pub fn with_serial_number(&self, sn: &str) -> Self { + pub fn with_reference_antenna_sn(&self, sn: &str) -> Self { let mut s = self.clone(); - s.reference_sn = Some(sn.to_string()); + s.reference_ant_sn = Some(sn.to_string()); s } } diff --git a/rinex/src/antex/pcv.rs b/rinex/src/antex/pcv.rs index 5ea4ab113..fd61c1054 100644 --- a/rinex/src/antex/pcv.rs +++ b/rinex/src/antex/pcv.rs @@ -11,10 +11,10 @@ pub enum Error { #[derive(Default, Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Pcv { - /// Given data is absolute + /// PCV is absolute #[default] Absolute, - /// Given data is relative, with type of relativity + /// PCV is relative to attached reference Relative(String), } diff --git a/rinex/src/antex/record.rs b/rinex/src/antex/record.rs index be50d537e..ac1d5018c 100644 --- a/rinex/src/antex/record.rs +++ b/rinex/src/antex/record.rs @@ -1,9 +1,17 @@ -use super::{Antenna, Calibration, CalibrationMethod, Frequency, Pattern}; -use crate::{carrier, merge, merge::Merge, Epoch}; use gnss::prelude::SV; +use std::collections::HashMap; use std::str::FromStr; use thiserror::Error; +use super::{ + antenna::SvAntennaParsingError, Antenna, AntennaSpecific, Calibration, CalibrationMethod, + Cospar, RxAntenna, SvAntenna, +}; +use crate::{carrier, linspace::Linspace, merge, merge::Merge, Carrier, Epoch}; + +#[cfg(feature = "serde")] +use serde::Serialize; + /// Returns true if this line matches /// the beginning of a `epoch` for ATX file (special files), /// this is not really an epoch but rather a group of dataset @@ -12,6 +20,34 @@ pub(crate) fn is_new_epoch(content: &str) -> bool { content.contains("START OF ANTENNA") } +/// Phase pattern description. +/// We currently do not support azimuth dependent phase patterns. +#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize))] +pub enum AntennaPhasePattern { + /// Azimuth Independent Phase pattern + AzimuthIndependentPattern(Vec), +} + +impl Default for AntennaPhasePattern { + fn default() -> Self { + Self::AzimuthIndependentPattern(Vec::::new()) + } +} + +#[derive(Debug, Default, Clone, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize))] +pub struct FrequencyDependentData { + /// Eccentricities of the mean APC as NEU coordinates in millimeters. + /// The offset position is either relative to + /// Antenna Reference point (ARP), if this is an [`RxAntenna`], + /// or the Spacecraft Mass Center, if this is an [`SvAntenna`]. + pub apc_eccentricity: (f64, f64, f64), + /// Antenna Phase Pattern. + /// We currently do not support Azimuth Dependent phase patterns. + pub phase_pattern: AntennaPhasePattern, +} + /// ANTEX RINEX record content. /// Data is a list of Antenna containing several [Frequency] items. /// We do not parse RMS frequencies at the moment, but it will @@ -52,128 +88,301 @@ pub(crate) fn is_new_epoch(content: &str) -> bool { /// } /// ``` */ -pub type Record = Vec<(Antenna, Vec)>; +pub type Record = Vec<(Antenna, HashMap)>; #[derive(Debug, Error)] pub enum Error { #[error("Unknown PCV \"{0}\"")] UnknownPcv(String), - #[error("Failed to parse carrier frequency")] - ParseCarrierError(#[from] carrier::Error), - #[error("sv parsing error")] + #[error("failed to determine the frequency")] SvParsing(#[from] gnss::sv::ParsingError), + #[error("failed to determine the frequency")] + ParseCarrierError(#[from] carrier::Error), + #[error("sv antenna parsing error")] + SvAntennaParsing(#[from] SvAntennaParsingError), + #[error("failed to parse APC/NEU northern coordinates")] + APCNorthernCoordinatesParsing, + #[error("failed to parse APC/NEU eastern coordinates")] + APCEasternCoordinatesParsing, + #[error("failed to parse APC/NEU upper coordinates")] + APCUpperCoordinatesParsing, + #[error("failed to identify number of calibrated antennas")] + NumberOfCalibratedAntennasParsing, + #[error("failed to parse start of validity period")] + StartOfValidityPeriodParsing, + #[error("failed to parse end of validity period")] + EndOfValidityPeriodParsing, + #[error("calibration period missing year field")] + DatetimeParsingMissingYear, + #[error("calibration period missing month field")] + DatetimeParsingMissingMonth, + #[error("calibration period missing day field")] + DatetimeParsingMissingDay, + #[error("calibration period missing hours field")] + DatetimeParsingMissingHours, + #[error("calibration period missing minutes field")] + DatetimeParsingMissingMinutes, + #[error("calibration period missing seconds field")] + DatetimeParsingMissingSeconds, + #[error("failed to parse year of this calibration")] + DatetimeYearParsing, + #[error("failed to parse month of this calibration")] + DatetimeMonthParsing, + #[error("failed to parse day of this calibration")] + DatetimeDayParsing, + #[error("failed to parse hours of this calibration")] + DatetimeHoursParsing, + #[error("failed to parse minutes of this calibration")] + DatetimeMinutesParsing, + #[error("failed to parse seconds of this calibration")] + DatetimeSecondsParsing, + #[error("failed to parse nanos of this calibration")] + DatetimeNanosParsing, + #[error("failed to parse start of zenith grid")] + ZenithGridStartParsing, + #[error("failed to parse end of zenith grid")] + ZenithGridEndParsing, + #[error("failed to parse spacing of zenith grid")] + ZenithGridSpacingParsing, +} + +fn parse_datetime(content: &str) -> Result { + let mut parser = content.split('-'); + + let year = parser.next().ok_or(Error::DatetimeYearParsing)?; + let year = year + .parse::() + .map_err(|_| Error::DatetimeYearParsing)?; + + let month = parser.next().ok_or(Error::DatetimeMonthParsing)?; + + let month = match month { + "JAN" | "Jan" => 1, + "FEB" | "Feb" => 2, + "MAR" | "Mar" => 3, + "APR" | "Apr" => 4, + "MAY" | "May" => 5, + "JUN" | "Jun" => 6, + "JUL" | "Jul" => 7, + "AUG" | "Aug" => 8, + "SEP" | "Sep" => 9, + "OCT" | "Oct" => 10, + "NOV" | "Nov" => 11, + "DEC" | "Dec" => 12, + _ => { + return Err(Error::DatetimeMonthParsing); + }, + }; + + let day = parser.next().ok_or(Error::DatetimeDayParsing)?; + let day = day.parse::().map_err(|_| Error::DatetimeDayParsing)?; + + Ok(Epoch::from_gregorian_utc_at_midnight( + 2000 + year, + month, + day, + )) +} + +/* + * Parses the calibration validity FROM/UNTIL field + */ +fn parse_validity_epoch(content: &str) -> Result { + let mut items = content.split_ascii_whitespace(); + + let year = items.next().ok_or(Error::DatetimeParsingMissingYear)?; + let year = year + .parse::() + .map_err(|_| Error::DatetimeYearParsing)?; + + let month = items.next().ok_or(Error::DatetimeParsingMissingMonth)?; + let month = month + .parse::() + .map_err(|_| Error::DatetimeMonthParsing)?; + + let day = items.next().ok_or(Error::DatetimeParsingMissingDay)?; + let day = day.parse::().map_err(|_| Error::DatetimeDayParsing)?; + + let hh = items.next().ok_or(Error::DatetimeParsingMissingHours)?; + let hh = hh.parse::().map_err(|_| Error::DatetimeHoursParsing)?; + + let mm = items.next().ok_or(Error::DatetimeParsingMissingMinutes)?; + let mm = mm + .parse::() + .map_err(|_| Error::DatetimeMinutesParsing)?; + + let ss = items.next().ok_or(Error::DatetimeParsingMissingSeconds)?; + + let secs: u8; + let mut nanos = 0_u32; + + if let Some(dot) = ss.find('.') { + secs = ss[..dot] + .trim() + .parse::() + .map_err(|_| Error::DatetimeSecondsParsing)?; + + nanos = ss[dot + 1..] + .trim() + .parse::() + .map_err(|_| Error::DatetimeNanosParsing)?; + } else { + secs = ss + .parse::() + .map_err(|_| Error::DatetimeSecondsParsing)?; + } + + Ok(Epoch::from_gregorian_utc( + year, month, day, hh, mm, secs, nanos, + )) } /// Parses entire Antenna block /// and all inner frequency entries -pub(crate) fn parse_epoch(content: &str) -> Result<(Antenna, Vec), Error> { +pub(crate) fn parse_antenna( + content: &str, +) -> Result<(Antenna, HashMap), Error> { let lines = content.lines(); let mut antenna = Antenna::default(); - let mut frequency = Frequency::default(); - let mut frequencies: Vec = Vec::new(); + let mut inner = HashMap::::new(); + let mut frequency = Carrier::default(); + let mut freq_data = FrequencyDependentData::default(); + let mut valid_from = Epoch::default(); + for line in lines { let (content, marker) = line.split_at(60); - if marker.contains("START OF ANTENNA") { - antenna = Antenna::default(); // pointless - // because we're parsing a single START OF antenna block - // but it helps the else {} condition - // at the very bottom, where we consider to be - // in the Frequency payload - } else if marker.contains("# OF FREQUENCIES") { - continue; // we don't care about this information, - // because it can be retrieved with - // an record.antenna.len() ;) - } else if marker.contains("END OF ANTENNA") { - break; // end of this block, considered as an `epoch` - // if we make a parallel with other types of RINEX - } else if marker.contains("TYPE / SERIAL NO") { - let (ant_type, rem) = content.split_at(17); - let (sn, _) = rem.split_at(20); - antenna = antenna.with_type(ant_type.trim()); - antenna = antenna.with_serial_num(sn.trim()) + if marker.contains("TYPE / SERIAL NO") { + let (ant_igs, rem) = content.split_at(16); // IGS V.1.4 does not follow the specs ? + let (block1, rem) = rem.split_at(20 + 4); + let (block2, rem) = rem.split_at(10); + let (block3, _rem) = rem.split_at(10); + + let (block1, block2, block3) = (block1.trim(), block2.trim(), block3.trim()); + /* + * SV/RX antenna determination + */ + let specificities = match block2.is_empty() && block3.is_empty() { + false => AntennaSpecific::SvAntenna(SvAntenna { + igs_type: ant_igs.trim().to_string(), + sv: SV::from_str(block1)?, + cospar: Cospar::from_str(block3)?, + }), + true => AntennaSpecific::RxAntenna(RxAntenna { + igs_type: ant_igs.trim().to_string(), + serial_number: { + if !block1.is_empty() && !block1.eq("NONE") { + Some(block1.to_string()) + } else { + None + } + }, + }), + }; + antenna = antenna.with_specificities(specificities); } else if marker.contains("METH / BY / # / DATE") { let (method, rem) = content.split_at(20); let (agency, rem) = rem.split_at(20); - let (_, rem) = rem.split_at(10); // N# + let (number, rem) = rem.split_at(10); // N# let (date, _) = rem.split_at(10); + let cal = Calibration { method: CalibrationMethod::from_str(method.trim()).unwrap(), + number: number + .trim() + .parse::() + .map_err(|_| Error::NumberOfCalibratedAntennasParsing)?, agency: agency.trim().to_string(), - date: date.trim().to_string(), + date: parse_datetime(date.trim())?, + validity_period: None, }; - antenna = antenna.with_calibration(cal) - } else if marker.contains("DAZI") { - let dazi = content.split_at(20).0.trim(); - if let Ok(dazi) = f64::from_str(dazi) { - antenna = antenna.with_dazi(dazi) - } - } else if marker.contains("ZEN1 / ZEN2 / DZEN") { - let (zen1, rem) = content.split_at(8); - let (zen2, rem) = rem.split_at(6); - let (dzen, _) = rem.split_at(6); - if let Ok(zen1) = f64::from_str(zen1.trim()) { - if let Ok(zen2) = f64::from_str(zen2.trim()) { - if let Ok(dzen) = f64::from_str(dzen.trim()) { - antenna = antenna.with_zenith(zen1, zen2, dzen) - } - } - } + + antenna.calibration = cal.clone(); } else if marker.contains("VALID FROM") { - if let Ok(epoch) = Epoch::from_str(content.trim()) { - antenna = antenna.with_valid_from(epoch) - } + valid_from = parse_validity_epoch(content.trim())?; } else if marker.contains("VALID UNTIL") { - if let Ok(epoch) = Epoch::from_str(content.trim()) { - antenna = antenna.with_valid_until(epoch) - } + let valid_until = parse_validity_epoch(content.trim())?; + + antenna = antenna.with_validity_period(valid_from, valid_until); } else if marker.contains("SINEX CODE") { - let sinex = content.split_at(10).0; - antenna = antenna.with_sinex_code(sinex.trim()) + let sinex = content.split_at(20).0; + antenna.sinex_code = sinex.trim().to_string(); + } else if marker.contains("DAZI") { + //let dazi = content.split_at(20).0.trim(); + //if let Ok(dazi) = f64::from_str(dazi) { + // antenna = antenna.with_dazi(dazi) + //} + } else if marker.contains("# OF FREQUENCIES") { + /* + * we actually do not care about this field + * it is easy to determine it from the current infrastructure + */ } else if marker.contains("START OF FREQUENCY") { let svnn = content.split_at(10).0; - let carrier = carrier::Carrier::from_sv(SV::from_str(svnn.trim())?)?; - frequency = Frequency::default().with_carrier(carrier); + let sv = SV::from_str(svnn.trim())?; + frequency = carrier::Carrier::from_sv(sv)?; } else if marker.contains("NORTH / EAST / UP") { let (north, rem) = content.split_at(10); let (east, rem) = rem.split_at(10); let (up, _) = rem.split_at(10); - if let Ok(north) = f64::from_str(north.trim()) { - if let Ok(east) = f64::from_str(east.trim()) { - if let Ok(up) = f64::from_str(up.trim()) { - frequency = frequency - .with_northern_eccentricity(north) - .with_eastern_eccentricity(east) - .with_upper_eccentricity(up) - } - } - } + let north = north + .trim() + .parse::() + .map_err(|_| Error::APCNorthernCoordinatesParsing)?; + let east = east + .trim() + .parse::() + .map_err(|_| Error::APCEasternCoordinatesParsing)?; + let up = up + .trim() + .parse::() + .map_err(|_| Error::APCUpperCoordinatesParsing)?; + + freq_data.apc_eccentricity = (north, east, up); + } else if marker.contains("ZEN1 / ZEN2 / DZEN") { + let (start, rem) = content.split_at(8); + let (end, rem) = rem.split_at(6); + let (spacing, _) = rem.split_at(6); + + let start = start + .trim() + .parse::() + .map_err(|_| Error::ZenithGridStartParsing)?; + let end = end + .trim() + .parse::() + .map_err(|_| Error::ZenithGridEndParsing)?; + let spacing = spacing + .trim() + .parse::() + .map_err(|_| Error::ZenithGridSpacingParsing)?; + + antenna.zenith_grid = Linspace { + start, + end, + spacing, + }; } else if marker.contains("END OF FREQUENCY") { - frequencies.push(frequency.clone()) + inner.insert(frequency, freq_data.clone()); + } else if marker.contains("END OF ANTENNA") { + break; // end of this block, considered as an `epoch` + // if we make a parallel with other types of RINEX } else { - // Inside frequency - // Determine type of pattern - let (content, rem) = line.split_at(8); - let values: Vec = rem - .split_ascii_whitespace() - .map(|item| { - if let Ok(f) = f64::from_str(item.trim()) { - f - } else { - panic!("failed to \"{}\" \"{}\"", content, marker); - } - }) - .collect(); - if line.contains("NOAZI") { - frequency = frequency.add_pattern(Pattern::NonAzimuthDependent(values.clone())) - } else { - let angle = f64::from_str(content.trim()).unwrap(); - frequency = - frequency.add_pattern(Pattern::AzimuthDependent((angle, values.clone()))) - } + // inside phase pattern } + // } else if marker.contains("SINEX CODE") { + // let sinex = content.split_at(10).0; + // antenna = antenna.with_sinex_code(sinex.trim()) + // if line.contains("NOAZI") { + // frequency = frequency.add_pattern(Pattern::NonAzimuthDependent(values.clone())) + // } else { + // let angle = f64::from_str(content.trim()).unwrap(); + // frequency = + // frequency.add_pattern(Pattern::AzimuthDependent((angle, values.clone()))) + // } + // } } - Ok((antenna, frequencies)) + Ok((antenna, inner)) } #[cfg(test)] @@ -204,22 +413,32 @@ impl Merge for Record { } /// Merges `rhs` into `Self` fn merge_mut(&mut self, rhs: &Self) -> Result<(), merge::Error> { - for antenna in rhs.iter() { - if self.contains(antenna) { - let (antenna, frequencies) = antenna; - for (aantenna, ffrequencies) in self.iter_mut() { - if antenna == aantenna { - // for this antenna - // add missing frequencies - for frequency in frequencies { - if !ffrequencies.contains(frequency) { - ffrequencies.push(frequency.clone()); + for (antenna, subset) in rhs.iter() { + for (carrier, freqdata) in subset.iter() { + /* + * determine whether self contains this antenna & signal or not + */ + let mut has_ant = false; + let mut has_signal = false; + for (lhs_ant, subset) in self.iter_mut() { + if lhs_ant == antenna { + has_ant |= true; + for (lhs_carrier, _) in subset.iter_mut() { + if lhs_carrier == carrier { + has_signal |= true; + break; } } + if !has_signal { + subset.insert(carrier.clone(), freqdata.clone()); + } } } - } else { - self.push(antenna.clone()); + if !has_ant { + let mut inner = HashMap::::new(); + inner.insert(carrier.clone(), freqdata.clone()); + self.push((antenna.clone(), inner)); + } } } Ok(()) diff --git a/rinex/src/carrier.rs b/rinex/src/carrier.rs index 2dd2e49d1..aca244f46 100644 --- a/rinex/src/carrier.rs +++ b/rinex/src/carrier.rs @@ -654,10 +654,11 @@ impl Carrier { }, } } - - /// Builds a Carrier Frequency from an SV 3 letter code descriptor, - /// mainly used in `ATX` RINEX for so called `frequency` field - pub fn from_sv(sv: SV) -> Result { + /* + * Build a frequency from standard SV description. + * This is used in ATX records to identify the antenna frequency + */ + pub(crate) fn from_sv(sv: SV) -> Result { match sv.constellation { Constellation::GPS => match sv.prn { 1 => Ok(Self::L1), diff --git a/rinex/src/context/mod.rs b/rinex/src/context/mod.rs index 551c19cb7..33452ef70 100644 --- a/rinex/src/context/mod.rs +++ b/rinex/src/context/mod.rs @@ -100,7 +100,7 @@ impl RnxContext { */ fn from_file(path: PathBuf) -> Result { let mut ctx = Self::default(); - ctx.load(&path.to_string_lossy().to_string())?; + ctx.load(path.to_string_lossy().as_ref())?; Ok(ctx) } /* @@ -108,7 +108,7 @@ impl RnxContext { */ fn from_directory(path: PathBuf) -> Result { let mut ret = RnxContext::default(); - let walkdir = WalkDir::new(&path.to_string_lossy().to_string()).max_depth(5); + let walkdir = WalkDir::new(path.to_string_lossy().to_string()).max_depth(5); for entry in walkdir.into_iter().filter_map(|e| e.ok()) { if !entry.path().is_dir() { let fullpath = entry.path().to_string_lossy().to_string(); @@ -160,15 +160,15 @@ impl RnxContext { /// 5. ATX Data if provided fn provided_rinex(&self) -> Option<&ProvidedData> { if let Some(data) = &self.obs { - Some(&data) + Some(data) } else if let Some(data) = &self.nav { - Some(&data) + Some(data) } else if let Some(data) = &self.meteo { - Some(&data) + Some(data) } else if let Some(data) = &self.ionex { - Some(&data) + Some(data) } else if let Some(data) = &self.atx { - Some(&data) + Some(data) } else { None } @@ -375,7 +375,7 @@ impl RnxContext { } fn load_obs(&mut self, path: &Path, rnx: &Rinex) -> Result<(), Error> { if let Some(obs) = &mut self.obs { - obs.data.merge_mut(&rnx)?; + obs.data.merge_mut(rnx)?; obs.paths.push(path.to_path_buf()); } else { self.obs = Some(ProvidedData { @@ -387,7 +387,7 @@ impl RnxContext { } fn load_nav(&mut self, path: &Path, rnx: &Rinex) -> Result<(), Error> { if let Some(nav) = &mut self.nav { - nav.data.merge_mut(&rnx)?; + nav.data.merge_mut(rnx)?; nav.paths.push(path.to_path_buf()); } else { self.nav = Some(ProvidedData { @@ -399,7 +399,7 @@ impl RnxContext { } fn load_meteo(&mut self, path: &Path, rnx: &Rinex) -> Result<(), Error> { if let Some(meteo) = &mut self.meteo { - meteo.data.merge_mut(&rnx)?; + meteo.data.merge_mut(rnx)?; meteo.paths.push(path.to_path_buf()); } else { self.meteo = Some(ProvidedData { @@ -411,7 +411,7 @@ impl RnxContext { } fn load_ionex(&mut self, path: &Path, rnx: &Rinex) -> Result<(), Error> { if let Some(ionex) = &mut self.ionex { - ionex.data.merge_mut(&rnx)?; + ionex.data.merge_mut(rnx)?; ionex.paths.push(path.to_path_buf()); } else { self.ionex = Some(ProvidedData { @@ -423,7 +423,7 @@ impl RnxContext { } fn load_antex(&mut self, path: &Path, rnx: &Rinex) -> Result<(), Error> { if let Some(atx) = &mut self.atx { - atx.data.merge_mut(&rnx)?; + atx.data.merge_mut(rnx)?; atx.paths.push(path.to_path_buf()); } else { self.atx = Some(ProvidedData { @@ -436,7 +436,7 @@ impl RnxContext { fn load_sp3(&mut self, path: &Path, sp3: &SP3) -> Result<(), Error> { if let Some(data) = &mut self.sp3 { /* extend existing context */ - data.data.merge_mut(&sp3)?; + data.data.merge_mut(sp3)?; data.paths.push(path.to_path_buf()); } else { self.sp3 = Some(ProvidedData { diff --git a/rinex/src/hatanaka/decompressor.rs b/rinex/src/hatanaka/decompressor.rs index 9c90ef530..3c5e82e10 100644 --- a/rinex/src/hatanaka/decompressor.rs +++ b/rinex/src/hatanaka/decompressor.rs @@ -109,6 +109,12 @@ fn format_epoch( Ok(result) } +impl Default for Decompressor { + fn default() -> Self { + Self::new() + } +} + impl Decompressor { /// Creates a new decompression structure pub fn new() -> Self { diff --git a/rinex/src/hatanaka/textdiff.rs b/rinex/src/hatanaka/textdiff.rs index 1c0f20b99..ef270bf2b 100644 --- a/rinex/src/hatanaka/textdiff.rs +++ b/rinex/src/hatanaka/textdiff.rs @@ -3,6 +3,12 @@ pub struct TextDiff { pub buffer: String, } +impl Default for TextDiff { + fn default() -> Self { + Self::new() + } +} + impl TextDiff { /// Creates a new `Text` differentiator. /// Text compression has no limitations diff --git a/rinex/src/header.rs b/rinex/src/header.rs index 1b500f664..a890aba4c 100644 --- a/rinex/src/header.rs +++ b/rinex/src/header.rs @@ -6,7 +6,11 @@ use crate::{ clocks::{ClockAnalysisAgency, ClockDataType}, ground_position::GroundPosition, hardware::{Antenna, Rcvr, SvAntenna}, - ionex, leap, meteo, observation, + ionex, leap, + linspace::Linspace, + meteo, + navigation::{IonMessage, KbModel}, + observation, observation::Crinex, reader::BufferedReader, types::Type, @@ -23,7 +27,7 @@ use thiserror::Error; use crate::{fmt_comment, fmt_rinex}; #[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; +use serde::Serialize; #[derive(Default, Clone, Debug, PartialEq, Eq, EnumString)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -99,7 +103,7 @@ pub struct PcvCompensation { /// Describes `RINEX` file header #[derive(Clone, Debug, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Header { /// revision for this `RINEX` pub version: Version, @@ -158,6 +162,10 @@ pub struct Header { /// attached to a specifid SV, only exists in ANTEX records #[cfg_attr(feature = "serde", serde(default))] pub sv_antenna: Option, + /// Possible Ionospheric Delay correction model. + /// Only exists in NAV V3 headers. In modern NAV, this + /// is regularly updated in the file's body. + pub ionod_correction: Option, /// Possible DCBs compensation information pub dcb_compensations: Vec, /// Possible PCVs compensation information @@ -218,7 +226,7 @@ pub enum ParsingError { #[error("failed to parse ionex grid {0} from \"{1}\"")] InvalidIonexGrid(String, String), #[error("invalid ionex grid definition")] - InvalidIonexGridDefinition(#[from] ionex::grid::Error), + InvalidIonexGridDefinition(#[from] linspace::Error), } fn parse_formatted_month(content: &str) -> Result { @@ -295,6 +303,7 @@ impl Header { let mut sampling_interval: Option = None; let mut ground_position: Option = None; let mut dcb_compensations: Vec = Vec::new(); + let mut ionod_correction = Option::::None; let mut pcv_compensations: Vec = Vec::new(); let mut scaling_count = 0_u16; // RINEX specific fields @@ -412,10 +421,10 @@ impl Header { pcv = pcv.with_relative_type(rel_type.trim()); } } - antex = antex.with_pcv(pcv); + antex = antex.with_pcv_type(pcv); } if !ref_sn.trim().is_empty() { - antex = antex.with_serial_number(ref_sn.trim()) + antex = antex.with_reference_antenna_sn(ref_sn.trim()); } } else if marker.contains("TYPE / SERIAL NO") { let items: Vec<&str> = content.split_ascii_whitespace().collect(); @@ -962,9 +971,32 @@ impl Header { //TODO //0.9011D+05 -0.6554D+05 -0.1311D+06 0.4588D+06 ION BETA } else if marker.contains("IONOSPHERIC CORR") { - // TODO - // GPSA 0.1025E-07 0.7451E-08 -0.5960E-07 -0.5960E-07 - // GPSB 0.1025E-07 0.7451E-08 -0.5960E-07 -0.5960E-07 + /* + * RINEX < 4 IONOSPHERIC Correction + * we still use the IonMessage (V4 compatible), + * the record will just contain a single model for the entire day course + */ + if let Ok(model) = IonMessage::from_rinex3_header(content) { + // The Klobuchar model needs two lines to be entirely described. + if let Some(kb_model) = model.as_klobuchar() { + let correction_type = content.split_at(5).0.trim(); + if correction_type.ends_with("B") { + let alpha = ionod_correction.unwrap().as_klobuchar().unwrap().alpha; + let (beta, region) = (kb_model.beta, kb_model.region); + ionod_correction = Some(IonMessage::KlobucharModel(KbModel { + alpha, + beta, + region, + })); + } else { + ionod_correction = Some(IonMessage::KlobucharModel(*kb_model)); + } + } else { + // The NequickG model fits on a single line. + // The BDGIM does not exist until RINEX4 + ionod_correction = Some(model); + } + } } else if marker.contains("TIME SYSTEM CORR") { // GPUT 0.2793967723E-08 0.000000000E+00 147456 1395 /* @@ -1047,14 +1079,14 @@ impl Header { let grid = match spacing == 0.0 { true => { // special case, 2D fixed altitude - ionex::GridLinspace { + Linspace { // avoid verifying the Linspace in this case start, end, spacing: 0.0, } }, - _ => ionex::GridLinspace::new(start, end, spacing)?, + _ => Linspace::new(start, end, spacing)?, }; ionex = ionex.with_altitude_grid(grid); @@ -1082,8 +1114,7 @@ impl Header { spacing )))?; - ionex = - ionex.with_latitude_grid(ionex::GridLinspace::new(start, end, spacing)?); + ionex = ionex.with_latitude_grid(Linspace::new(start, end, spacing)?); } else { return Err(grid_format_error!("LAT1 / LAT2 / DLAT", content)); } @@ -1108,8 +1139,7 @@ impl Header { spacing )))?; - ionex = - ionex.with_longitude_grid(ionex::GridLinspace::new(start, end, spacing)?); + ionex = ionex.with_longitude_grid(Linspace::new(start, end, spacing)?); } else { return Err(grid_format_error!("LON1 / LON2 / DLON", content)); } @@ -1139,6 +1169,7 @@ impl Header { glo_channels, leap, ground_position, + ionod_correction, dcb_compensations, pcv_compensations, wavelengths: None, @@ -1905,12 +1936,12 @@ impl Merge for Header { if let Some(rhs) = &rhs.antex { // ANTEX records can only be merged together // if they have the same type of inner phase data - let mut mixed_antex = lhs.pcv.is_relative() && !rhs.pcv.is_relative(); - mixed_antex |= !lhs.pcv.is_relative() && rhs.pcv.is_relative(); + let mut mixed_antex = lhs.pcv_type.is_relative() && !rhs.pcv_type.is_relative(); + mixed_antex |= !lhs.pcv_type.is_relative() && rhs.pcv_type.is_relative(); if mixed_antex { return Err(merge::Error::AntexAbsoluteRelativeMismatch); } - merge::merge_mut_option(&mut lhs.reference_sn, &rhs.reference_sn); + // merge::merge_mut_option(&mut lhs.reference_sn, &rhs.reference_sn); } } if let Some(lhs) = &mut self.clocks { diff --git a/rinex/src/ionex/grid.rs b/rinex/src/ionex/grid.rs index 2947ec2c8..87639f55a 100644 --- a/rinex/src/ionex/grid.rs +++ b/rinex/src/ionex/grid.rs @@ -1,73 +1,8 @@ -use std::ops::Rem; -use thiserror::Error; +use crate::linspace::Linspace; #[cfg(feature = "serde")] use serde::Serialize; -/// Grid definition Error -#[derive(Error, Debug)] -pub enum Error { - #[error("faulty grid definition: `start` and `end` must be multiples of each other")] - GridStartEndError, - #[error("faulty grid definition: `start` and `end` must be multiples of `spacing`")] - GridSpacingError, -} - -/// Grid linear space, -/// starting from `start` ranging to `end` (included) -/// with given spacing, defined in km. -#[derive(Debug, Clone, Default, PartialEq, PartialOrd)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct GridLinspace { - /// Grid start coordinates in decimal degrees - pub start: f64, - /// Grid end coordinates in decimal degrees - pub end: f64, - /// Grid spacing (increment value), in decimal degrees - pub spacing: f64, -} - -impl GridLinspace { - /// Builds a new Linspace definition - pub fn new(start: f64, end: f64, spacing: f64) -> Result { - let r = end.rem(start); - /* - * End / Start must be multiple of one another - */ - if r == 0.0 { - if end.rem(spacing) == 0.0 { - Ok(Self { - start, - end, - spacing, - }) - } else { - Err(Error::GridSpacingError) - } - } else { - Err(Error::GridStartEndError) - } - } - // Returns grid length, in terms of data points - pub fn length(&self) -> usize { - (self.end / self.spacing).floor() as usize - } - /// Returns true if self is a single point space - pub fn is_single_point(&self) -> bool { - (self.end == self.start) && self.spacing == 0.0 - } -} - -impl From<(f64, f64, f64)> for GridLinspace { - fn from(tuple: (f64, f64, f64)) -> Self { - Self { - start: tuple.0, - end: tuple.1, - spacing: tuple.2, - } - } -} - /// Reference Grid, /// defined in terms of Latitude, Longitude and Altitude. /// If 2D-TEC maps, static altitude is defined, ie.: @@ -76,11 +11,11 @@ impl From<(f64, f64, f64)> for GridLinspace { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Grid { /// Latitude - pub latitude: GridLinspace, + pub latitude: Linspace, /// Longitude - pub longitude: GridLinspace, + pub longitude: Linspace, /// Altitude - pub height: GridLinspace, + pub height: Linspace, } impl Grid { @@ -101,16 +36,16 @@ mod test { use super::*; #[test] fn test_grid() { - let default = GridLinspace::default(); + let default = Linspace::default(); assert_eq!( default, - GridLinspace { + Linspace { start: 0.0, end: 0.0, spacing: 0.0, } ); - let grid = GridLinspace::new(1.0, 10.0, 1.0).unwrap(); + let grid = Linspace::new(1.0, 10.0, 1.0).unwrap(); assert_eq!(grid.length(), 10); assert!(!grid.is_single_point()); } diff --git a/rinex/src/ionex/mod.rs b/rinex/src/ionex/mod.rs index 150369a46..c4a7ae738 100644 --- a/rinex/src/ionex/mod.rs +++ b/rinex/src/ionex/mod.rs @@ -8,7 +8,8 @@ pub mod record; pub use record::{Record, TECPlane, TEC}; pub mod grid; -pub use grid::{Grid, GridLinspace}; +use crate::linspace::Linspace; +pub use grid::Grid; pub mod system; pub use system::RefSystem; @@ -170,19 +171,19 @@ impl HeaderFields { s } /// Adds latitude grid definition - pub fn with_latitude_grid(&self, grid: GridLinspace) -> Self { + pub fn with_latitude_grid(&self, grid: Linspace) -> Self { let mut s = self.clone(); s.grid.latitude = grid; s } /// Adds longitude grid definition - pub fn with_longitude_grid(&self, grid: GridLinspace) -> Self { + pub fn with_longitude_grid(&self, grid: Linspace) -> Self { let mut s = self.clone(); s.grid.longitude = grid; s } /// Adds altitude grid definition - pub fn with_altitude_grid(&self, grid: GridLinspace) -> Self { + pub fn with_altitude_grid(&self, grid: Linspace) -> Self { let mut s = self.clone(); s.grid.height = grid; s diff --git a/rinex/src/ionex/record.rs b/rinex/src/ionex/record.rs index a1abeae75..147d3ad36 100644 --- a/rinex/src/ionex/record.rs +++ b/rinex/src/ionex/record.rs @@ -1,7 +1,5 @@ use crate::{merge, merge::Merge, prelude::*, split, split::Split}; -use super::grid; - use crate::epoch; use hifitime::Duration; use std::collections::{BTreeMap, HashMap}; @@ -67,7 +65,7 @@ pub enum Error { #[error("faulty epoch description")] EpochDescriptionError, #[error("bad grid definition")] - BadGridDefinition(#[from] grid::Error), + BadGridDefinition(#[from] crate::linspace::Error), #[error("failed to parse {0} coordinates from \"{1}\"")] CoordinatesParsing(String, String), #[error("failed to parse epoch")] diff --git a/rinex/src/lib.rs b/rinex/src/lib.rs index d0e0f342b..03544f6c2 100644 --- a/rinex/src/lib.rs +++ b/rinex/src/lib.rs @@ -26,6 +26,7 @@ pub mod version; mod bibliography; mod ground_position; mod leap; +mod linspace; mod observable; #[cfg(test)] @@ -52,7 +53,8 @@ use writer::BufferedWriter; use std::collections::{BTreeMap, HashMap}; use thiserror::Error; -use hifitime::Duration; +use antex::{Antenna, AntennaSpecific, FrequencyDependentData}; +use hifitime::{Duration, Unit}; use ionex::TECPlane; use observable::Observable; use observation::Crinex; @@ -60,6 +62,8 @@ use version::Version; /// Package to include all basic structures pub mod prelude { + #[cfg(feature = "antex")] + pub use crate::antex::AntennaMatcher; #[cfg(feature = "sp3")] pub use crate::context::RnxContext; pub use crate::epoch::EpochFlag; @@ -1613,7 +1617,7 @@ impl Rinex { } /// Returns Navigation Data interator (any type of message). /// NAV records may contain several different types of frames. - /// You should prefer narrowed down methods, like [ephemeris] or + /// You should prefer more precise methods, like [ephemeris] or /// [ionosphere_models] but those require the "nav" feature. /// ``` /// use rinex::prelude::*; @@ -1642,6 +1646,17 @@ impl Rinex { .flat_map(|record| record.iter()), ) } + /// ANTEX antennas specifications browsing + pub fn antennas( + &self, + ) -> Box)> + '_> { + Box::new( + self.record + .as_antex() + .into_iter() + .flat_map(|record| record.iter()), + ) + } } // #[cfg(feature = "obs")] @@ -2459,8 +2474,11 @@ impl Rinex { }), ) } - /// Returns [`IonMessage`] frames Iterator - pub fn ionosphere_models( + /// [`IonMessage`] (Ionospheric corrections) frames Iterator. + /// Prefer the [ionod_correction] method down below, to determine the + /// Ionospheric correction to apply at a given time and for a given system. + /// This will only return correction models in RINEX4, as they're regularly updated. + pub fn ionod_correction_models( &self, ) -> Box + '_> { Box::new(self.navigation().flat_map(|(e, frames)| { @@ -2487,7 +2505,7 @@ impl Rinex { /// ``` pub fn klobuchar_models(&self) -> Box + '_> { Box::new( - self.ionosphere_models() + self.ionod_correction_models() .filter_map(|(e, (_, sv, ion))| ion.as_klobuchar().map(|model| (*e, sv, *model))), ) } @@ -2503,7 +2521,7 @@ impl Rinex { /// ``` pub fn nequick_g_models(&self) -> Box + '_> { Box::new( - self.ionosphere_models() + self.ionod_correction_models() .filter_map(|(e, (_, _, ion))| ion.as_nequick_g().map(|model| (*e, *model))), ) } @@ -2518,12 +2536,19 @@ impl Rinex { /// ``` pub fn bdgim_models(&self) -> Box + '_> { Box::new( - self.ionosphere_models() + self.ionod_correction_models() .filter_map(|(e, (_, _, ion))| ion.as_bdgim().map(|model| (*e, *model))), ) } - /// Returns Ionospheric Delay Model (as meters of delay) to use - pub fn ionod_model( + /// Returns Ionospheric delay correction to apply at given Epoch + /// and given location on Earth. + /// The correction is expressed as meters of delay. + /// If Self is a RINEX3, it can only describe a correction for a 24H time frame. + /// If "t" is not close enough to T0 of this file, we will not propose its model. + /// The same correction will also apply for that entire day. + /// Only RINEX4 can truly represent regularly updated correction models. This method + /// will return the closest correction in time. + pub fn ionod_correction( &self, t: Epoch, sv_elevation: f64, @@ -2532,11 +2557,30 @@ impl Rinex { user_lon_ddeg: f64, carrier: Carrier, ) -> Option { - // grab nearest model ; in time - let (_, model) = self - .ionosphere_models() + // determine nearest in time + let nearest_model = self + .ionod_correction_models() .map(|(t, (_, sv, msg))| (t, (sv, msg))) - .min_by_key(|(t_i, _)| (t - **t_i).abs())?; + .min_by_key(|(t_i, _)| (t - **t_i).abs()); + + let (t, model) = match nearest_model { + Some((t, (model_sv, model))) => (*t, (model_sv, *model)), + None => { + // RINEX3 possible case: depicted in the header + let ionod_corr = self.header.ionod_correction?; + /* + * only valid for 24 hours, at publication time + */ + let t0 = self.first_epoch()?; + let dt = t - t0; + let total_seconds = dt.to_seconds(); + if total_seconds >= 0.0 && dt < 24 * Unit::Hour { + (t0, (SV::default(), ionod_corr)) + } else { + return None; + } + }, + }; let (model_sv, model) = model; @@ -2886,10 +2930,15 @@ impl Merge for Rinex { /// Merges `rhs` into `Self` in place fn merge_mut(&mut self, rhs: &Self) -> Result<(), merge::Error> { self.header.merge_mut(&rhs.header)?; - if self.epoch().count() == 0 { - // lhs is empty : overwrite - self.record = rhs.record.clone(); - } else if rhs.epoch().count() != 0 { + if !self.is_antex() { + if self.epoch().count() == 0 { + // lhs is empty : overwrite + self.record = rhs.record.clone(); + } else if rhs.epoch().count() != 0 { + // real merge + self.record.merge_mut(&rhs.record)?; + } + } else { // real merge self.record.merge_mut(&rhs.record)?; } @@ -3233,6 +3282,87 @@ impl Rinex { } } +/* + * ANTEX specific feature + */ +#[cfg(feature = "antex")] +#[cfg_attr(docrs, doc(cfg(feature = "antex")))] +impl Rinex { + /// Iterates over antenna specifications that are still valid + pub fn antex_valid_calibrations( + &self, + now: Epoch, + ) -> Box)> + '_> { + Box::new(self.antennas().filter_map(move |(ant, data)| { + if ant.is_valid(now) { + Some((ant, data)) + } else { + None + } + })) + } + /// Returns APC offset for given spacecraft, expressed in NEU coordinates [mm] for given + /// frequency. "now" is used to determine calibration validity (in time). + pub fn sv_antenna_apc_offset( + &self, + now: Epoch, + sv: SV, + freq: Carrier, + ) -> Option<(f64, f64, f64)> { + self.antex_valid_calibrations(now) + .filter_map(|(ant, freqdata)| match &ant.specific { + AntennaSpecific::SvAntenna(sv_ant) => { + if sv_ant.sv == sv { + freqdata + .get(&freq) + .map(|freqdata| freqdata.apc_eccentricity) + } else { + None + } + }, + _ => None, + }) + .reduce(|k, _| k) // we're expecting a single match here + } + /// Returns APC offset for given RX Antenna model (ground station model). + /// Model name is the IGS code, which has to match exactly but we're case insensitive. + /// The APC offset is expressed in NEU coordinates + /// [mm]. "now" is used to determine calibration validity (in time). + pub fn rx_antenna_apc_offset( + &self, + now: Epoch, + matcher: AntennaMatcher, + freq: Carrier, + ) -> Option<(f64, f64, f64)> { + let to_match = matcher.to_lowercase(); + self.antex_valid_calibrations(now) + .filter_map(|(ant, freqdata)| match &ant.specific { + AntennaSpecific::RxAntenna(rx_ant) => match &to_match { + AntennaMatcher::IGSCode(code) => { + if rx_ant.igs_type.to_lowercase().eq(code) { + freqdata + .get(&freq) + .map(|freqdata| freqdata.apc_eccentricity) + } else { + None + } + }, + AntennaMatcher::SerialNumber(sn) => { + if rx_ant.igs_type.to_lowercase().eq(sn) { + freqdata + .get(&freq) + .map(|freqdata| freqdata.apc_eccentricity) + } else { + None + } + }, + }, + _ => None, + }) + .reduce(|k, _| k) // we're expecting a single match here + } +} + #[cfg(test)] mod test { use super::*; diff --git a/rinex/src/linspace.rs b/rinex/src/linspace.rs new file mode 100644 index 000000000..b350debaf --- /dev/null +++ b/rinex/src/linspace.rs @@ -0,0 +1,65 @@ +use std::ops::Rem; +use thiserror::Error; + +/// Grid definition Error +#[derive(Error, Debug)] +pub enum Error { + #[error("faulty grid definition: `start` and `end` must be multiples of each other")] + GridStartEndError, + #[error("faulty grid definition: `start` and `end` must be multiples of `spacing`")] + GridSpacingError, +} + +/// Linear space as used in IONEX or Antenna grid definitions. +/// Linear space starting from `start` ranging to `end` (included). +#[derive(Debug, Clone, Default, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Linspace { + /// start coordinates or value + pub start: f64, + /// end coordinates or value + pub end: f64, + /// spacing (increment value) + pub spacing: f64, +} + +impl Linspace { + /// Builds a new Linear space + pub fn new(start: f64, end: f64, spacing: f64) -> Result { + let r = end.rem(start); + /* + * End / Start must be multiple of one another + */ + if r == 0.0 { + if end.rem(spacing) == 0.0 { + Ok(Self { + start, + end, + spacing, + }) + } else { + Err(Error::GridSpacingError) + } + } else { + Err(Error::GridStartEndError) + } + } + // Returns grid length, in terms of data points + pub fn length(&self) -> usize { + (self.end / self.spacing).floor() as usize + } + /// Returns true if self is a single point space + pub fn is_single_point(&self) -> bool { + (self.end == self.start) && self.spacing == 0.0 + } +} + +impl From<(f64, f64, f64)> for Linspace { + fn from(tuple: (f64, f64, f64)) -> Self { + Self { + start: tuple.0, + end: tuple.1, + spacing: tuple.2, + } + } +} diff --git a/rinex/src/navigation/ionmessage.rs b/rinex/src/navigation/ionmessage.rs index 4099feb07..b5e2d58f2 100644 --- a/rinex/src/navigation/ionmessage.rs +++ b/rinex/src/navigation/ionmessage.rs @@ -19,8 +19,14 @@ use std::f64::consts::PI; pub enum Error { #[error("ng model missing 1st line")] NgModelMissing1stLine, + #[error("failed to parse nequick-g parameter")] + NgValueError, #[error("kb model missing 1st line")] KbModelMissing1stLine, + #[error("failed to parse klobuchar alpha parameter")] + KbAlphaValueError, + #[error("failed to parse klobuchar beta parameter")] + KbBetaValueError, #[error("bd model missing 1st line")] BdModelMissing1stLine, #[error("ng model missing 2nd line")] @@ -35,8 +41,8 @@ pub enum Error { BdModelMissing3rdLine, #[error("missing data fields")] MissingData, - #[error("failed to parse float data")] - ParseFloatError(#[from] std::num::ParseFloatError), + #[error("failed to parse bgdim parameter")] + BdValueError, #[error("failed to parse epoch")] EpochParsingError(#[from] EpochParsingError), } @@ -45,9 +51,9 @@ pub enum Error { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize))] pub enum KbRegionCode { - /// Coefficients apply to wide area + /// Worlwide (GPS) Orbits. WideArea = 0, - /// Japan Area coefficients + /// QZSS Japanese special Orbital plan. JapanArea = 1, } @@ -120,16 +126,16 @@ impl KbModel { let (epoch, _) = parse_in_timescale(epoch.trim(), ts)?; let alpha = ( - f64::from_str(a0.trim()).unwrap_or(0.0_f64), - f64::from_str(a1.trim()).unwrap_or(0.0_f64), - f64::from_str(a2.trim()).unwrap_or(0.0_f64), - f64::from_str(a3.trim()).unwrap_or(0.0_f64), + f64::from_str(a0.trim()).map_err(|_| Error::KbAlphaValueError)?, + f64::from_str(a1.trim()).map_err(|_| Error::KbAlphaValueError)?, + f64::from_str(a2.trim()).map_err(|_| Error::KbAlphaValueError)?, + f64::from_str(a3.trim()).map_err(|_| Error::KbAlphaValueError)?, ); let beta = ( - f64::from_str(b0.trim()).unwrap_or(0.0_f64), - f64::from_str(b1.trim()).unwrap_or(0.0_f64), - f64::from_str(b2.trim()).unwrap_or(0.0_f64), - f64::from_str(b3.trim()).unwrap_or(0.0_f64), + f64::from_str(b0.trim()).map_err(|_| Error::KbBetaValueError)?, + f64::from_str(b1.trim()).map_err(|_| Error::KbBetaValueError)?, + f64::from_str(b2.trim()).map_err(|_| Error::KbBetaValueError)?, + f64::from_str(b3.trim()).map_err(|_| Error::KbBetaValueError)?, ); Ok(( @@ -253,11 +259,11 @@ impl NgModel { let (epoch, _) = parse_in_timescale(epoch.trim(), ts)?; let a = ( - f64::from_str(a0.trim())?, - f64::from_str(a1.trim())?, - f64::from_str(rem.trim())?, + f64::from_str(a0.trim()).map_err(|_| Error::NgValueError)?, + f64::from_str(a1.trim()).map_err(|_| Error::NgValueError)?, + f64::from_str(rem.trim()).map_err(|_| Error::NgValueError)?, ); - let f = f64::from_str(line.trim())?; + let f = f64::from_str(line.trim()).map_err(|_| Error::NgValueError)?; Ok(( epoch, Self { @@ -349,6 +355,68 @@ impl Default for IonMessage { } impl IonMessage { + /* Parses old (RINEX3) Ionospheric Correction as IonMessage. + * The IonMessage is shared by RINEX3 and newest revision + * to represent the Ionospheric correction model. + * It's just unique and applies to the entire daycourse in RINEX3. + * The API remains totally coherent. */ + pub(crate) fn from_rinex3_header(header: &str) -> Result { + let (corr_type, rem) = header.split_at(5); + match corr_type.trim() { + /* + * Models that only needs 3 fields + */ + "GAL" => { + let (a0, rem) = rem.split_at(12); + let (a1, rem) = rem.split_at(12); + let (a2, _) = rem.split_at(12); + let a0 = f64::from_str(a0.trim()).map_err(|_| Error::NgValueError)?; + let a1 = f64::from_str(a1.trim()).map_err(|_| Error::NgValueError)?; + let a2 = f64::from_str(a2.trim()).map_err(|_| Error::NgValueError)?; + Ok(Self::NequickGModel(NgModel { + a: (a0, a1, a2), + region: NgRegionFlags::default(), // RINEX3 not accurate enough + })) + }, + corr_type => { + /* + * Model has 4 fields + */ + let (a0, rem) = rem.split_at(12); + let (a1, rem) = rem.split_at(12); + let (a2, rem) = rem.split_at(12); + let (a3, _) = rem.split_at(12); + // World or QZSS special orbital plan + let region = match corr_type.contains("QZS") { + true => KbRegionCode::JapanArea, + false => KbRegionCode::WideArea, + }; + /* determine which field we're dealing with */ + if corr_type.ends_with("A") { + let a0 = f64::from_str(a0.trim()).map_err(|_| Error::KbAlphaValueError)?; + let a1 = f64::from_str(a1.trim()).map_err(|_| Error::KbAlphaValueError)?; + let a2 = f64::from_str(a2.trim()).map_err(|_| Error::KbAlphaValueError)?; + let a3 = f64::from_str(a3.trim()).map_err(|_| Error::KbAlphaValueError)?; + + Ok(Self::KlobucharModel(KbModel { + alpha: (a0, a1, a2, a3), + beta: (0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64), + region, + })) + } else { + let b0 = f64::from_str(a0.trim()).map_err(|_| Error::KbBetaValueError)?; + let b1 = f64::from_str(a1.trim()).map_err(|_| Error::KbBetaValueError)?; + let b2 = f64::from_str(a2.trim()).map_err(|_| Error::KbBetaValueError)?; + let b3 = f64::from_str(a3.trim()).map_err(|_| Error::KbBetaValueError)?; + Ok(Self::KlobucharModel(KbModel { + alpha: (0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64), + beta: (b0, b1, b2, b3), + region, + })) + } + }, + } + } // /* converts self to meters of delay */ // pub(crate) fn meters_delay( // &self, @@ -458,4 +526,89 @@ mod test { assert!(msg.as_nequick_g().is_none()); assert!(msg.as_bdgim().is_none()); } + #[test] + fn rinex3_kb_header_parsing() { + let kb = IonMessage::from_rinex3_header( + "GPSA 7.4506e-09 -1.4901e-08 -5.9605e-08 1.1921e-07 ", + ); + assert!(kb.is_ok(), "failed to parse GPSA iono correction header"); + let kb = kb.unwrap(); + assert_eq!( + kb, + IonMessage::KlobucharModel(KbModel { + alpha: (7.4506E-9, -1.4901E-8, -5.9605E-8, 1.1921E-7), + beta: (0.0, 0.0, 0.0, 0.0), + region: KbRegionCode::WideArea, + }) + ); + + let kb = IonMessage::from_rinex3_header( + "GPSB 9.0112e+04 -6.5536e+04 -1.3107e+05 4.5875e+05 ", + ); + assert!(kb.is_ok(), "failed to parse GPSB iono correction header"); + let kb = kb.unwrap(); + assert_eq!( + kb, + IonMessage::KlobucharModel(KbModel { + alpha: (0.0, 0.0, 0.0, 0.0), + beta: (9.0112E4, -6.5536E4, -1.3107E5, 4.5875E5), + region: KbRegionCode::WideArea, + }) + ); + + let kb = IonMessage::from_rinex3_header( + "BDSA 1.1176e-08 2.9802e-08 -4.1723e-07 6.5565e-07 ", + ); + assert!(kb.is_ok(), "failed to parse BDSA iono correction header"); + let kb = kb.unwrap(); + assert_eq!( + kb, + IonMessage::KlobucharModel(KbModel { + alpha: (1.1176E-8, 2.9802E-8, -4.1723E-7, 6.5565E-7), + beta: (0.0, 0.0, 0.0, 0.0), + region: KbRegionCode::WideArea, + }) + ); + + let kb = IonMessage::from_rinex3_header( + "BDSB 1.4131e+05 -5.2429e+05 1.6384e+06 -4.5875e+05 3 ", + ); + assert!(kb.is_ok(), "failed to parse BDSB iono correction header"); + let kb = kb.unwrap(); + assert_eq!( + kb, + IonMessage::KlobucharModel(KbModel { + alpha: (0.0, 0.0, 0.0, 0.0), + beta: (1.4131E5, -5.2429E5, 1.6384E6, -4.5875E5), + region: KbRegionCode::WideArea, + }) + ); + + /* + * Test japanese (QZSS) orbital plan + */ + let kb = IonMessage::from_rinex3_header( + "QZSA 7.4506e-09 -1.4901e-08 -5.9605e-08 1.1921e-07 ", + ); + assert!(kb.is_ok(), "failed to parse QZSA iono correction header"); + let kb = kb.unwrap(); + let kb = kb.as_klobuchar().unwrap(); + assert_eq!( + kb.region, + KbRegionCode::JapanArea, + "QZSA ionospheric corr badly interprated as worldwide correction" + ); + + let kb = IonMessage::from_rinex3_header( + "QZSB 9.0112e+04 -6.5536e+04 -1.3107e+05 4.5875e+05 ", + ); + assert!(kb.is_ok(), "failed to parse QZSB iono correction header"); + let kb = kb.unwrap(); + let kb = kb.as_klobuchar().unwrap(); + assert_eq!( + kb.region, + KbRegionCode::JapanArea, + "QZSB ionospheric corr badly interprated as worldwide correction" + ); + } } diff --git a/rinex/src/observable.rs b/rinex/src/observable.rs index 13890a6e7..8a64727be 100644 --- a/rinex/src/observable.rs +++ b/rinex/src/observable.rs @@ -23,6 +23,10 @@ pub enum Observable { SSI(String), /// Pseudo range observation PseudoRange(String), + /// Channel number Pseudo Observable. + /// Attached to Pahse or PseudoRange observable to accurately + /// described how they were sampled. + ChannelNumber(String), /// Pressure observation in hPa Pressure, /// Dry temperature measurement in Celcius degrees @@ -65,6 +69,9 @@ impl Observable { pub fn is_ssi_observable(&self) -> bool { matches!(self, Self::SSI(_)) } + pub fn is_channel_number(&self) -> bool { + matches!(self, Self::ChannelNumber(_)) + } pub fn code(&self) -> Option { match self { Self::Phase(c) | Self::Doppler(c) | Self::SSI(c) | Self::PseudoRange(c) => { @@ -279,9 +286,11 @@ impl std::fmt::Display for Observable { Self::WindSpeed => write!(f, "WS"), Self::RainIncrement => write!(f, "RI"), Self::HailIndicator => write!(f, "HI"), - Self::SSI(c) | Self::Phase(c) | Self::Doppler(c) | Self::PseudoRange(c) => { - write!(f, "{}", c) - }, + Self::PseudoRange(c) => write!(f, "{}", c), + Self::Phase(c) => write!(f, "{}", c), + Self::Doppler(c) => write!(f, "{}", c), + Self::SSI(c) => write!(f, "{}", c), + Self::ChannelNumber(x) => write!(f, "{}", x), } } } diff --git a/rinex/src/observation/record.rs b/rinex/src/observation/record.rs index 51b650bb7..d97d8b420 100644 --- a/rinex/src/observation/record.rs +++ b/rinex/src/observation/record.rs @@ -91,7 +91,7 @@ impl ObservationData { /// + SNR must match the .is_ok() criteria, refer to API pub fn is_ok(self) -> bool { let lli_ok = self.lli.unwrap_or(LliFlags::OK_OR_UNKNOWN) == LliFlags::OK_OR_UNKNOWN; - let snr_ok = self.snr.unwrap_or(SNR::default()).strong(); + let snr_ok = self.snr.unwrap_or_default().strong(); lli_ok && snr_ok } @@ -1347,7 +1347,6 @@ fn dual_freq_combination( rec: &Record, combination: Combination, ) -> HashMap<(Observable, Observable), BTreeMap>> { - const SPEED_OF_LIGHT: f64 = 2.99792458E8; let mut ret: HashMap< (Observable, Observable), BTreeMap>, @@ -1363,7 +1362,7 @@ fn dual_freq_combination( // consider anything but L1 let lhs_code = lhs_observable.to_string(); - let lhs_is_l1 = lhs_code.contains("1"); + let lhs_is_l1 = lhs_code.contains('1'); if lhs_is_l1 { continue; } @@ -1380,7 +1379,7 @@ fn dual_freq_combination( } let refcode = ref_observable.to_string(); - if refcode.contains("1") { + if refcode.contains('1') { reference = Some((ref_observable.clone(), ref_data.obs)); break; // DONE searching } @@ -1392,7 +1391,7 @@ fn dual_freq_combination( let (ref_observable, ref_data) = reference.unwrap(); // determine frequencies - let lhs_carrier = Carrier::from_observable(sv.constellation, &lhs_observable); + let lhs_carrier = Carrier::from_observable(sv.constellation, lhs_observable); let ref_carrier = Carrier::from_observable(sv.constellation, &ref_observable); if lhs_carrier.is_err() | ref_carrier.is_err() { continue; // undetermined frequency @@ -1412,34 +1411,15 @@ fn dual_freq_combination( let beta = match combination { Combination::GeometryFree => 1.0_f64, - Combination::IonosphereFree => { - if ref_observable.is_pseudorange_observable() { - fi.powi(2) - } else { - SPEED_OF_LIGHT * fi - } - }, - Combination::WideLane | Combination::NarrowLane => { - if ref_observable.is_pseudorange_observable() { - 1.0_f64 - } else { - SPEED_OF_LIGHT - } - }, + Combination::IonosphereFree => fi.powi(2), + Combination::WideLane | Combination::NarrowLane => fi, Combination::MelbourneWubbena => unreachable!("mw combination"), }; let gamma = match combination { Combination::GeometryFree => 1.0_f64, - Combination::IonosphereFree => { - if ref_observable.is_pseudorange_observable() { - fj.powi(2) / fi.powi(2) - } else { - fj / fi - } - }, - Combination::WideLane => 1.0_f64, - Combination::NarrowLane => 1.0_f64, + Combination::IonosphereFree => fj.powi(2), + Combination::WideLane | Combination::NarrowLane => fj, Combination::MelbourneWubbena => unreachable!("mw combination"), }; @@ -1461,13 +1441,13 @@ fn dual_freq_combination( }; let value = match combination { - Combination::NarrowLane => alpha * beta * (v_i + gamma * v_j), - _ => alpha * beta * (v_i - gamma * v_j), + Combination::NarrowLane => alpha * (beta * v_i + gamma * v_j), + _ => alpha * (beta * v_i - gamma * v_j), }; let combination = (lhs_observable.clone(), ref_observable.clone()); if let Some(data) = ret.get_mut(&combination) { - if let Some(data) = data.get_mut(&sv) { + if let Some(data) = data.get_mut(sv) { data.insert(*epoch, value); } else { let mut map: BTreeMap<(Epoch, EpochFlag), f64> = BTreeMap::new(); @@ -1503,14 +1483,14 @@ fn mw_combination( if lhs_obs.is_phase_observable() { if let Some(code_data) = code_narrow.get(&(lhs_code_obs, rhs_code_obs)) { phase_wide.retain(|sv, phase_data| { - if let Some(code_data) = code_data.get(&sv) { - phase_data.retain(|epoch, _| code_data.get(&epoch).is_some()); - phase_data.len() > 0 + if let Some(code_data) = code_data.get(sv) { + phase_data.retain(|epoch, _| code_data.get(epoch).is_some()); + !phase_data.is_empty() } else { false } }); - phase_wide.len() > 0 + !phase_wide.is_empty() } else { false } @@ -1527,9 +1507,9 @@ fn mw_combination( if let Some(code_data) = code_narrow.get(&(lhs_code_obs, rhs_code_obs)) { for (phase_sv, data) in phase_data { - if let Some(code_data) = code_data.get(&phase_sv) { + if let Some(code_data) = code_data.get(phase_sv) { for (epoch, phase_wide) in data { - if let Some(narrow_code) = code_data.get(&epoch) { + if let Some(narrow_code) = code_data.get(epoch) { *phase_wide -= narrow_code; } } @@ -1721,7 +1701,7 @@ pub(crate) fn code_multipath( let code = observable.to_string(); let carrier = &code[1..2].to_string(); - let code_is_l1 = code.contains("1"); + let code_is_l1 = code.contains('1'); let mut phase_i = Option::::None; let mut phase_j = Option::::None; @@ -1735,7 +1715,7 @@ pub(crate) fn code_multipath( let rhs_code = rhs_observable.to_string(); // identify carrier signal - let rhs_carrier = Carrier::from_observable(sv.constellation, &rhs_observable); + let rhs_carrier = Carrier::from_observable(sv.constellation, rhs_observable); if rhs_carrier.is_err() { continue; } @@ -1743,21 +1723,19 @@ pub(crate) fn code_multipath( let lambda = rhs_carrier.wavelength(); if code_is_l1 { - if rhs_code.contains("2") { - f_j = Some(rhs_carrier.frequency()); - phase_j = Some(rhs_data.obs * lambda); - } else if rhs_code.contains(carrier) { - f_i = Some(rhs_carrier.frequency()); - phase_i = Some(rhs_data.obs * lambda); - } - } else { - if rhs_code.contains("1") { + if rhs_code.contains('2') { f_j = Some(rhs_carrier.frequency()); phase_j = Some(rhs_data.obs * lambda); } else if rhs_code.contains(carrier) { f_i = Some(rhs_carrier.frequency()); phase_i = Some(rhs_data.obs * lambda); } + } else if rhs_code.contains('1') { + f_j = Some(rhs_carrier.frequency()); + phase_j = Some(rhs_data.obs * lambda); + } else if rhs_code.contains(carrier) { + f_i = Some(rhs_carrier.frequency()); + phase_i = Some(rhs_data.obs * lambda); } if phase_i.is_some() && phase_j.is_some() { @@ -1774,8 +1752,8 @@ pub(crate) fn code_multipath( let beta = 2.0 / (gamma - 1.0); let value = obsdata.obs - alpha * phase_i.unwrap() + beta * phase_j.unwrap(); - if let Some(data) = ret.get_mut(&observable) { - if let Some(data) = data.get_mut(&sv) { + if let Some(data) = ret.get_mut(observable) { + if let Some(data) = data.get_mut(sv) { data.insert(*epoch, value); } else { let mut map: BTreeMap<(Epoch, EpochFlag), f64> = BTreeMap::new(); diff --git a/rinex/src/record.rs b/rinex/src/record.rs index c7f52876a..aca80215f 100644 --- a/rinex/src/record.rs +++ b/rinex/src/record.rs @@ -454,23 +454,13 @@ pub fn parse_record( } }, Type::AntennaData => { - if let Ok((antenna, frequencies)) = - antex::record::parse_epoch(&epoch_content) - { - let mut found = false; - for (ant, freqz) in atx_rec.iter_mut() { - if *ant == antenna { - for f in frequencies.iter() { - freqz.push(f.clone()); - } - found = true; - break; - } - } - if !found { - atx_rec.push((antenna, frequencies)); - } - } + let (antenna, content) = + antex::record::parse_antenna(&epoch_content).unwrap(); + atx_rec.push((antenna, content)); + //if let Ok((antenna, content)) = antex::record::parse_antenna(&epoch_content) + //{ + // atx_rec.push((antenna, content)); + //} }, Type::IonosphereMaps => { if let Ok((epoch, altitude, plane)) = @@ -611,21 +601,11 @@ pub fn parse_record( } }, Type::AntennaData => { - if let Ok((antenna, frequencies)) = antex::record::parse_epoch(&epoch_content) { - let mut found = false; - for (ant, freqz) in atx_rec.iter_mut() { - if *ant == antenna { - for f in frequencies.iter() { - freqz.push(f.clone()); - } - found = true; - break; - } - } - if !found { - atx_rec.push((antenna, frequencies)); - } - } + //if let Ok((antenna, content)) = antex::record::parse_antenna(&epoch_content) { + // atx_rec.push((antenna, content)); + //} + let (antenna, content) = antex::record::parse_antenna(&epoch_content).unwrap(); + atx_rec.push((antenna, content)); }, } // new comments ? diff --git a/rinex/src/tests/antex.rs b/rinex/src/tests/antex.rs index ab0e6361c..11b0c36de 100644 --- a/rinex/src/tests/antex.rs +++ b/rinex/src/tests/antex.rs @@ -2,7 +2,11 @@ mod test { use crate::antex::pcv::Pcv; use crate::antex::CalibrationMethod; + use crate::carrier::Carrier; + use crate::linspace::Linspace; use crate::prelude::*; + use std::str::FromStr; + #[cfg(feature = "antex")] #[test] fn v1_trosar_25r4_leit_2020_09_23() { let test_resource = env!("CARGO_MANIFEST_DIR").to_owned() @@ -11,41 +15,117 @@ mod test { assert!(rinex.is_ok()); let rinex = rinex.unwrap(); assert!(rinex.is_antex()); - let header = rinex.header; + + let header = &rinex.header; assert_eq!(header.version.major, 1); assert_eq!(header.version.minor, 4); assert!(header.antex.is_some()); + let atx_header = header.antex.as_ref().unwrap(); - assert_eq!(atx_header.pcv, Pcv::Absolute); + assert_eq!(atx_header.pcv_type, Pcv::Absolute); + + /* + * record test + */ let record = rinex.record.as_antex(); assert!(record.is_some()); let record = record.unwrap(); - assert_eq!(record.len(), 1); // Only 1 antenna - let (antenna, frequencies) = record.first().unwrap(); - assert_eq!(antenna.ant_type, "TROSAR25.R4"); - assert_eq!(antenna.sn, "LEIT727259"); - let cal = &antenna.calibration; - assert_eq!(cal.method, CalibrationMethod::Chamber); - assert_eq!(cal.agency, "IGG, Univ. Bonn"); - assert_eq!(cal.date, "23-SEP-20"); - assert_eq!(antenna.dazi, 5.0); - assert_eq!(antenna.zen, (0.0, 90.0)); - assert_eq!(antenna.dzen, 5.0); - assert!(antenna.valid_from.is_none()); - assert!(antenna.valid_until.is_none()); - for freq in frequencies.iter() { - let first = freq.patterns.first(); - assert!(first.is_some()); - let first = first.unwrap(); - assert!(!first.is_azimuth_dependent()); - let mut angle = 0.0_f64; - for i in 1..freq.patterns.len() { - let p = &freq.patterns[i]; - assert!(p.is_azimuth_dependent()); - let (a, _) = p.azimuth_pattern().unwrap(); - assert_eq!(angle, a); - angle += antenna.dzen; + + assert_eq!(record.len(), 1); + let (antenna, freq_data) = record.first().unwrap(); + + assert_eq!(antenna.calibration.method, CalibrationMethod::Chamber); + assert_eq!(antenna.calibration.agency, "IGG, Univ. Bonn"); + assert_eq!(antenna.calibration.number, 1); + assert_eq!( + antenna.calibration.date, + Epoch::from_str("2023-09-20T00:00:00 UTC").unwrap() + ); + assert_eq!( + antenna.zenith_grid, + Linspace { + start: 0.0, + end: 90.0, + spacing: 5.0, } + ); + + // specs for 3 freqz + assert_eq!(freq_data.len(), 3); + + // L1 frequency + assert!( + freq_data.get(&Carrier::L1).is_some(), + "missing specs for L1 frequency" + ); + let l1_specs = freq_data.get(&Carrier::L1).unwrap(); + assert_eq!( + l1_specs.apc_eccentricity, + (-0.22, -0.01, 154.88), + "bad APC for L1 frequency" + ); + + // L5 frequency + assert!( + freq_data.get(&Carrier::L5).is_some(), + "missing specs for L5 frequency" + ); + let l5_specs = freq_data.get(&Carrier::L5).unwrap(); + assert_eq!( + l5_specs.apc_eccentricity, + (0.34, -0.62, 164.34), + "bad APC for L5 frequency" + ); + + // B2B frequency + assert!( + freq_data.get(&Carrier::B2B).is_some(), + "missing specs for B2B frequency" + ); + let b2b_specs = freq_data.get(&Carrier::B2B).unwrap(); + assert_eq!( + b2b_specs.apc_eccentricity, + (0.32, -0.63, 160.39), + "bad APC for B2B frequency" + ); + + /* + * crate feature: RX antenna location + */ + let fake_now = Epoch::from_gregorian_utc_at_midnight(2023, 01, 01); + let apc = rinex.rx_antenna_apc_offset( + fake_now, + AntennaMatcher::IGSCode("trosar25.r4".to_string()), + Carrier::L1, + ); + assert!( + apc.is_some(), + "failed to locate APC for TROSAR25.R4 antenna" + ); + assert_eq!(apc.unwrap(), (-0.22, -0.01, 154.88)); + } + #[cfg(feature = "flate2")] + #[cfg(feature = "antex")] + #[test] + fn v1_4_igs_atx() { + let test_resource = + env!("CARGO_MANIFEST_DIR").to_owned() + "/../test_resources/ATX/V1/igs14_small.atx.gz"; + + let rinex = Rinex::from_file(&test_resource).unwrap(); + + let fake_now = Epoch::from_gregorian_utc_at_midnight(2023, 01, 01); + + for (antenna, expected) in [ + ("JPSLEGANT_E", (1.36, -0.43, 35.44)), + ("JPSODYSSEY_I", (1.06, -2.43, 70.34)), + ] { + let apc = rinex.rx_antenna_apc_offset( + fake_now, + AntennaMatcher::IGSCode(antenna.to_string()), + Carrier::L1, + ); + assert!(apc.is_some(), "failed to locate APC {} antenna", antenna,); + assert_eq!(apc.unwrap(), expected); } } } diff --git a/rinex/src/tests/merge.rs b/rinex/src/tests/merge.rs index 592602eea..e39b63b3a 100644 --- a/rinex/src/tests/merge.rs +++ b/rinex/src/tests/merge.rs @@ -169,4 +169,44 @@ mod test { // remove file we just generated let _ = std::fs::remove_file("merge.txt"); } + #[cfg(feature = "antex")] + use crate::antex::antenna::AntennaMatcher; + #[cfg(feature = "antex")] + use crate::Carrier; + #[test] + #[cfg(feature = "flate2")] + #[cfg(feature = "antex")] + fn merge_atx() { + let fp = env!("CARGO_MANIFEST_DIR").to_owned() + + "/../test_resources/ATX/V1/TROSAR25.R4__LEIT_2020_09_23.atx"; + let rinex_a = Rinex::from_file(&fp); + let rinex_a = rinex_a.unwrap(); + + let fp = + env!("CARGO_MANIFEST_DIR").to_owned() + "/../test_resources/ATX/V1/igs14_small.atx.gz"; + let rinex_b = Rinex::from_file(&fp); + let rinex_b = rinex_b.unwrap(); + + let merged = rinex_a.merge(&rinex_b); + assert!(merged.is_ok(), "merged atx(a,b) failed"); + let merged = merged.unwrap(); + + let antennas: Vec<_> = merged.antennas().collect(); + assert_eq!(antennas.len(), 7, "bad number of antennas"); + + for (name, expected_apc) in [ + ("JPSLEGANT_E", (1.36, -0.43, 35.44)), + ("JPSODYSSEY_I", (1.06, -2.43, 70.34)), + ("TROSAR25.R4", (-0.22, -0.01, 154.88)), + ] { + let fakenow = Epoch::from_gregorian_utc_at_midnight(2023, 01, 01); + let apc = merged.rx_antenna_apc_offset( + fakenow, + AntennaMatcher::IGSCode(name.to_string()), + Carrier::L1, + ); + assert!(apc.is_some(), "APC should still be contained after merge()"); + assert_eq!(apc.unwrap(), expected_apc); + } + } } diff --git a/rinex/src/tests/nav.rs b/rinex/src/tests/nav.rs index 74a8c95c6..5af09f53b 100644 --- a/rinex/src/tests/nav.rs +++ b/rinex/src/tests/nav.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod test { + use crate::carrier::Carrier; use crate::navigation::*; use crate::prelude::*; use gnss_rs::prelude::SV; @@ -1175,7 +1176,7 @@ mod test { } } } - for (epoch, (msg, sv, iondata)) in rinex.ionosphere_models() { + for (epoch, (msg, sv, iondata)) in rinex.ionod_correction_models() { if sv == sv!("G21") { assert_eq!(msg, NavMsgType::LNAV); if *epoch == Epoch::from_str("2023-03-12T00:08:54 UTC").unwrap() { @@ -1441,4 +1442,61 @@ mod test { } } } + #[test] + #[cfg(feature = "nav")] + fn v3_ionospheric_corr() { + let path = PathBuf::new() + .join(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("test_resources") + .join("NAV") + .join("V3") + .join("CBW100NLD_R_20210010000_01D_MN.rnx"); + let rinex = Rinex::from_file(&path.to_string_lossy()); + assert!( + rinex.is_ok(), + "failed to parse NAV/V3/BCW100NLD_R_2021, error: {:?}", + rinex.err() + ); + let rinex = rinex.unwrap(); + + for (t0, should_work) in [ + // MIDNIGHT T0 exact match + (Epoch::from_gregorian_utc_at_midnight(2021, 01, 01), true), + // VALID day course : 1sec into that day + (Epoch::from_gregorian_utc(2021, 01, 01, 00, 00, 1, 0), true), + // VALID day course : random into that dat + (Epoch::from_gregorian_utc(2021, 01, 01, 05, 33, 24, 0), true), + // VALID day course : 1 sec prior next day + (Epoch::from_str("2021-01-01T23:59:59 GPST").unwrap(), true), + // TOO LATE : MIDNIGHT DAY +1 + (Epoch::from_str("2021-01-02T00:00:00 GPST").unwrap(), false), + // TOO LATE : MIDNIGHT DAY +1 + (Epoch::from_gregorian_utc_at_midnight(2021, 02, 01), false), + // TOO EARLY + (Epoch::from_gregorian_utc_at_midnight(2020, 12, 31), false), + ] { + let ionod_corr = rinex.ionod_correction( + t0, + 30.0, // fake elev: DONT CARE + 30.0, // fake azim: DONT CARE + 10.0, // fake latitude: DONT CARE + 20.0, // fake longitude: DONT CARE + Carrier::default(), // fake signal: DONT CARE + ); + if should_work { + assert!( + ionod_corr.is_some(), + "v3 ionod corr: should have returned a correction model for datetime {:?}", + t0 + ); + } else { + assert!( + ionod_corr.is_none(), + "v3 ionod corr: should not have returned a correction model for datetime {:?}", + t0 + ); + } + } + } } diff --git a/rinex/src/tests/parsing.rs b/rinex/src/tests/parsing.rs index 8d390301a..df12a4093 100644 --- a/rinex/src/tests/parsing.rs +++ b/rinex/src/tests/parsing.rs @@ -116,7 +116,7 @@ mod test { /* * Verify ION logical correctness */ - for (_, (msg, sv, ion_msg)) in rinex.ionosphere_models() { + for (_, (msg, sv, ion_msg)) in rinex.ionod_correction_models() { match sv.constellation { Constellation::GPS => { assert!( diff --git a/rinex/src/types.rs b/rinex/src/types.rs index a7f4df7a9..01129f35f 100644 --- a/rinex/src/types.rs +++ b/rinex/src/types.rs @@ -76,7 +76,7 @@ impl std::str::FromStr for Type { } else if s.eq("ionosphere maps") { Ok(Self::IonosphereMaps) } else { - Err(ParsingError::TypeParsing(String::from(s))) + Err(ParsingError::TypeParsing(s)) } } } diff --git a/rnx2cggtts/Cargo.toml b/rnx2cggtts/Cargo.toml index 6bcfb7377..897cdc09b 100644 --- a/rnx2cggtts/Cargo.toml +++ b/rnx2cggtts/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rnx2cggtts" -version = "1.0.0" +version = "1.0.1" license = "MIT OR Apache-2.0" authors = ["Guillaume W. Bres "] description = "CGGTTS data generation from RINEX" @@ -23,14 +23,14 @@ env_logger = "0.10" gnss-rs = { version = "2.1.2" , features = ["serde"] } clap = { version = "4.4.10", features = ["derive", "color"] } serde = { version = "1.0", default-features = false, features = ["derive"] } -rinex = { path = "../rinex", version = "=0.15.1", features = ["full"] } +rinex = { path = "../rinex", version = "=0.15.2", features = ["full"] } # cggtts -cggtts = { version = "4.1.0", features = ["serde", "scheduler"] } +cggtts = { version = "4.1.1", features = ["serde", "scheduler"] } # cggtts = { git = "https://github.com/gwbres/cggtts", branch = "develop", features = ["serde", "scheduler"] } # cggtts = { path = "../../cggtts/cggtts", features = ["serde", "scheduler"] } # solver -gnss-rtk = { version = "0.4.0", features = ["serde"] } +gnss-rtk = { version = "0.4.1", features = ["serde"] } # gnss-rtk = { git = "https://github.com/rtk-rs/gnss-rtk", branch = "develop", features = ["serde"] } # gnss-rtk = { path = "../../rtk-rs/gnss-rtk", features = ["serde"] } diff --git a/rnx2cggtts/src/cli.rs b/rnx2cggtts/src/cli.rs index d858d2d7a..40f9cce57 100644 --- a/rnx2cggtts/src/cli.rs +++ b/rnx2cggtts/src/cli.rs @@ -10,7 +10,7 @@ pub struct Cli { } use cggtts::{prelude::ReferenceTime, track::Scheduler}; -use gnss_rtk::prelude::{Config, Mode as SolverMode}; +use gnss_rtk::prelude::Config; use rinex::prelude::*; impl Cli { @@ -202,55 +202,34 @@ Refer to rinex-cli Preprocessor documentation for more information")) fn get_flag(&self, flag: &str) -> bool { self.matches.get_flag(flag) } - /* returns RTK solver mode to implement */ - pub fn solver_mode(&self) -> SolverMode { - if self.matches.get_flag("spp") { - SolverMode::SPP - } else if self.matches.get_flag("ppp") { - SolverMode::PPP - } else { - SolverMode::LSQSPP - } - } - pub fn positioning(&self) -> bool { - self.matches.get_flag("spp") - || self.matches.get_flag("lsqspp") - || self.matches.get_flag("ppp") - } /// Returns the manualy defined RFDLY (in nanoseconds!) pub fn rf_delay(&self) -> Option> { - if let Some(delays) = self.matches.get_many::("rfdly") { - Some( - delays - .into_iter() - .filter_map(|string| { - let items: Vec<_> = string.split(':').collect(); - if items.len() < 2 { - error!("format error, command should be --rf-delay CODE:[nanos]"); - None - } else { - let code = items[0].trim(); - let nanos = items[0].trim(); - if let Ok(code) = Observable::from_str(code) { - if let Ok(f) = nanos.parse::() { - Some((code, f)) - } else { - error!("invalid nanos: expecting valid f64"); - None - } + self.matches.get_many::("rfdly").map(|delays| { + delays + .into_iter() + .filter_map(|string| { + let items: Vec<_> = string.split(':').collect(); + if items.len() < 2 { + error!("format error, command should be --rf-delay CODE:[nanos]"); + None + } else { + let code = items[0].trim(); + let nanos = items[0].trim(); + if let Ok(code) = Observable::from_str(code) { + if let Ok(f) = nanos.parse::() { + Some((code, f)) } else { - error!( - "invalid pseudo range CODE, expecting codes like \"L1C\",..." - ); + error!("invalid nanos: expecting valid f64"); None } + } else { + error!("invalid pseudo range CODE, expecting codes like \"L1C\",..."); + None } - }) - .collect(), - ) - } else { - None - } + } + }) + .collect() + }) } /// Returns the manualy defined REFDLY (in nanoseconds!) pub fn reference_time_delay(&self) -> Option { diff --git a/rnx2cggtts/src/main.rs b/rnx2cggtts/src/main.rs index fd9a1d6f5..76f80cdbc 100644 --- a/rnx2cggtts/src/main.rs +++ b/rnx2cggtts/src/main.rs @@ -63,7 +63,7 @@ pub fn workspace_path(ctx: &RnxContext) -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .join("..") .join("WORKSPACE") - .join(&context_stem(ctx)) + .join(context_stem(ctx)) } /* @@ -132,7 +132,7 @@ pub fn main() -> Result<(), Error> { // Workspace let workspace = match cli.custom_workspace() { - Some(workspace) => Path::new(workspace).join(&context_stem(&ctx)).to_path_buf(), + Some(workspace) => Path::new(workspace).join(context_stem(&ctx)).to_path_buf(), None => workspace_path(&ctx), }; create_workspace(workspace.clone()); @@ -253,7 +253,7 @@ pub fn main() -> Result<(), Error> { * Create file */ let filename = match cli.custom_filename() { - Some(filename) => workspace.join(filename.to_string()), + Some(filename) => workspace.join(filename), None => workspace.join(cggtts.filename()), }; diff --git a/rnx2cggtts/src/solver.rs b/rnx2cggtts/src/solver.rs index b36118df2..38717ec59 100644 --- a/rnx2cggtts/src/solver.rs +++ b/rnx2cggtts/src/solver.rs @@ -18,11 +18,10 @@ use rtk::prelude::{ Config, Duration, Epoch, - InterpolatedPosition, InterpolationResult, IonosphericBias, KbModel, - Mode, + Method, NgModel, Observation, PVTSolutionType, @@ -131,22 +130,31 @@ fn kb_model(nav: &Rinex, t: Epoch) -> Option { .klobuchar_models() .min_by_key(|(t_i, _, _)| (t - *t_i).abs()); - match kb_model { - Some((_, sv, kb_model)) => { + if let Some((_, sv, kb_model)) = kb_model { + Some(KbModel { + h_km: { + match sv.constellation { + Constellation::BeiDou => 375.0, + // we only expect GPS or BDS here, + // badly formed RINEX will generate errors in the solutions + _ => 350.0, + } + }, + alpha: kb_model.alpha, + beta: kb_model.beta, + }) + } else { + /* RINEX 3 case */ + let iono_corr = nav.header.ionod_correction?; + if let Some(kb_model) = iono_corr.as_klobuchar() { Some(KbModel { - h_km: { - match sv.constellation { - Constellation::BeiDou => 375.0, - // we only expect GPS or BDS here, - // badly formed RINEX will generate errors in the solutions - _ => 350.0, - } - }, + h_km: 350.0, //TODO improve this alpha: kb_model.alpha, beta: kb_model.beta, }) - }, - None => None, + } else { + None + } } } @@ -170,34 +178,35 @@ fn reset_sv_tracker(sv: SV, trackers: &mut HashMap<(SV, Observable), SVTracker>) } } -fn reset_sv_sig_tracker( - sv_sig: (SV, Observable), - trackers: &mut HashMap<(SV, Observable), SVTracker>, -) { - for (k, tracker) in trackers { - if k == &sv_sig { - tracker.reset(); - } - } -} +//TODO: see TODO down below +// fn reset_sv_sig_tracker( +// sv_sig: (SV, Observable), +// trackers: &mut HashMap<(SV, Observable), SVTracker>, +// ) { +// for (k, tracker) in trackers { +// if k == &sv_sig { +// tracker.reset(); +// } +// } +// } pub fn resolve(ctx: &mut RnxContext, cli: &Cli) -> Result, Error> { // custom tracking duration let trk_duration = cli.tracking_duration(); info!("tracking duration set to {}", trk_duration); - // custom strategy - let mode = cli.solver_mode(); - match mode { - Mode::SPP => info!("single point positioning"), - Mode::LSQSPP => info!("recursive lsq single point positioning"), - Mode::PPP => info!("precise point positioning"), - }; - // parse custom config, if any let cfg = match cli.config() { Some(cfg) => cfg, - None => Config::default(mode), + None => { + /* no manual config: we use the optimal known to this day */ + Config::preset(Method::SPP) + }, + }; + + match cfg.method { + Method::SPP => info!("single point positioning"), + Method::PPP => info!("precise point positioning"), }; let pos = match cli.manual_apc() { @@ -235,12 +244,12 @@ pub fn resolve(ctx: &mut RnxContext, cli: &Cli) -> Result, Error> { }; let sp3_data = ctx.sp3_data(); + let atx_data = ctx.atx_data(); let meteo_data = ctx.meteo_data(); let mut solver = Solver::new( - mode, - apriori, &cfg, + apriori, /* state vector interpolator */ |t, sv, order| { /* SP3 source is prefered */ @@ -249,48 +258,42 @@ pub fn resolve(ctx: &mut RnxContext, cli: &Cli) -> Result, Error> { let (x, y, z) = (x * 1.0E3, y * 1.0E3, z * 1.0E3); let (elevation, azimuth) = Ephemeris::elevation_azimuth((x, y, z), apriori_ecef); - Some(InterpolationResult { - azimuth, - elevation, - velocity: None, - position: InterpolatedPosition::MassCenter(Vector3::new(x, y, z)), - }) + Some( + InterpolationResult::from_mass_center_position((x, y, z)) + .with_elevation_azimuth((elevation, azimuth)), + ) } else { // debug!("{:?} ({}): sp3 interpolation failed", t, sv); if let Some((x, y, z)) = nav_data.sv_position_interpolate(sv, t, order) { let (x, y, z) = (x * 1.0E3, y * 1.0E3, z * 1.0E3); let (elevation, azimuth) = Ephemeris::elevation_azimuth((x, y, z), apriori_ecef); - Some(InterpolationResult { - azimuth, - elevation, - velocity: None, - position: InterpolatedPosition::AntennaPhaseCenter(Vector3::new( - x, y, z, - )), - }) + Some( + InterpolationResult::from_apc_position((x, y, z)) + .with_elevation_azimuth((elevation, azimuth)), + ) } else { // debug!("{:?} ({}): nav interpolation failed", t, sv); None } } + } else if let Some((x, y, z)) = nav_data.sv_position_interpolate(sv, t, order) { + let (x, y, z) = (x * 1.0E3, y * 1.0E3, z * 1.0E3); + let (elevation, azimuth) = Ephemeris::elevation_azimuth((x, y, z), apriori_ecef); + Some( + InterpolationResult::from_apc_position((x, y, z)) + .with_elevation_azimuth((elevation, azimuth)), + ) } else { - if let Some((x, y, z)) = nav_data.sv_position_interpolate(sv, t, order) { - let (x, y, z) = (x * 1.0E3, y * 1.0E3, z * 1.0E3); - let (elevation, azimuth) = - Ephemeris::elevation_azimuth((x, y, z), apriori_ecef); - Some(InterpolationResult { - azimuth, - elevation, - velocity: None, - position: InterpolatedPosition::AntennaPhaseCenter(Vector3::new(x, y, z)), - }) - } else { - // debug!("{:?} ({}): nav interpolation failed", t, sv); - None - } + // debug!("{:?} ({}): nav interpolation failed", t, sv); + None } }, + /* APC corrections provider */ + |_t, _sv, _freq| { + let _atx = atx_data?; + None + }, )?; // CGGTTS specifics @@ -354,25 +357,13 @@ pub fn resolve(ctx: &mut RnxContext, cli: &Cli) -> Result, Error> { if observable.is_pseudorange_observable() { code = Some(Observation { frequency, - snr: { - if let Some(snr) = data.snr { - Some(snr.into()) - } else { - None - } - }, + snr: { data.snr.map(|snr| snr.into()) }, value: data.obs, }); } else if observable.is_phase_observable() { phase = Some(Observation { frequency, - snr: { - if let Some(snr) = data.snr { - Some(snr.into()) - } else { - None - } - }, + snr: { data.snr.map(|snr| snr.into()) }, value: data.obs, }); } @@ -389,13 +380,7 @@ pub fn resolve(ctx: &mut RnxContext, cli: &Cli) -> Result, Error> { if observable.is_doppler_observable() && observable == &doppler_to_match { assoc_doppler = Some(Observation { frequency, - snr: { - if let Some(snr) = data.snr { - Some(snr.into()) - } else { - None - } - }, + snr: { data.snr.map(|snr| snr.into()) }, value: data.obs, }); } @@ -472,15 +457,9 @@ pub fn resolve(ctx: &mut RnxContext, cli: &Cli) -> Result, Error> { None => 0.0_f64, }; - let mdio = match pvt_data.iono_bias.modeled { - Some(iono) => Some(iono), - None => None, - }; + let mdio = pvt_data.iono_bias.modeled; - let msio = match pvt_data.iono_bias.measured { - Some(iono) => Some(iono), - None => None, - }; + let msio = pvt_data.iono_bias.measured; debug!( "{:?} : new {}:{} PVT solution (elev={:.2}°, azi={:.2}°, REFSV={:.3E}, REFSYS={:.3E})", @@ -536,7 +515,7 @@ pub fn resolve(ctx: &mut RnxContext, cli: &Cli) -> Result, Error> { dominant_sampling_period, trk_midpoint, ) { - Ok(((trk_elev, trk_azi), trk_data)) => { + Ok(((trk_elev, trk_azi), trk_data, _iono_data)) => { info!( "{:?} - new {} track: elev {:.2}° - azi {:.2}° - REFSV {:.3E} REFSYS {:.3E}", t, @@ -580,7 +559,10 @@ pub fn resolve(ctx: &mut RnxContext, cli: &Cli) -> Result, Error> { }; // match constellation tracks.push(track); }, - Err(e) => warn!("{:?} - track fitting error: \"{}\"", t, e), + Err(e) => { + warn!("{:?} - track fitting error: \"{}\"", t, e); + // TODO: most likely we should reset the SV signal tracker here + }, } //.fit() // reset so we start a new track diff --git a/rnx2crx/Cargo.toml b/rnx2crx/Cargo.toml index 488abc144..e747836a4 100644 --- a/rnx2crx/Cargo.toml +++ b/rnx2crx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rnx2crx" -version = "1.1.2" +version = "1.1.3" license = "MIT OR Apache-2.0" authors = ["Guillaume W. Bres "] description = "RINEX data compressor" @@ -15,4 +15,4 @@ readme = "README.md" chrono = "0.4" thiserror = "1" clap = { version = "4.4.10", features = ["derive", "color"] } -rinex = { path = "../rinex", version = "=0.15.1", features = ["serde"] } +rinex = { path = "../rinex", version = "=0.15.2", features = ["serde"] } diff --git a/sinex/Cargo.toml b/sinex/Cargo.toml index cbb429436..98500a8f7 100644 --- a/sinex/Cargo.toml +++ b/sinex/Cargo.toml @@ -21,4 +21,3 @@ thiserror = "1" strum = { version = "0.25", features = ["derive"] } strum_macros = "0.25" gnss-rs = { version = "2.1.2", features = ["serde"] } -# rinex = { path = "../rinex", version = "=0.15.1", features = ["serde"] } diff --git a/test_resources/ATX/V1/igs14_small.atx.gz b/test_resources/ATX/V1/igs14_small.atx.gz new file mode 100644 index 0000000000000000000000000000000000000000..abfb8814e7c2c1ec9649ca1eab11e68e312f83c0 GIT binary patch literal 18465 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 C(AXIO literal 0 HcmV?d00001 diff --git a/ublox-rnx/Cargo.toml b/ublox-rnx/Cargo.toml index 3662ae416..a334d7e14 100644 --- a/ublox-rnx/Cargo.toml +++ b/ublox-rnx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ublox-rnx" -version = "0.1.3" +version = "0.1.4" license = "MIT OR Apache-2.0" authors = ["Guillaume W. Bres "] description = "Efficient RINEX production from a Ublox GNSS receiver" @@ -22,4 +22,4 @@ serialport = "4.2.0" ublox = "0.4.4" clap = { version = "4.4.10", features = ["derive", "color"] } gnss-rs = { version = "2.1.2", features = ["serde"] } -rinex = { path = "../rinex", version = "=0.15.1", features = ["serde", "nav", "obs"] } +rinex = { path = "../rinex", version = "=0.15.2", features = ["serde", "nav", "obs"] }