diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7a741fdb5..2b705618f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -34,11 +34,11 @@ from an `Observable`.
- `src/hatanaka/mod.rs`: the Hatanaka module contains the RINEX Compressor and Decompressor
- `src/antex/antenna.rs`: defines the index structure of ANTEX format
-NAV RINEX
-=========
+Navigation Data
+===============
-Orbit instantaneous parameters, broadcasted by GNSS vehicles, are presented in different
-forms depending on the RINEX revision and the GNSS constellation.
+Orbit broadcasted parameters are presented in different form depending on the RINEX revisions
+and also may differ in their nature depending on which constellation we're talking about.
To solve that problem, we use a dictionary, in the form of `src/db/NAV/orbits.json`,
which describes all fields per RINEX revision and GNSS constellation.
@@ -56,3 +56,60 @@ Introducing a new RINEX type
`src/meteo/mod.rs` is the easiest format and can serve as a guideline to follow.
When introducing a new Navigation Data, the dictionary will most likely have to be updated (see previous paragraph).
+
+GNSS Constellations
+===================
+
+Supported constellations are defined in the Constellation Module.
+This structure defines both Orbiting and Stationary vehicles.
+
+Adding new SBAS vehicles
+========================
+
+To add a newly launched SBAS vehicles, simply add it to the
+rinex/db/SBAS/sbas.json database.
+
+The only mandatory fields are :
+- the "constellation" field
+- the SBAS "prn" field (which is 100 + prn number)
+- "id": the name of that vehicle, for example "ASTRA-5B"
+- "launched\_year": the year this vehicle was launched
+
+Other optional fields are:
+- "launched\_month": month ths vehicle was launched
+- "launched\_day": day of month this vehicle was launched
+
+We don't support undeployed vehicles (in advance).
+
+Build scripts
+=============
+
+The build script is rinex/build.rs.
+
+It is responsible for building several important but hidden structures.
+
+1. Navigation RINEX specs, described by rinex/db/NAV
+2. Geostationary vehicles identification in rinex/db/sbas/sbas.json,
+that follows the L1-CA-PRN Code assignment specifications (see online specs).
+3. rinex/db/SBAS/*.wkt contains geographic definitions for most
+standard SBAS systems. We parse them as Geo::LineStrings to
+define a contour area for a given SBAS system. This gives one method
+to select a SBAS from given location on Earth
+
+Crate dependencies
+==================
+
+- `qc-traits` and `sinex` are core libraries.
+- `rinex` is the central dependency to most other libraries or applications.
+- tiny applications like `rnx2crx`, `crx2rnx` and `ublox-rnx` only depend on the rinex crate
+- `sp3` is a library that only depends on `rinex`
+- `gnss-rtk` is a library that depends on `rinex`, `sp3` and `rinex-qc`
+- `cli` is an application that exposes `rinex-qc`, `gnss-rtk`, `sp3` and `rinex`
+
+External key dependencies:
+
+- `Hifitime` (timing lib) is used by all libraries
+- `Nyx-space` (navigation lib) is used by `gnss-rtk`
+- `Ublox-rs` (UBX protocol) is used by `ublox-rnx`
+
+
diff --git a/README.md b/README.md
index 7c12243c6..9d7d64c9a 100644
--- a/README.md
+++ b/README.md
@@ -22,10 +22,12 @@ and we aim towards advanced geodesic and ionospheric analysis.
- Seamless .gzip decompression with `flate2` compilation feature
- RINEX V4 full support, that includes modern Navigation messages
- Meteo RINEX full support
-- IONEX and Clock RINEX partial support, will be concluded soon
+- IONEX (2D) support, partial 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
+- 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)
@@ -41,7 +43,6 @@ summon from the "cli" application directly.
- QZNSST is represented as GPST at the moment
- GLONASST and IRNSST are not supported : calculations (mostly orbits) will not be accurate
-- Partial SBAS support : some features are not yet available
- 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).
@@ -73,7 +74,7 @@ RINEX formats & applications
|----------------------------|-------------------|---------------------|----------------------|----------------------|--------------------------| ---------------------|
| Navigation (NAV) | :heavy_check_mark:| Ephemeris :construction: V4 :construction: | :heavy_check_mark: :chart_with_upwards_trend: | :construction: | Orbit parameters, Ionospheric models.. | Epoch iteration |
| Observation (OBS) | :heavy_check_mark:| :heavy_check_mark: | :heavy_check_mark: :chart_with_upwards_trend: | :construction: | Phase, Pseudo Range, Doppler, SSI | Epoch iteration |
-| CRINEX (Compressed OBS) | :heavy_check_mark:| RNX2CRX1 :heavy_check_mark: RNX2CRX3 :construction: | :heavy_check_mark: :chart_with_upwards_trend: | :construction: | see OBS Data | Epoch iteration |
+| CRINEX (Compressed OBS) | :heavy_check_mark:| RNX2CRX1 :heavy_check_mark: RNX2CRX3 :construction: | :heavy_check_mark: :chart_with_upwards_trend: | :construction: | Phase, Pseudo Range, Doppler, SSI | Epoch iteration |
| Meteorological data (MET) | :heavy_check_mark:| :heavy_check_mark: | :heavy_check_mark: :chart_with_upwards_trend: | :construction: | Meteo sensors data (Temperature, Moisture..) | Epoch iteration |
| Clocks (CLK) | :heavy_check_mark:| :construction: | :construction: |:construction: | Clock comparison | Epoch iteration |
| Antenna (ATX) | :heavy_check_mark:| :construction: | :construction: |:construction: | Antenna calibration data | Sorted by `antex::Antenna` |
diff --git a/crx2rnx/Cargo.toml b/crx2rnx/Cargo.toml
index 7a9881443..f3f811aea 100644
--- a/crx2rnx/Cargo.toml
+++ b/crx2rnx/Cargo.toml
@@ -12,4 +12,4 @@ readme = "README.md"
[dependencies]
clap = { version = "4", features = ["derive", "color"] }
-rinex = { path = "../rinex", version = "=0.14.0", features = ["serde"] }
+rinex = { path = "../rinex", version = "=0.14.1", features = ["serde"] }
diff --git a/crx2rnx/src/cli.rs b/crx2rnx/src/cli.rs
index f9f4f2957..b778e482d 100644
--- a/crx2rnx/src/cli.rs
+++ b/crx2rnx/src/cli.rs
@@ -33,7 +33,7 @@ impl Cli {
}
}
pub fn input_path(&self) -> &str {
- &self.matches.get_one::("filepath").unwrap()
+ self.matches.get_one::("filepath").unwrap()
}
pub fn output_path(&self) -> Option<&String> {
self.matches.get_one::("output")
diff --git a/crx2rnx/src/main.rs b/crx2rnx/src/main.rs
index 8bb721222..46c8308bf 100644
--- a/crx2rnx/src/main.rs
+++ b/crx2rnx/src/main.rs
@@ -13,9 +13,9 @@ fn main() -> Result<(), rinex::Error> {
Some(path) => path.clone(),
_ => {
// deduce from input path
- match input_path.strip_suffix("d") {
+ match input_path.strip_suffix('d') {
Some(prefix) => prefix.to_owned() + "o",
- _ => match input_path.strip_suffix("D") {
+ _ => match input_path.strip_suffix('D') {
Some(prefix) => prefix.to_owned() + "O",
_ => match input_path.strip_suffix("crx") {
Some(prefix) => prefix.to_owned() + "rnx",
diff --git a/doc/dependencies.png b/doc/dependencies.png
new file mode 100644
index 000000000..4a320af4b
Binary files /dev/null and b/doc/dependencies.png differ
diff --git a/doc/plots/sp3_residual.png b/doc/plots/sp3_residual.png
old mode 100755
new mode 100644
diff --git a/doc/plots/tec.png b/doc/plots/tec.png
new file mode 100644
index 000000000..bed8c6706
Binary files /dev/null and b/doc/plots/tec.png differ
diff --git a/gnss-rtk/Cargo.toml b/gnss-rtk/Cargo.toml
index e0f8bfba8..e51dc6e69 100644
--- a/gnss-rtk/Cargo.toml
+++ b/gnss-rtk/Cargo.toml
@@ -14,6 +14,9 @@ readme = "README.md"
[dependencies]
log = "0.4"
thiserror = "1"
+nalgebra = "=0.32"
nyx-space = "2.0.0-alpha.2"
-rinex-qc = { path = "../rinex-qc", features = ["serde"] }
-rinex = { path = "../rinex", features = ["serde", "flate2", "sbas", "obs", "nav", "qc", "processing"] }
+hifitime = { version = "3.8.4", features = ["serde", "std"] }
+rinex-qc = { path = "../rinex-qc", version = "=0.1.4", features = ["serde"] }
+rinex = { path = "../rinex", version = "=0.14.1", features = ["serde", "flate2", "sbas", "obs", "nav", "qc", "processing"] }
+serde = { version = "1.0", optional = true, default-features = false, features = ["derive"] }
diff --git a/gnss-rtk/README.md b/gnss-rtk/README.md
index 8e6979f68..959d7d1ef 100644
--- a/gnss-rtk/README.md
+++ b/gnss-rtk/README.md
@@ -1,8 +1,106 @@
GNSS-RTK
========
-Precise position solver.
+[![crates.io](https://img.shields.io/crates/v/gnss-rtk.svg)](https://crates.io/crates/gnss-rtk)
+[![rustc](https://img.shields.io/badge/rustc-1.64%2B-blue.svg)](https://img.shields.io/badge/rustc-1.64%2B-blue.svg)
+[![crates.io](https://docs.rs/gnss-rtk/badge.svg)](https://docs.rs/gnss-rtk/badge.svg)
+
+RTK precise position and timing solver, in rust.
The Solver can work from a RINEX context blob, defined in this crate toolsuite, but is not exclusively tied to RINEX.
The solver implements precise positioning algorithms, which are based on raw GNSS signals.
+
+Performances
+============
+
+I'm able to resolve every single Epoch in a modern 24h data context, in about 1 second, on my 8 core CPU.
+
+Solving method
+==============
+
+Only a straightforward Matrix based resolution method is implemented.
+Other solutions, like Kalman filter, exist and could potentially improve performances
+at the expense of more complexity and possibly
+
+The matrix resolution technique gives the best result for every single epoch
+
+- there are no initialization iterations
+- there is no iteration or recursive behavior
+
+Behavior and Output
+===================
+
+The solver will try to resolve a position for every single existing Epoch.
+
+When working with RINEX, preprocessing operations may apply.
+If you're working with the attached "cli" application, this is done with `-P`.
+For example, if the input context is huge, a smoothing or decimation
+
+The solver will output a SolverEstimate object on each resolved Epoch.
+Refer to this structure's documentation for more information.
+
+Timing DOP and Position DOP are estimated and attached to every single result.
+
+SPP
+===
+
+The solver supports the spp strategy. This strategy is the only strategy we can deploy
+on single carrier context. It is most likely the unique strategy you can deploy if you're working
+with old RINEX (like GPS only V2), or single frequency RINEX data.
+
+When using SPP :
+
+- you can only hope for residual errors of a few meters
+- an interpolation order above 9 makes no sense
+- Ionospheric delay must be considered and modeled. Refer to the related section.
+
+If you're operating this library from the "cli" application integrated to this toolsuite,
+a forced `--spp` mode exists. It is a convenient way to restrict this library to SPP solving
+and compare it to PPP.
+
+PPP
+===
+
+The solver will adapt to PPP strategy if the context is sufficient (more than one carrier).
+PPP simplifies the solving process greatly, ionospheric delay is cancelled and does not have to be taken into account.
+
+PPP is deployed if you're typically working with modern RINEX data.
+
+We allow the possibility to deploy a PPP strategy without SP3 data. This is not a typical use case.
+Other tools like glab or rtklib probably do not allow this.
+You need to understand that in this case, you want good navigation data quality in order to reduce
+the error their interpolation will introduce.
+
+When working with PPP, we recommend the interpolation order to be set to 11 (or above).
+
+Ionospheric Delay
+=================
+
+TODO
+
+SP3 and Broadcast Ephemeris
+===========================
+
+The solver will always prefer SP3 over Broadcast ephemeris.
+That stands whatever the solving method and strategy might be.
+
+RTK from RINEX
+==============
+
+The solver can be initialized from a RINEX context, defined as `QcContext` in the RINEX library suite.
+This structure is adaptable and quite efficient. For example it allows the combination of both
+SP3 and Broadcast Ephemeris.
+
+When initialized from RINEX, we can determine whether PPP is feasible or not
+
+RTK Configuration
+=================
+
+The RTKConfiguration structure, describes all configuration and customization
+the solver supports.
+
+It is important to understand how, when and what to customize depending on your goals.
+
+When working with the "cli" application, you can provide an RTKConfiguration
+in the form of JSON, with `--rtk-cfg`.
diff --git a/gnss-rtk/src/cfg.rs b/gnss-rtk/src/cfg.rs
new file mode 100644
index 000000000..090552732
--- /dev/null
+++ b/gnss-rtk/src/cfg.rs
@@ -0,0 +1,131 @@
+use crate::model::Modeling;
+use crate::SolverType;
+use hifitime::prelude::TimeScale;
+
+use std::str::FromStr;
+
+#[cfg(feature = "serde")]
+use serde::Deserialize;
+
+use rinex::prelude::GroundPosition;
+
+use rinex::observation::Snr;
+
+fn default_timescale() -> TimeScale {
+ TimeScale::GPST
+}
+
+fn default_interp() -> usize {
+ 7
+}
+
+fn default_max_sv() -> usize {
+ 10
+}
+
+fn default_smoothing() -> bool {
+ false
+}
+
+fn default_iono() -> bool {
+ false
+}
+
+fn default_tropo() -> bool {
+ false
+}
+
+#[derive(Default, Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "serde", derive(Deserialize))]
+pub struct RTKConfig {
+ /// Time scale
+ #[cfg_attr(feature = "serde", serde(default = "default_timescale"))]
+ pub timescale: TimeScale,
+ /// positioning mode
+ #[cfg_attr(feature = "serde", serde(default))]
+ pub mode: SolverMode,
+ /// (Position) interpolation filter order.
+ /// A minimal order must be respected for correct results.
+ /// - 7 when working with broadcast ephemeris
+ /// - 11 when working with SP3
+ #[cfg_attr(feature = "serde", serde(default = "default_interp"))]
+ pub interp_order: usize,
+ /// Whether the solver is working in fixed altitude mode or not
+ #[cfg_attr(feature = "serde", serde(default))]
+ pub fixed_altitude: Option,
+ /// Position receveir position, if known before hand
+ pub rcvr_position: Option,
+ /// PR code smoothing filter before moving forward
+ #[cfg_attr(feature = "serde", serde(default = "default_smoothing"))]
+ pub code_smoothing: bool,
+ /// true if we're using troposphere modeling
+ #[cfg_attr(feature = "serde", serde(default = "default_tropo"))]
+ pub tropo: bool,
+ /// true if we're using ionosphere modeling
+ #[cfg_attr(feature = "serde", serde(default = "default_iono"))]
+ pub iono: bool,
+ /// Minimal percentage ]0; 1[ of Sun light to be received by an SV
+ /// for not to be considered in Eclipse.
+ /// A value closer to 0 means we tolerate fast Eclipse exit.
+ /// A value closer to 1 is a stringent criteria: eclipse must be totally exited.
+ #[cfg_attr(feature = "serde", serde(default))]
+ pub min_sv_sunlight_rate: Option,
+ /// Minimal elevation angle. SV below that angle will not be considered.
+ pub min_sv_elev: Option,
+ /// Minimal SNR for an SV to be considered.
+ pub min_sv_snr: Option,
+ /// modeling
+ #[cfg_attr(feature = "serde", serde(default))]
+ pub modeling: Modeling,
+ /// Max. number of vehicules to consider.
+ /// The more the merrier, but it also means heavier computations
+ #[cfg_attr(feature = "serde", serde(default = "default_max_sv"))]
+ pub max_sv: usize,
+}
+
+impl RTKConfig {
+ pub fn default(solver: SolverType) -> Self {
+ match solver {
+ SolverType::SPP => Self {
+ timescale: default_timescale(),
+ mode: SolverMode::default(),
+ fixed_altitude: None,
+ rcvr_position: None,
+ interp_order: default_interp(),
+ code_smoothing: default_smoothing(),
+ tropo: default_tropo(),
+ iono: default_iono(),
+ min_sv_sunlight_rate: None,
+ min_sv_elev: Some(10.0),
+ min_sv_snr: Some(Snr::from_str("weak").unwrap()),
+ modeling: Modeling::default(),
+ max_sv: default_max_sv(),
+ },
+ SolverType::PPP => Self {
+ timescale: default_timescale(),
+ mode: SolverMode::default(),
+ fixed_altitude: None,
+ rcvr_position: None,
+ interp_order: 11,
+ code_smoothing: default_smoothing(),
+ tropo: default_tropo(),
+ iono: default_iono(),
+ min_sv_sunlight_rate: Some(0.75),
+ min_sv_elev: Some(25.0),
+ min_sv_snr: Some(Snr::from_str("strong").unwrap()),
+ modeling: Modeling::default(),
+ max_sv: default_max_sv(),
+ },
+ }
+ }
+}
+
+#[derive(Default, Debug, Clone, Copy, PartialEq)]
+#[cfg_attr(feature = "serde", derive(Deserialize))]
+pub enum SolverMode {
+ /// Receiver is kept at fixed location
+ #[default]
+ Static,
+ /// Receiver is not static
+ Kinematic,
+}
diff --git a/gnss-rtk/src/estimate.rs b/gnss-rtk/src/estimate.rs
new file mode 100644
index 000000000..4e98a7002
--- /dev/null
+++ b/gnss-rtk/src/estimate.rs
@@ -0,0 +1,63 @@
+use nyx_space::cosmic::SPEED_OF_LIGHT;
+// use nalgebra::linalg::svd::SVD;
+use nalgebra::base::{DVector, MatrixXx4};
+
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+/*
+ * Solver solution estimate
+ * is always expressed as a correction of an 'a priori' position
+*/
+#[derive(Debug, Copy, Clone, Default)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct SolverEstimate {
+ /// X coordinates correction
+ pub dx: f64,
+ /// Y coordinates correction
+ pub dy: f64,
+ /// Z coordinates correction
+ pub dz: f64,
+ /// Time correction
+ pub dt: f64,
+ /// Dilution of Position Precision, horizontal component
+ pub hdop: f64,
+ /// Dilution of Position Precision, vertical component
+ pub vdop: f64,
+ /// Time Dilution of Precision
+ pub tdop: f64,
+}
+
+impl SolverEstimate {
+ /*
+ * Builds a new SolverEstimate from `g` Nav Matrix,
+ * and `y` Nav Vector
+ */
+ pub fn new(g: MatrixXx4, y: DVector) -> Option {
+ //let svd = g.clone().svd(true, true);
+ //let u = svd.u?;
+ //let v = svd.v_t?;
+ //let s = svd.singular_values;
+ //let s_inv = s.pseudo_inverse(1.0E-8).unwrap();
+ //let x = v * u.transpose() * y * s_inv;
+
+ let g_prime = g.clone().transpose();
+ let q = (g_prime.clone() * g.clone()).try_inverse()?;
+ let x = q * g_prime.clone();
+ let x = x * y;
+
+ let hdop = (q[(0, 0)] + q[(1, 1)]).sqrt();
+ let vdop = q[(2, 2)].sqrt();
+ let tdop = q[(3, 3)].sqrt();
+
+ Some(Self {
+ dx: x[0],
+ dy: x[1],
+ dz: x[2],
+ dt: x[3] / SPEED_OF_LIGHT,
+ hdop,
+ vdop,
+ tdop,
+ })
+ }
+}
diff --git a/gnss-rtk/src/lib.rs b/gnss-rtk/src/lib.rs
index 1474df897..9195b3e30 100644
--- a/gnss-rtk/src/lib.rs
+++ b/gnss-rtk/src/lib.rs
@@ -1,35 +1,68 @@
use nyx_space::cosmic::eclipse::{eclipse_state, EclipseState};
use nyx_space::cosmic::{Orbit, SPEED_OF_LIGHT};
use nyx_space::md::prelude::{Bodies, LightTimeCalc};
-use rinex::prelude::{Duration, Epoch, Sv};
+use rinex::navigation::Ephemeris;
+use rinex::prelude::{
+ //Duration,
+ Epoch,
+ Sv,
+};
use rinex_qc::QcContext;
use std::collections::HashMap;
+use hifitime::{Duration, TimeScale, Unit};
+
extern crate nyx_space as nyx;
+use nalgebra::base::{
+ DVector,
+ MatrixXx4,
+ //Vector1,
+ //Vector3,
+ //Vector4,
+};
use nyx::md::prelude::{Arc, Cosm};
-mod models;
-mod opts;
+mod cfg;
+mod estimate;
+mod model;
pub mod prelude {
- pub use crate::opts::PositioningMode;
- pub use crate::opts::SolverOpts;
+ pub use crate::cfg::RTKConfig;
+ pub use crate::cfg::SolverMode;
+ pub use crate::estimate::SolverEstimate;
+ pub use crate::model::Modeling;
pub use crate::Solver;
- pub use crate::SolverEstimate;
+ pub use crate::SolverError;
pub use crate::SolverType;
}
-use opts::SolverOpts;
+use cfg::RTKConfig;
+use estimate::SolverEstimate;
+use model::Modeling;
-use log::{debug, trace, warn};
+use log::{debug, error, trace, warn};
use thiserror::Error;
-#[derive(Debug, Clone, Copy, Error)]
-pub enum Error {
- #[error("provided context is either unsufficient or invalid for any position solving")]
+#[derive(Debug, Clone, Error)]
+pub enum SolverError {
+ #[error("provided context is either not sufficient or invalid")]
Unfeasible,
+ #[error("apriori position is not defined")]
+ UndefinedAprioriPosition,
+ #[error("failed to initialize solver - \"{0}\"")]
+ InitializationError(String),
+ #[error("no vehicles elected @{0}")]
+ NoSv(Epoch),
+ #[error("not enough vehicles elected @{0}")]
+ LessThan4Sv(Epoch),
+ #[error("failed to retrieve work epoch (index: {0})")]
+ EpochDetermination(usize),
+ #[error("badop: solver not initialized")]
+ NotInitialized,
+ #[error("failed to invert navigation matrix @{0}")]
+ SolvingError(Epoch),
}
#[derive(Default, Debug, Clone, Copy, PartialEq)]
@@ -53,60 +86,42 @@ impl std::fmt::Display for SolverType {
}
impl SolverType {
- fn from(ctx: &QcContext) -> Result {
+ fn from(ctx: &QcContext) -> Result {
if ctx.primary_data().is_observation_rinex() {
- if ctx.has_sp3() {
- Ok(Self::PPP)
- } else {
- if ctx.has_navigation_data() {
- Ok(Self::SPP)
- } else {
- Err(Error::Unfeasible)
- }
- }
+ //TODO : multi carrier for selected constellations
+ Ok(Self::SPP)
} else {
- Err(Error::Unfeasible)
+ Err(SolverError::Unfeasible)
}
}
}
#[derive(Debug)]
pub struct Solver {
- /// Cosmic model
- cosmic: Arc,
/// Solver parametrization
- pub opts: SolverOpts,
- /// Whether this solver is initiated (ready to iterate) or not
- initiated: bool,
+ pub cfg: RTKConfig,
/// Type of solver implemented
pub solver: SolverType,
- /// Current epoch
+ /// cosmic model
+ cosmic: Arc,
+ /// true if self has been initiated and is ready to compute
+ initiated: bool,
+ /// current epoch
nth_epoch: usize,
- /// current estimate
- pub estimate: SolverEstimate,
-}
-
-#[derive(Debug, Copy, Clone, Default)]
-pub struct SolverEstimate {
- /// Position estimate
- pub pos: (f64, f64, f64),
- /// Time offset estimate
- pub clock_offset: Duration,
}
impl Solver {
- pub fn from(context: &QcContext) -> Result {
+ pub fn from(context: &QcContext) -> Result {
let solver = SolverType::from(context)?;
Ok(Self {
cosmic: Cosm::de438(),
solver,
initiated: false,
- opts: SolverOpts::default(solver),
+ cfg: RTKConfig::default(solver),
nth_epoch: 0,
- estimate: SolverEstimate::default(),
})
}
- pub fn init(&mut self, ctx: &mut QcContext) {
+ pub fn init(&mut self, ctx: &mut QcContext) -> Result<(), SolverError> {
trace!("{} solver initialization..", self.solver);
//TODO: Preprocessing:
// only for ppp solver
@@ -123,140 +138,332 @@ impl Solver {
// total_dropped,
// total
// );
+ /*
+ * Solving needs a ref. position
+ */
+ if self.cfg.rcvr_position.is_none() {
+ // defined in context ?
+ let position = ctx.ground_position();
+ if let Some(position) = position {
+ self.cfg.rcvr_position = Some(position);
+ } else {
+ return Err(SolverError::UndefinedAprioriPosition);
+ }
+ }
- // 2: interpolate: if need be
- //if !ctx.interpolated {
- // trace!("orbit interpolation..");
- // let order = self.opts.interp_order;
- // ctx.orbit_interpolation(order, None);
- // //TODO could be nice to have some kind of timing/perf evaluation here
- // // and also total number of required interpolations
- //}
+ /*
+ * print some infos on latched config
+ */
+ if self.cfg.modeling.earth_rotation {
+ warn!("can't compensate for earth rotation at the moment");
+ }
+ if self.cfg.modeling.relativistic_clock_corr {
+ warn!("relativistic clock corr. is not feasible at the moment");
+ }
+ if self.solver == SolverType::PPP && self.cfg.min_sv_sunlight_rate.is_some() {
+ warn!("eclipse filter is not meaningful when using spp strategy");
+ }
- //initialization
self.nth_epoch = 0;
- self.estimate.pos = self.opts.rcvr_position.into();
self.initiated = true;
+ Ok(())
}
- pub fn run(&mut self, ctx: &mut QcContext) -> Option<(Epoch, SolverEstimate)> {
+ pub fn run(&mut self, ctx: &mut QcContext) -> Result<(Epoch, SolverEstimate), SolverError> {
if !self.initiated {
- self.init(ctx);
- trace!("solver initiated");
- } else {
- // move on to next epoch
+ return Err(SolverError::NotInitialized);
+ }
+
+ let pos0 = self
+ .cfg
+ .rcvr_position
+ .ok_or(SolverError::UndefinedAprioriPosition)?;
+
+ let (x0, y0, z0): (f64, f64, f64) = pos0.into();
+
+ let modeling = self.cfg.modeling;
+ let interp_order = self.cfg.interp_order;
+
+ // 0: grab work instant
+ let t = ctx.primary_data().epoch().nth(self.nth_epoch);
+
+ if t.is_none() {
+ self.nth_epoch += 1;
+ return Err(SolverError::EpochDetermination(self.nth_epoch));
+ }
+ let t = t.unwrap();
+
+ // 1: elect sv
+ let sv = Self::sv_at_epoch(ctx, t);
+ if sv.is_none() {
+ warn!("no vehicles found @ {}", t);
self.nth_epoch += 1;
+ return Err(SolverError::NoSv(t));
}
- // grab work instant
- let t = ctx.primary_data().epoch().nth(self.nth_epoch)?;
+ let mut elected_sv: Vec = sv.unwrap().into_iter().take(self.cfg.max_sv).collect();
+
+ trace!("{:?}: {} candidates", t, elected_sv.len());
+
+ // retrieve associated PR
+ let pr: Vec<_> = ctx
+ .primary_data()
+ .pseudo_range_ok()
+ .filter_map(|(epoch, svnn, _, pr)| {
+ if epoch == t && elected_sv.contains(&svnn) {
+ Some((svnn, pr))
+ } else {
+ None
+ }
+ })
+ .collect();
- let interp_order = self.opts.interp_order;
+ // apply first set of filters : on OBSERVATION
+ // - no pseudo range: nothing is feasible
+ // - if we're in ppp mode: must be compliant
+ // - if an SNR mask is defined: SNR must be good enough
+ elected_sv.retain(|sv| {
+ let has_pr = pr
+ .iter()
+ .filter_map(|(svnn, pr)| if svnn == sv { Some(pr) } else { None })
+ .reduce(|pr, _| pr)
+ .is_some();
- /* elect vehicles */
- let elected_sv = Self::sv_election(ctx, t);
- if elected_sv.is_none() {
- warn!("no vehicles elected @ {}", t);
- return Some((t, self.estimate));
+ let mut ppp_ok = !(self.solver == SolverType::PPP);
+ if self.solver == SolverType::PPP {
+ //TODO: verify PPP compliancy
+ }
+
+ let mut snr_ok = self.cfg.min_sv_snr.is_none();
+ if let Some(min_snr) = self.cfg.min_sv_snr {
+ let snr = ctx
+ .primary_data()
+ .snr()
+ .filter_map(|((epoch, _), svnn, _, snr)| {
+ if epoch == t && svnn == *sv {
+ Some(snr)
+ } else {
+ None
+ }
+ })
+ .reduce(|snr, _| snr);
+ if let Some(snr) = snr {
+ snr_ok = snr >= min_snr;
+ }
+ }
+
+ if !has_pr {
+ trace!("{:?}: {} no pseudo range", t, sv);
+ }
+ if !ppp_ok {
+ trace!("{:?}: {} not ppp compliant", t, sv);
+ }
+ if !snr_ok {
+ trace!("{:?}: {} snr below criteria", t, sv);
+ }
+
+ has_pr && snr_ok & ppp_ok
+ });
+
+ // make sure we still have enough SV
+ if elected_sv.len() < 4 {
+ debug!("{:?}: not enough vehicles elected", t);
+ self.nth_epoch += 1;
+ return Err(SolverError::LessThan4Sv(t));
}
- let mut elected_sv = elected_sv.unwrap();
- debug!("elected sv : {:?}", elected_sv);
+ debug!("{:?}: {} elected sv", t, elected_sv.len());
- /* determine sv positions */
- /* TODO: SP3 APC corrections: Self::eval_sun_vector3d */
+ let mut sv_data: HashMap = HashMap::new();
- let mut sv_pos: HashMap = HashMap::new();
+ // 3: sv position evaluation
for sv in &elected_sv {
- if let Some(sp3) = ctx.sp3_data() {
- if let Some((x_km, y_km, z_km)) = sp3.sv_position_interpolate(*sv, t, interp_order)
- {
- sv_pos.insert(*sv, (x_km, y_km, z_km));
- } else if let Some(nav) = ctx.navigation_data() {
- if let Some((x_km, y_km, z_km)) =
- nav.sv_position_interpolate(*sv, t, interp_order)
- {
- sv_pos.insert(*sv, (x_km, y_km, z_km));
- }
- }
- } else {
- if let Some(nav) = ctx.navigation_data() {
- if let Some((x_km, y_km, z_km)) =
- nav.sv_position_interpolate(*sv, t, interp_order)
- {
- sv_pos.insert(*sv, (x_km, y_km, z_km));
+ // retrieve pr for this SV @ t
+ let pr = pr
+ .iter()
+ .filter_map(|(svnn, pr)| if svnn == sv { Some(*pr) } else { None })
+ .reduce(|pr, _| pr)
+ .unwrap(); // can't fail at this point
+
+ let ts = sv.timescale().unwrap(); // can't fail at this point ?
+
+ let nav = ctx.navigation_data().unwrap(); // can't fail at this point ?
+
+ let ephemeris = nav.sv_ephemeris(*sv, t);
+ if ephemeris.is_none() {
+ error!("{:?} : {} no valid ephemeris", t, sv);
+ continue;
+ }
+
+ let (toe, eph) = ephemeris.unwrap();
+ let clock_bias = eph.sv_clock();
+ let (t_tx, dt_sat) =
+ Self::sv_transmission_time(t, *sv, toe, pr, eph, modeling, clock_bias, ts);
+
+ if modeling.earth_rotation {
+ //TODO
+ // dt = || rsat - rcvr0 || /c
+ // rsat = R3 * we * dt * rsat
+ // we = 7.2921151467 E-5
+ }
+
+ if modeling.relativistic_clock_corr {
+ //TODO
+ let e = 1.204112719279E-2;
+ let sqrt_a = 5.153704689026E3;
+ let sqrt_mu = (3986004.418E8_f64).sqrt();
+ //let dt = -2.0_f64 * sqrt_a * sqrt_mu / SPEED_OF_LIGHT / SPEED_OF_LIGHT * e * elev.sin();
+ }
+
+ // interpolate
+ let pos: Option<(f64, f64, f64)> = match ctx.sp3_data() {
+ Some(sp3) => {
+ /*
+ * SP3 always prefered
+ */
+ let pos = sp3.sv_position_interpolate(*sv, t_tx, interp_order);
+ if let Some(pos) = pos {
+ Some(pos)
+ } else {
+ /* try to fall back to ephemeris nav */
+ nav.sv_position_interpolate(*sv, t_tx, interp_order)
}
+ },
+ _ => nav.sv_position_interpolate(*sv, t_tx, interp_order),
+ };
+
+ if pos.is_none() {
+ trace!("{:?} : {} interpolation failed", t, sv);
+ continue;
+ }
+
+ let (x_km, y_km, z_km) = pos.unwrap();
+
+ // Elevation filter
+ if let Some(min_elev) = self.cfg.min_sv_elev {
+ let (e, _) = Ephemeris::elevation_azimuth(
+ (x_km * 1.0E3, y_km * 1.0E3, z_km * 1.0E3),
+ pos0.into(),
+ );
+ if e < min_elev {
+ trace!("{:?} : {} elev below mask", t, sv);
+ continue;
}
}
- }
- /* remove sv in eclipse */
- if let Some(min_rate) = self.opts.min_sv_sunlight_rate {
- sv_pos.retain(|sv, (x_km, y_km, z_km)| {
- let state = self.eclipse_state(*x_km, *y_km, *z_km, t);
+ // Eclipse filter
+ if let Some(min_rate) = self.cfg.min_sv_sunlight_rate {
+ let state = self.eclipse_state(x_km, y_km, z_km, t_tx);
let eclipsed = match state {
EclipseState::Umbra => true,
EclipseState::Visibilis => false,
- EclipseState::Penumbra(r) => {
- debug!("{} state: {}", sv, state);
- r < min_rate
- },
+ EclipseState::Penumbra(r) => r < min_rate,
};
-
if eclipsed {
- debug!("dropping eclipsed {}", sv);
+ debug!("{:?} : dropping eclipsed {}", t, sv);
+ } else {
+ sv_data.insert(*sv, (x_km * 1.0E3, y_km * 1.0E3, z_km * 1.0E3, pr, dt_sat));
}
- !eclipsed
- });
+ } else {
+ sv_data.insert(*sv, (x_km * 1.0E3, y_km * 1.0E3, z_km * 1.0E3, pr, dt_sat));
+ }
}
- // 3: t_tx
- let mut t_tx: HashMap = HashMap::new();
- for sv in &elected_sv {
- if let Some(sv_t_tx) = Self::sv_transmission_time(ctx, *sv, t) {
- t_tx.insert(*sv, sv_t_tx);
- }
+ // 6: form matrix
+ let mut y = DVector::::zeros(elected_sv.len());
+ let mut g = MatrixXx4::::zeros(elected_sv.len());
+
+ if sv_data.iter().count() < 4 {
+ error!("{:?} : not enough sv to resolve", t);
+ self.nth_epoch += 1;
+ return Err(SolverError::LessThan4Sv(t));
}
- //TODO
- // add other models
+ for (index, (sv, data)) in sv_data.iter().enumerate() {
+ let pr = data.3;
+ let dt_sat = data.4.to_seconds();
+ let (sv_x, sv_y, sv_z) = (data.0, data.1, data.2);
+
+ let rho = ((sv_x - x0).powi(2) + (sv_y - y0).powi(2) + (sv_z - z0).powi(2)).sqrt();
+
+ //TODO
+ let mut models = -SPEED_OF_LIGHT * dt_sat;
+ //let models = models
+ // .iter()
+ // .filter_map(|sv, model| {
+ // if sv == svnn {
+ // Some(model)
+ // } else {
+
+ // }
+ // })
+ // .reduce(|m, _| m)
+ // .unwrap();
+
+ y[index] = pr - rho - models;
- // form matrix
- // resolve
- Some((t, self.estimate))
+ g[(index, 0)] = (x0 - sv_x) / rho;
+ g[(index, 1)] = (y0 - sv_y) / rho;
+ g[(index, 2)] = (z0 - sv_z) / rho;
+ g[(index, 3)] = 1.0_f64;
+ }
+
+ // 7: resolve
+ //trace!("y: {} | g: {}", y, g);
+ let estimate = SolverEstimate::new(g, y);
+ self.nth_epoch += 1;
+
+ if estimate.is_none() {
+ return Err(SolverError::SolvingError(t));
+ } else {
+ Ok((t, estimate.unwrap()))
+ }
}
/*
- * Evalutes T_tx transmission time, for given Sv at desired 't'
+ * Evalutes Sv position
*/
- fn sv_transmission_time(ctx: &QcContext, sv: Sv, t: Epoch) -> Option {
- let nav = ctx.navigation_data()?;
- // need one pseudo range observation for this SV @ 't'
- let mut pr = ctx
- .primary_data()
- .pseudo_range()
- .filter_map(|((e, flag), svnn, _, p)| {
- if e == t && flag.is_ok() && svnn == sv {
- Some(p)
- } else {
- None
- }
- })
- .take(1);
- if let Some(pr) = pr.next() {
- let t_tx = Duration::from_seconds(t.to_duration().to_seconds() - pr / SPEED_OF_LIGHT);
- debug!("t_tx(pr): {}@{} : {}", sv, t, t_tx);
+ fn sv_transmission_time(
+ t: Epoch,
+ sv: Sv,
+ toe: Epoch,
+ pr: f64,
+ eph: &Ephemeris,
+ m: Modeling,
+ clock_bias: (f64, f64, f64),
+ ts: TimeScale,
+ ) -> (Epoch, Duration) {
+ let seconds_ts = t.to_duration().to_seconds();
- let mut e_tx = Epoch::from_duration(t_tx, sv.constellation.timescale()?);
- let dt_sat = nav.sv_clock_bias(sv, e_tx)?;
- debug!("clock bias: {}@{} : {}", sv, t, dt_sat);
+ let dt_tx = seconds_ts - pr / SPEED_OF_LIGHT;
+ let mut e_tx = Epoch::from_duration(dt_tx * Unit::Second, t.time_scale);
+ let mut dt_sat = Duration::default();
+ if m.sv_clock_bias {
+ dt_sat = Ephemeris::sv_clock_corr(sv, clock_bias, t, toe);
+ debug!("{:?}: {} dt_sat {}", t, sv, dt_sat);
e_tx -= dt_sat;
- debug!("{} : t(obs): {} | t(tx) {}", sv, t, e_tx);
+ }
- Some(e_tx)
- } else {
- debug!("missing PR measurement");
- None
+ if m.sv_total_group_delay {
+ if let Some(tgd) = eph.tgd() {
+ let tgd = tgd * Unit::Second;
+ debug!("{:?}: {} tgd {}", t, sv, tgd);
+ e_tx -= tgd;
+ }
}
+
+ debug!("{:?}: {} t_tx {:?}", t, sv, e_tx);
+
+ /*
+ * physical verification on result
+ */
+ let dt = (t - e_tx).to_seconds();
+ assert!(dt > 0.0, "t_tx can't physically be after t_rx..!");
+ assert!(
+ dt < 1.0,
+ "|t - t_tx| < 1s is physically impossible (signal propagation..)"
+ );
+
+ (e_tx, dt_sat)
}
/*
* Evaluates Sun/Earth vector, expressed in Km
@@ -291,9 +498,9 @@ impl Solver {
eclipse_state(&sv_orbit, sun_frame, earth_frame, &self.cosmic)
}
/*
- * Elects sv for this epoch
+ * Returns all Sv at "t"
*/
- fn sv_election(ctx: &QcContext, t: Epoch) -> Option> {
+ fn sv_at_epoch(ctx: &QcContext, t: Epoch) -> Option> {
ctx.primary_data()
.sv_epoch()
.filter_map(|(epoch, svs)| if epoch == t { Some(svs) } else { None })
diff --git a/gnss-rtk/src/model.rs b/gnss-rtk/src/model.rs
new file mode 100644
index 000000000..f77344b8d
--- /dev/null
+++ b/gnss-rtk/src/model.rs
@@ -0,0 +1,58 @@
+use crate::SolverType;
+
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+fn default_sv_clock() -> bool {
+ true
+}
+
+fn default_sv_tgd() -> bool {
+ true
+}
+
+fn default_earth_rot() -> bool {
+ false
+}
+
+fn default_rel_clock_corr() -> bool {
+ false
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct Modeling {
+ #[cfg_attr(feature = "serde", serde(default = "default_sv_clock"))]
+ pub sv_clock_bias: bool,
+ #[cfg_attr(feature = "serde", serde(default = "default_sv_tgd"))]
+ pub sv_total_group_delay: bool,
+ #[cfg_attr(feature = "serde", serde(default = "default_earth_rot"))]
+ pub earth_rotation: bool,
+ #[cfg_attr(feature = "serde", serde(default = "default_rel_clock_corr"))]
+ pub relativistic_clock_corr: bool,
+}
+
+impl Default for Modeling {
+ fn default() -> Self {
+ Self {
+ sv_clock_bias: default_sv_clock(),
+ sv_total_group_delay: default_sv_tgd(),
+ earth_rotation: default_earth_rot(),
+ relativistic_clock_corr: default_rel_clock_corr(),
+ }
+ }
+}
+
+impl From for Modeling {
+ fn from(solver: SolverType) -> Self {
+ let mut s = Self::default();
+ match solver {
+ SolverType::PPP => {
+ s.earth_rotation = true;
+ s.relativistic_clock_corr = false;
+ },
+ _ => {},
+ }
+ s
+ }
+}
diff --git a/gnss-rtk/src/opts/mod.rs b/gnss-rtk/src/opts/mod.rs
deleted file mode 100644
index 35a355065..000000000
--- a/gnss-rtk/src/opts/mod.rs
+++ /dev/null
@@ -1,107 +0,0 @@
-use crate::SolverType;
-use rinex::prelude::{Constellation, GroundPosition};
-
-#[derive(Default, Debug, Clone, PartialEq)]
-pub struct SolverOpts {
- /// Criteria (for convergence)
- pub epsilon: f64,
- /// (Position) interpolation filter order.
- /// A minimal order must be respected for correct results.
- /// - 7 when working with broadcast ephemeris
- /// - 11 when working with SP3
- pub interp_order: usize,
- /// positioning mode
- pub positioning: PositioningMode,
- /// Whether the solver is working in fixed altitude mode or not
- pub fixed_altitude: Option,
- /// Position receveir position, if known before hand
- pub rcvr_position: GroundPosition,
- /// constellation to consider,
- pub gnss: Vec,
- /// PR code smoothing filter before moving forward
- pub code_smoothing: bool,
- /// true if we're using troposphere modeling
- pub tropo: bool,
- /// true if we're using ionosphere modeling
- pub iono: bool,
- /// true if we're using total group delay modeling
- pub tgd: bool,
- /// Minimal percentage ]0; 1[ of Sun light to be received by an SV
- /// for not to be considered in Eclipse.
- /// A value closer to 0 means we tolerate fast Eclipse exit.
- /// A value closer to 1 is a stringent criteria: eclipse must be totally exited.
- pub min_sv_sunlight_rate: Option,
-}
-
-impl SolverOpts {
- pub fn default(solver: SolverType) -> Self {
- match solver {
- SolverType::SPP => Self {
- epsilon: 5.0_f64,
- gnss: vec![Constellation::GPS, Constellation::Galileo],
- fixed_altitude: None,
- rcvr_position: GroundPosition::default(),
- interp_order: 7,
- positioning: PositioningMode::default(),
- code_smoothing: false,
- tropo: false,
- iono: false,
- tgd: false,
- min_sv_sunlight_rate: None,
- },
- SolverType::PPP => Self {
- epsilon: 0.1_f64,
- gnss: vec![Constellation::GPS, Constellation::Galileo],
- fixed_altitude: None,
- rcvr_position: GroundPosition::default(),
- interp_order: 11,
- positioning: PositioningMode::default(),
- code_smoothing: false,
- tropo: false,
- iono: false,
- tgd: false,
- min_sv_sunlight_rate: Some(0.3),
- },
- }
- }
-}
-
-#[derive(Default, Debug, Clone, Copy, PartialEq)]
-pub enum PositioningMode {
- /// Receiver is kept at fixed location
- #[default]
- Static,
- /// Receiver is not static
- Kinematic,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq)]
-#[allow(dead_code)]
-pub enum SpecificOpts {
- /// SPP solver specific parameters
- SPPSpecificOpts(SppOpts),
- /// PPP solver specific parameters
- PPPSpecificOpts(PppOpts),
-}
-
-#[allow(dead_code)]
-impl SpecificOpts {
- fn spp(&self) -> Option {
- match self {
- Self::SPPSpecificOpts(opts) => Some(*opts),
- _ => None,
- }
- }
- fn ppp(&self) -> Option {
- match self {
- Self::PPPSpecificOpts(opts) => Some(*opts),
- _ => None,
- }
- }
-}
-
-#[derive(Default, Debug, Clone, Copy, PartialEq)]
-pub struct SppOpts {}
-
-#[derive(Default, Debug, Clone, Copy, PartialEq)]
-pub struct PppOpts {}
diff --git a/gnss-rtk/src/solver.rs b/gnss-rtk/src/solver.rs
deleted file mode 100644
index e69de29bb..000000000
diff --git a/rinex-cli/Cargo.toml b/rinex-cli/Cargo.toml
index c5bfc0d68..ee6d703d4 100644
--- a/rinex-cli/Cargo.toml
+++ b/rinex-cli/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rinex-cli"
-version = "0.9.3"
+version = "0.9.4"
license = "MIT OR Apache-2.0"
authors = ["Guillaume W. Bres "]
description = "Command line tool parse and analyze RINEX data"
@@ -14,19 +14,23 @@ rust-version = "1.64"
[dependencies]
log = "0.4"
-pretty_env_logger = "0.5"
+env_logger = "0.10"
clap = { version = "4.4.3", features = ["derive", "color"] }
rand = "0.8"
serde_json = "1"
-sp3 = { path = "../sp3", version = "=1.0.4", features = ["serde", "flate2"] }
+sp3 = { path = "../sp3", version = "=1.0.5", features = ["serde", "flate2"] }
rinex-qc = { path = "../rinex-qc", version = "=0.1.4", features = ["serde"] }
-rinex = { path = "../rinex", version = "=0.14.0", features = ["full"] }
-gnss-rtk = { path = "../gnss-rtk", version = "=0.0.1" }
+rinex = { path = "../rinex", version = "=0.14.1", features = ["full"] }
+gnss-rtk = { path = "../gnss-rtk", version = "=0.0.1", features = ["serde"] }
thiserror = "1"
itertools = "0.11"
-plotly = "0.8.4"
+# plotly = "0.8.4"
+plotly = { git = "https://github.com/gwbres/plotly", branch = "density-mapbox" }
+# plotly = { path = "../../plotly-rs/plotly" }
map_3d = "0.1.5"
ndarray = "0.15"
colorous = "1.0"
horrorshow = "0.8"
nyx-space = "2.0.0-alpha.2"
+hifitime = { version = "3.8.4", features = ["serde", "std"] }
+serde = { version = "1.0", default-features = false, features = ["derive"] }
diff --git a/rinex-cli/README.md b/rinex-cli/README.md
index 3879d12c8..1c27ac975 100644
--- a/rinex-cli/README.md
+++ b/rinex-cli/README.md
@@ -108,12 +108,13 @@ if you know how to operate the preprocessing toolkit
- [quality check](doc/qc.md): RINEX data quality analysis (mainly statistics and only on OBS RINEX at the moment)
- other advanced operations are documented in the [processing](doc/processing.md) suite
-## Positioning
+## Positioning (RTK)
-`rinex-cli` integrates a position solver that will resolve the user location
-the best it can, from the provided RINEX context. This mode in requested with `-p`.
+`rinex-cli` integrates a position solver that will resolve the radio receiver location
+the best it can, by post processing the provided RINEX context.
+This mode in requested with `-r` or `--rtk` and is turned off by default.
-To learn how to operate the solver, refer to [the dedicated page](doc/positioning.md).
+To learn how to operate the solver, refer to [the dedicated page](doc/rtk.md).
## Getting started
@@ -236,10 +237,10 @@ rinex-cli -f OBS/V2/KOSG0010.95O --epochs
rinex-cli -f test_resources/OBS/V2/KOSG0010.95O --epochs --sv
```
-The `--pretty` option is there to make the datasets more readable (json format):
+The `--pretty` (`-p`) option is there to make the datasets more readable (json format):
```bash
-rinex-cli -f test_resources/OBS/V2/KOSG0010.95O --epochs --sv --pretty
+rinex-cli -f test_resources/OBS/V2/KOSG0010.95O -g --epochs --sv -p
```
## Data analysis
diff --git a/rinex-cli/config/gnss_snr30db.json b/rinex-cli/config/qc/gnss_snr30db.json
similarity index 100%
rename from rinex-cli/config/gnss_snr30db.json
rename to rinex-cli/config/qc/gnss_snr30db.json
diff --git a/rinex-cli/config/sv_manual_gap.json b/rinex-cli/config/qc/sv_manual_gap.json
similarity index 100%
rename from rinex-cli/config/sv_manual_gap.json
rename to rinex-cli/config/qc/sv_manual_gap.json
diff --git a/rinex-cli/config/rtk/gpst_10sv_basic.json b/rinex-cli/config/rtk/gpst_10sv_basic.json
new file mode 100644
index 000000000..0a219e729
--- /dev/null
+++ b/rinex-cli/config/rtk/gpst_10sv_basic.json
@@ -0,0 +1,9 @@
+{
+ "timescale": "GPST",
+ "interp_order": 11,
+ "max_sv": 10,
+ "modeling": {
+ "sv_clock_bias": true,
+ "sv_total_group_delay": true
+ }
+}
diff --git a/rinex-cli/config/rtk/gpst_4sv_basic.json b/rinex-cli/config/rtk/gpst_4sv_basic.json
new file mode 100644
index 000000000..15f2165b0
--- /dev/null
+++ b/rinex-cli/config/rtk/gpst_4sv_basic.json
@@ -0,0 +1,9 @@
+{
+ "timescale": "GPST",
+ "interp_order": 11,
+ "max_sv": 4,
+ "modeling": {
+ "sv_clock_bias": true,
+ "sv_total_group_delay": true
+ }
+}
diff --git a/rinex-cli/doc/file-combination.md b/rinex-cli/doc/file-combination.md
index 3e9cf5eaa..de3325969 100644
--- a/rinex-cli/doc/file-combination.md
+++ b/rinex-cli/doc/file-combination.md
@@ -100,10 +100,25 @@ joint `--nav` and `--sp3` context yourself.
## IONEX analysis
-To analyze a IONEX file, a primary file of this type should be passed
-to `--fp` (or `-f`). In this case, you get a world map visualization
-of the provided TEC map. Unfortunately we can only visualize the TEC map
-at a single epoch, because we cannot animate the world map at the moment.
-Therefore, it makes sense to zoom in on the Epoch you're interested in,
-with the proper `-P` preprocessor command. Refer to related section.
+IONEX is one of those files that can only serve as primary files.
+Thefore all IONEX files should be passed with `--fp` (`-f`).
+We can then plot the TEC map. Unfortunately we have no means to animate the plot
+at the moment, so we create a TEC visualization for every single Epochs.
+Usually IONEX files comprise 12 to 24 Epochs, so it's not that much but the HTML
+graphs might come heavy.
+
+We recommend zooming on the time frame you're interested in, for example with something like this
+
+```bash
+./target/release/rinex-cli \
+ -f CKMG0090.21I.gz --epochs
+
+["2021-01-09T00:00:00 UTC","2021-01-09T01:00:00 UTC", ..., "2021-01-10T00:00:00 UTC"]
+
+./target/release/rinex-cli \
+ -f CKMG0090.21I.gz \
+ -P ">=2021-01-09T19:00:00 UTC"
+```
+
+
diff --git a/rinex-cli/doc/positioning.md b/rinex-cli/doc/positioning.md
deleted file mode 100644
index 94f241f22..000000000
--- a/rinex-cli/doc/positioning.md
+++ /dev/null
@@ -1,68 +0,0 @@
-Position solver
-===============
-
-The position solver is currently an "advanced" SPP solver.
-SPP stands for Single Frequency Precice Point solver which means
-you get a precise point location (ideally with metric accuracy) for a minimal
-- down to single frequency data context.
-
-When we say "advanced" SPP it means it supports more than the minimal prerequisites
-for a pure SPP solver. For example it is possible to still require SPP solving
-but use other criteria that makes it a little closer to PPP.
-
-Command line interface
-======================
-
-* use `-p` to request position solving.
-From the provided data context, we will try to evaluate the user position
-the best we can
-
-* use `--spp` to force to SPP solving.
-
-* `--ppp` to force to PPP solving. It exists but not entirely supported to this day.
-
-Minimal data context
-====================
-
-A minimum of one primary RINEX Observation file with broadcast Ephemeris
-valid for that particular time frame is required.
-
-SP3 can be stacked, broadcast Ephemeris are still required, we will prefer SP3
-for certain steps in the solving process.
-
-Example of minimum requirement :
-
-```bash
-./target/release/rinex-cli -P GPS,GLO --spp \
- --fp DATA/2023/OBS/256/ANK200TUR_S_20232560000_01D_30S_MO.crx \
- --nav DATA/2023/NAV/255 \
- --nav DATA/2023/NAV/256
-```
-
-Example of SP3 extension :
-
-```bash
-./target/release/rinex-cli -P GPS,GLO --spp \
- --fp DATA/2023/OBS/256/ANK200TUR_S_20232560000_01D_30S_MO.crx \
- --nav DATA/2023/NAV/255 \
- --nav DATA/2023/NAV/256 \
- --sp3 DATA/2023/SP3/255 \
- --sp3 DATA/2023/SP3/256
-```
-
-Position solver and results
-===========================
-
-The solver will try to resolve the navigation equations for every single Epoch
-for which :
-
-* enough raw GNSS signals were observed in the Observation RINEX
-* enough SV fit the Navigation requirements
-* all minimal or requested models were correctly modelized
-
-The solver can totally work with its default configuration, as long as the previous points stand.
-But you need to understand that in this configuration, you can't hope for an optimal result accuracy.
-
-Mastering and operating a position solver is a complex task.
-To fully understand what can be achieved and how to achieve such results,
-refer to the [gnss-rtk](../gnss-rtk/README.md) library documentation.
diff --git a/rinex-cli/doc/preprocessing.md b/rinex-cli/doc/preprocessing.md
index 03986f4c8..cecc46fe0 100644
--- a/rinex-cli/doc/preprocessing.md
+++ b/rinex-cli/doc/preprocessing.md
@@ -72,18 +72,20 @@ advanced mask filters.
## Stacked preprocessing ops
-A whitespace separates two preprocessing operations.
+A whitespace separates two preprocessing operations (ie., two sets of CSV).
+Therefore it is considered as two separate filters. For example here, we're only left with
+G08 and R03 data.
```bash
rinex-cli \
--fp test_resources/CRNX/V3/ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz \
- -P GPS,GLO G08,R03
+ -P GPS,GLO,BDS G08,R03
```
-Therefore, if a filter operation involves a whitespace, it requires to be wrapped
+If one opeation requies a whitespace, it needs to be wrapped
in between inverted commas. Most common example is the [Epoch](epoch-target) description.
-## Epoch target
+## Epoch filter
Any valid Hifitime::Epoch string description is supported.
@@ -105,11 +107,7 @@ rinex-cli \
-P ">2020-06-12T08:00:00 UTC" "<=2020-06-25T16:00:00 UTC" GPS >G08
```
-## Duration target
-
-TODO
-
-## Sv target
+## SV filter
A comma separated list of Sv (of any length) is supported.
For example, retain _R03_ and _E10_ with the following:
@@ -121,8 +119,8 @@ rinex-cli \
```
`Sv` target is the only one amongst CSV arrays that supports more than "=" or "!=" operands.
-For example we can select PRN above 08 for GPS and below 10 for Galileo constellations (only, others are untouched)
-with this command:
+This is used to filter on SV PRN.
+For example here we can select PRN above 08 for GPS and below (included) 10 for Galileo:
```bash
rinex-cli \
@@ -130,25 +128,66 @@ rinex-cli \
-P >G08 "<=E10"
```
-## Constellations
+## Constellations filter
+
+Retain specific constellations. For example we only retain GPS with this:
+
+```bash
+rinex-cli \
+ --fp test_resources/CRNX/V3/ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz \
+ -P GPS
+```
-A comma separated list of Constellations is supported.
-For example, with the following, we are left with data from Glonass and GPS
+You can stack as many filters as you want, using csv. For example, retain
+BeiDou also:
```bash
rinex-cli \
--fp test_resources/CRNX/V3/ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz \
- -P !=BDS GPS,GLO # ineq(BDS) AND eq(GPS,GLO)
+ -P GPS,BDS
```
-`teqc` like quick GNSS filters also exist:
+Inequality is also supported. For example: retain everything but Glonass
-- `-G` to remove GPS
-- `-C` to remove BDS
-- `-E` to remove Galileo
-- `-R` to remove Glonnass
-- `-J` to remove QZSS
-- `-S` to remove SBAS vehicles
+```bash
+rinex-cli \
+ --fp test_resources/CRNX/V3/ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz \
+ -P !=GLO
+```
+
+SBAS is a special case. If you use "SBAS", you can retain or discard
+SBAS systems, whatever their actual constellation. For example we
+retain all GPS and any SBAS with this:
+
+```bash
+rinex-cli \
+ --fp test_resources/CRNX/V3/ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz -P GPS,SBAS
+```
+
+If you want to retain specific SBAS, you have to name them precisely, we support all of them
+(see Constellation module API). For example, retain GPS, EGNOS and SDCM with this:
+
+```bash
+rinex-cli \
+ --fp test_resources/CRNX/V3/ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz -P GPS,EGNOS,SDCM
+```
+
+Note that the following `teqc` equivalent filters are also supported.
+
+- `-G` removes GPS (equivalent to `-P !=GPS`)
+- `-C` removes BDS
+- `-E` removes Galileo
+- `-R` removes Glonnass
+- `-J` removes QZSS
+- `-S` removes all SBAS vehicles
+
+If you want to remove specific SBAS constellations, for example EGNOS, you have to use
+`-P`:
+
+```bash
+rinex-cli \
+ --fp test_resources/CRNX/V3/ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz -P !=EGNOS
+```
## Observables
diff --git a/rinex-cli/doc/qc.md b/rinex-cli/doc/qc.md
index dd560ca9f..52da524ec 100644
--- a/rinex-cli/doc/qc.md
+++ b/rinex-cli/doc/qc.md
@@ -1,7 +1,7 @@
Quality Check (QC)
==================
-RINEX quality check is a special mode, activated with `--qc`.
+RINEX quality check is a special mode. It is activated with `--qc` and is turned off by default.
QC is first developed for Observation files analysis, but this tool
will accept other RINEX files, for which it will compute basic statistical analysis.
@@ -85,7 +85,7 @@ Run this configuration for the most basic QC:
rinex-cli \
-P GPS,GLO \
--qc-only \
- --qc-cfg rinex-cli/config/gnss_snr30db.json \
+ --qc-cfg rinex-cli/config/qc/gnss_snr30db.json \
--fp test_resources/CRNX/V3/ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz
```
@@ -134,7 +134,7 @@ To this one :
./target/release/rinex-cli \
--fp test_resources/CRNX/V3/MOJN00DNK_R_20201770000_01D_30S_MO.crx.gz \
-P G08,G15,G16,R23,C19,C09 \
- --qc --qc-cfg rinex-cli/config/sv_manual_gap.json
+ --qc --qc-cfg rinex-cli/config/qc/sv_manual_gap.json
```
### SNR parametrization
@@ -157,7 +157,7 @@ the go out of sight more rapidly, due to the stringent elevation criteria :
rinex-cli \
-P gps,glo \
-P G08,G15,G16,R23,C19,C09 \
- --qc --qc-cfg rinex-cli/config/sv_manual_gap_ev35.json
+ --qc --qc-cfg rinex-cli/config/qc/sv_manual_gap_ev35.json
--fp test_resources/CRNX/V3/ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz \
--nav test_resources/OBS/V3/ESBC00DNK_R_20201770000_01D_MN.rnx.gz
```
diff --git a/rinex-cli/doc/rtk.md b/rinex-cli/doc/rtk.md
new file mode 100644
index 000000000..b48033ec1
--- /dev/null
+++ b/rinex-cli/doc/rtk.md
@@ -0,0 +1,128 @@
+RTK solver
+==========
+
+RTK mode is requested with `-r` or `--rtk`.
+
+RTK (position solving) is feasible if you provide at least RINEX Observations
+(`-f`) and overlapping RINEX Navigation data (`--nav`).
+
+Currently it is also mandatory to provide overlapping SP3 with `--sp3` but that should be fixed
+in near future.
+
+As an example (this dataset is not provided), the most basic command line would look like this,
+where observations are imported for day 256 and we combine several NAV/SP3 by lazyili importing entire folders:
+
+```bash
+./target/release/rinex-cli -P GPS,GLO -r \
+ --fp DATA/2023/OBS/256/ANK200TUR_S_20232560000_01D_30S_MO.crx \
+ --nav DATA/2023/NAV/256 \
+ --sp3 DATA/2023/SP3/256
+```
+
+Current limitations
+===================
+
+Several limitations exit to this day and must be kept in mind.
+
+- Glonass and SBAS vehicles cannot be pushed into the pool of eligible vehicles.
+Until further notice, one must combine -R and -S to the rtk mode.
+
+- We've only tested the solver against mixed GPS, Galileo and BeiDou vehicles
+
+- We only support GPST, GST and BDT. QZSST is expressed as GPST and I'm not 100% sure this
+is correct.
+
+- The estimated clock offset is expressed against the timescale for which the Observation file is referenced to.
+We don't have the flexibility to change that at the moment.
+So far the solver has only be tested against Observations referenced against GPST.
+
+RTK (only)
+==========
+
+Use `-r` (or `--rtk-only`) to disable other opmodes. This gives you the quickest results.
+
+```bash
+./target/release/rinex-cli -R -S -r \
+ --fp DATA/2023/OBS/256/ANK200TUR_S_20232560000_01D_30S_MO.crx \
+ --nav DATA/2023/NAV/256 \
+ --sp3 DATA/2023/SP3/256
+```
+
+RTK configuration
+=================
+
+The solver can be customized, either to improve performances
+or improve the final resolution. Refer to the library section
+that defines the [RTK configuration](https://github.com/georust/rinex/gnss-rtk/doc/cfg.md)
+to understand the physics and what they imply on the end result.
+
+A few configuration files are provided in the rinex-cli/config/rtk directory.
+
+You can use them with `--rtk-cfg`:
+
+Forced SPP mode
+===============
+
+By default the solver will adapt to the provided context and will deploy the best strategy.
+
+You can force the strategy to SPP with `--spp`
+
+It is possible to use the configuration file, even in forced SPP mode, to improve the end results:
+
+In this scenario, one wants to define Ionospheric delay model
+
+Provide SP3
+===========
+
+When SP3 is provided, they are prefered over NAV RINEX.
+Refer to the library documentation [TODO](TODO)
+
+Example of SP3 extension :
+
+```bash
+./target/release/rinex-cli -R -S --spp \
+ --fp DATA/2023/OBS/256/ANK200TUR_S_20232560000_01D_30S_MO.crx \
+ --nav DATA/2023/NAV/255 \
+ --nav DATA/2023/NAV/256 \
+ --sp3 DATA/2023/SP3/255 \
+ --sp3 DATA/2023/SP3/256
+```
+
+It is totally possible to combine SP3 to a single frequency context,
+or a forced --spp strategy.
+
+Results
+=======
+
+The solver will try to resolve the navigation equations for every single Epoch
+for which :
+
+* enough raw GNSS signals were observed in the Observation RINEX
+* enough SV fit the Navigation requirements
+* all minimal or requested models were correctly modelized
+
+The solver can totally work with its default configuration, as long as the previous points stand.
+But you need to understand that in this configuration, you can't hope for an optimal result accuracy.
+
+Mastering and operating a position solver is a complex task.
+To fully understand what can be achieved and how to achieve such results,
+refer to the [gnss-rtk](../gnss-rtk/README.md) library documentation.
+
+RTK and logger
+==============
+
+The RTK solver and its dependencies, make extensive use of the Rust logger.
+Turn it on so you have meaningful information on what is happening:
+
+- Epochs for which we perform the calculations
+- Navigation context evolution
+- Results and meaningful information
+- More information on the configuration and what can be achieved
+
+The Rust logger sensitivity is controlled by the RUST\_LOG environment variable,
+which you can either export or adjust for a single run. `trace` is the most sensitive,
+`info` is the standard value.
+
+The output is directed towards Stdout, therefore it can be streamed into a text file for example,
+to easily compare runs between them.
+
diff --git a/rinex-cli/src/cli.rs b/rinex-cli/src/cli.rs
index 5b8d79c19..b287da5ed 100644
--- a/rinex-cli/src/cli.rs
+++ b/rinex-cli/src/cli.rs
@@ -1,4 +1,5 @@
use clap::{Arg, ArgAction, ArgMatches, ColorChoice, Command};
+use gnss_rtk::prelude::RTKConfig;
use log::{error, info};
use rinex::prelude::*;
use rinex_qc::QcOpts;
@@ -27,8 +28,8 @@ impl Cli {
.long("fp")
.value_name("FILE")
.help("Input RINEX file. Serves as primary data.
-In advanced usage, this must be Observation Data.
-Observation, Meteo and IONEX, can only serve as primary data.")
+Must be Observation Data for --rtk.
+Observation, Meteo and IONEX can only serve as primary data.")
.action(ArgAction::Append)
.required(true))
.next_help_heading("General")
@@ -37,8 +38,9 @@ Observation, Meteo and IONEX, can only serve as primary data.")
.long("quiet")
.action(ArgAction::SetTrue)
.help("Disable all terminal output. Also disables auto HTML reports opener."))
- .arg(Arg::new("readable")
- .short('r')
+ .arg(Arg::new("pretty")
+ .short('p')
+ .long("pretty")
.action(ArgAction::SetTrue)
.help("Make terminal output more readable."))
.arg(Arg::new("workspace")
@@ -48,15 +50,19 @@ Observation, Meteo and IONEX, can only serve as primary data.")
.help("Customize workspace location (folder does not have to exist).
The default workspace is rinex-cli/workspace"))
.next_help_heading("Data identification")
+ .arg(Arg::new("full-id")
+ .short('i')
+ .action(ArgAction::SetTrue)
+ .help("Turn all identifications ON"))
.arg(Arg::new("epochs")
.long("epochs")
.action(ArgAction::SetTrue)
.help("Enumerate all epochs"))
- .arg(Arg::new("constellations")
- .long("constellations")
- .short('c')
+ .arg(Arg::new("gnss")
+ .long("gnss")
+ .short('g')
.action(ArgAction::SetTrue)
- .help("Enumerate GNSS constellations"))
+ .help("Enumerate GNSS constellations present in entire context."))
.arg(Arg::new("sv")
.long("sv")
.action(ArgAction::SetTrue)
@@ -109,6 +115,7 @@ Useful to determine common Epochs or compare sample rates in between
.num_args(1..)
.help("Design preprocessing operations, like data filtering or resampling,
prior further analysis. You can stack as many ops as you need.
+Preprocessing ops apply prior entering both -q and --rtk modes.
Refer to rinex-cli/doc/preprocessing.md to learn how to operate this interface."))
.next_help_heading("Observation RINEX")
.arg(Arg::new("observables")
@@ -250,27 +257,38 @@ The summary report by default is integrated to the global HTML report."))
.long("qc-only")
.action(ArgAction::SetTrue)
.help("Activates QC mode and disables all other features: quickest qc rendition."))
- .next_help_heading("Position Solver")
- .arg(Arg::new("positioning")
- .short('p')
- .long("positioning")
+ .next_help_heading("RTK (Positioning)")
+ .arg(Arg::new("rtk")
+ .long("rtk")
.action(ArgAction::SetTrue)
.help("Activate GNSS receiver position solver.
This is only possible if provided context is sufficient.
Depending on provided context, either SPP (high accuracy) or PPP (ultra high accuracy)
-method is deployed.
-This is turned of by default, because it involves quite heavy computations.
+solver is deployed.
+This mode is turned off by default because it involves quite heavy computations.
+Use the RUST_LOG env. variable for verbosity.
See [spp] for more information. "))
.arg(Arg::new("spp")
.long("spp")
.action(ArgAction::SetTrue)
.help("Enables Positioning forced to Single Frequency SPP solver mode.
Disregards whether the provided context is PPP compatible.
-NB: we do not account for Relativistic effects in clock bias estimates."))
- .arg(Arg::new("positioning-only")
- .long("pos-only")
+NB: we do not account for Relativistic effects by default and raw pseudo range are used.
+For indepth customization, refer to the configuration file and online documentation."))
+ .arg(Arg::new("rtk-only")
+ .long("rtk-only")
+ .short('r')
.action(ArgAction::SetTrue)
- .help("Activates GNSS position solver, disables all other modes: most performant solver."))
+ .help("Activates GNSS position solver, disables all other modes.
+This is the most performant mode to solve a position."))
+ .arg(Arg::new("rtk-config")
+ .long("rtk-cfg")
+ .value_name("FILE")
+ .help("Pass RTK custom configuration."))
+ .arg(Arg::new("kml")
+ .long("kml")
+ .help("Form a KML track with resolved positions.
+This turns off the default visualization."))
.next_help_heading("File operations")
.arg(Arg::new("merge")
.short('m')
@@ -394,6 +412,7 @@ Refer to README"))
| self.matches.get_flag("orbits")
| self.matches.get_flag("nav-msg")
| self.matches.get_flag("anomalies")
+ | self.matches.get_flag("full-id")
}
/// Returns true if Sv accross epoch display is requested
pub fn sv_epoch(&self) -> bool {
@@ -413,38 +432,52 @@ Refer to README"))
}
/// Returns list of requested data to extract
pub fn identification_ops(&self) -> Vec<&str> {
- let flags = vec![
- "sv",
- "epochs",
- "header",
- "constellations",
- "observables",
- "ssi-range",
- "ssi-sv-range",
- "orbits",
- "nav-msg",
- "anomalies",
- ];
- flags
- .iter()
- .filter(|x| self.matches.get_flag(x))
- .map(|x| *x)
- .collect()
+ if self.matches.get_flag("full-id") {
+ vec![
+ "sv",
+ "epochs",
+ "gnss",
+ "observables",
+ "ssi-range",
+ "ssi-sv-range",
+ "orbits",
+ "nav-msg",
+ "anomalies",
+ ]
+ } else {
+ let flags = vec![
+ "sv",
+ "header",
+ "epochs",
+ "gnss",
+ "observables",
+ "ssi-range",
+ "ssi-sv-range",
+ "orbits",
+ "nav-msg",
+ "anomalies",
+ ];
+ flags
+ .iter()
+ .filter(|x| self.matches.get_flag(x))
+ .map(|x| *x)
+ .collect()
+ }
}
fn get_flag(&self, flag: &str) -> bool {
self.matches.get_flag(flag)
}
/// returns true if pretty JSON is requested
- pub fn readable_json(&self) -> bool {
- self.get_flag("readable")
+ pub fn pretty(&self) -> bool {
+ self.get_flag("pretty")
}
/// Returns true if quiet mode is activated
pub fn quiet(&self) -> bool {
self.matches.get_flag("quiet")
}
/// Returns true if position solver is enabled
- pub fn positioning(&self) -> bool {
- self.matches.get_flag("positioning") || self.forced_spp() || self.forced_ppp()
+ pub fn rtk(&self) -> bool {
+ self.matches.get_flag("rtk") || self.forced_spp() || self.forced_ppp()
}
/// Returns true if position solver forced to SPP
pub fn forced_spp(&self) -> bool {
@@ -454,8 +487,26 @@ Refer to README"))
pub fn forced_ppp(&self) -> bool {
self.matches.get_flag("spp")
}
- pub fn positioning_only(&self) -> bool {
- self.matches.get_flag("positioning-only")
+ pub fn rtk_only(&self) -> bool {
+ self.matches.get_flag("rtk-only")
+ }
+ pub fn rtk_config(&self) -> Option {
+ if let Some(path) = self.matches.get_one::("rtk-config") {
+ if let Ok(content) = std::fs::read_to_string(path) {
+ let opts = serde_json::from_str(&content);
+ if let Ok(opts) = opts {
+ info!("loaded rtk config: \"{}\"", path);
+ return Some(opts);
+ } else {
+ error!("failed to parse config file \"{}\"", path);
+ info!("using default parameters");
+ }
+ } else {
+ error!("failed to read config file \"{}\"", path);
+ info!("using default parameters");
+ }
+ }
+ None
}
pub fn cs_graph(&self) -> bool {
self.matches.get_flag("cs")
diff --git a/rinex-cli/src/identification.rs b/rinex-cli/src/identification.rs
index 53e2e50ae..491a5f311 100644
--- a/rinex-cli/src/identification.rs
+++ b/rinex-cli/src/identification.rs
@@ -1,4 +1,6 @@
use crate::Cli;
+use hifitime::Epoch;
+use rinex::observation::Snr;
use rinex::*;
use rinex_qc::QcContext;
@@ -6,70 +8,109 @@ use rinex_qc::QcContext;
* Basic identification operations
*/
pub fn rinex_identification(ctx: &QcContext, cli: &Cli) {
- let pretty = cli.readable_json();
+ let pretty = cli.pretty();
let ops = cli.identification_ops();
- identification(&ctx.primary_data(), pretty, ops.clone());
+ identification(
+ &ctx.primary_data(),
+ &ctx.primary_path().to_string_lossy().to_string(),
+ pretty,
+ ops.clone(),
+ );
+
if let Some(nav) = &ctx.navigation_data() {
- identification(&nav, pretty, ops.clone());
+ identification(&nav, "Navigation Context blob", pretty, ops.clone());
}
}
-fn identification(rnx: &Rinex, pretty: bool, ops: Vec<&str>) {
+use serde::Serialize;
+
+#[derive(Clone, Debug, Serialize)]
+struct EpochReport {
+ pub first: String,
+ pub last: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+struct SSIReport {
+ pub min: Option,
+ pub max: Option,
+}
+
+fn identification(rnx: &Rinex, path: &str, pretty: bool, ops: Vec<&str>) {
for op in ops {
+ debug!("identification: {}", op);
if op.eq("header") {
let content = match pretty {
true => serde_json::to_string_pretty(&rnx.header).unwrap(),
false => serde_json::to_string(&rnx.header).unwrap(),
};
- println!("{}", content);
+ println!("[{}]: {}", path, content);
} else if op.eq("epochs") {
- let data: Vec = rnx.epoch().map(|e| e.to_string()).collect();
- let content = match pretty {
- true => serde_json::to_string_pretty(&data).unwrap(),
- false => serde_json::to_string(&data).unwrap(),
- };
- println!("{}", content);
- } else if op.eq("sv") {
- let data: Vec<_> = rnx.sv().collect();
- let content = match pretty {
- true => serde_json::to_string_pretty(&data).unwrap(),
- false => serde_json::to_string(&data).unwrap(),
+ let report = EpochReport {
+ first: format!("{:?}", rnx.first_epoch()),
+ last: format!("{:?}", rnx.last_epoch()),
};
- println!("{}", content);
- } else if op.eq("observables") {
- let data: Vec<_> = rnx.observable().collect();
let content = match pretty {
- true => serde_json::to_string_pretty(&data).unwrap(),
- false => serde_json::to_string(&data).unwrap(),
+ true => serde_json::to_string_pretty(&report).unwrap(),
+ false => serde_json::to_string(&report).unwrap(),
};
- println!("{}", content);
+ println!("[{}]: {}", path, content);
+ } else if op.eq("sv") {
+ let mut csv = String::new();
+ for (i, sv) in rnx.sv().enumerate() {
+ if i == rnx.sv().count() - 1 {
+ csv.push_str(&format!("{}\n", sv.to_string()));
+ } else {
+ csv.push_str(&format!("{}, ", sv.to_string()));
+ }
+ }
+ println!("[{}]: {}", path, csv);
+ } else if op.eq("observables") && rnx.is_observation_rinex() {
+ let mut data: Vec<_> = rnx.observable().collect();
+ data.sort();
+ //let content = match pretty {
+ // true => serde_json::to_string_pretty(&data).unwrap(),
+ // false => serde_json::to_string(&data).unwrap(),
+ //};
+ println!("[{}]: {:?}", path, data);
} else if op.eq("gnss") {
let data: Vec<_> = rnx.constellation().collect();
let content = match pretty {
true => serde_json::to_string_pretty(&data).unwrap(),
false => serde_json::to_string(&data).unwrap(),
};
- println!("{}", content);
- } else if op.eq("ssi-range") {
- let data = &rnx.observation_ssi_minmax();
+ println!("[{}]: {}", path, content);
+ } else if op.eq("ssi-range") && rnx.is_observation_rinex() {
+ let ssi = SSIReport {
+ min: {
+ rnx.snr()
+ .min_by(|(_, _, _, snr_a), (_, _, _, snr_b)| snr_a.cmp(snr_b))
+ .map(|(_, _, _, snr)| snr)
+ },
+ max: {
+ rnx.snr()
+ .max_by(|(_, _, _, snr_a), (_, _, _, snr_b)| snr_a.cmp(snr_b))
+ .map(|(_, _, _, snr)| snr)
+ },
+ };
let content = match pretty {
- true => serde_json::to_string_pretty(data).unwrap(),
- false => serde_json::to_string(data).unwrap(),
+ true => serde_json::to_string_pretty(&ssi).unwrap(),
+ false => serde_json::to_string(&ssi).unwrap(),
};
- println!("{}", content);
- } else if op.eq("orbits") {
- unimplemented!("nav::orbits");
+ println!("[{}]: {}", path, content);
+ } else if op.eq("orbits") && rnx.is_navigation_rinex() {
+ error!("nav::orbits not available yet");
//let data: Vec<_> = rnx.orbit_fields();
//let content = match pretty {
// true => serde_json::to_string_pretty(&data).unwrap(),
// false => serde_json::to_string(&data).unwrap(),
//};
//println!("{}", content);
- } else if op.eq("nav-msg") {
+ } else if op.eq("nav-msg") && rnx.is_navigation_rinex() {
let data: Vec<_> = rnx.nav_msg_type().collect();
println!("{:?}", data);
- } else if op.eq("anomalies") {
+ } else if op.eq("anomalies") && rnx.is_observation_rinex() {
let data: Vec<_> = rnx.epoch_anomalies().collect();
println!("{:#?}", data);
}
diff --git a/rinex-cli/src/main.rs b/rinex-cli/src/main.rs
index c63fd5890..e0cadfcef 100644
--- a/rinex-cli/src/main.rs
+++ b/rinex-cli/src/main.rs
@@ -14,14 +14,14 @@ use preprocessing::preprocess;
//use horrorshow::Template;
use rinex::{
merge::Merge,
- observation::{Combine, Dcb, IonoDelay, Mp},
+ observation::{Combine, Dcb, IonoDelay}, //Mp},
prelude::RinexType,
prelude::*,
split::Split,
};
extern crate gnss_rtk as rtk;
-use rtk::prelude::{Solver, SolverOpts, SolverType};
+use rtk::prelude::{Solver, SolverError, SolverEstimate, SolverType};
use rinex_qc::*;
@@ -29,8 +29,8 @@ use cli::Cli;
use identification::rinex_identification;
use plot::PlotContext;
-extern crate pretty_env_logger;
-use pretty_env_logger::env_logger::Builder;
+//extern crate pretty_env_logger;
+use env_logger::{Builder, Target};
#[macro_use]
extern crate log;
@@ -299,7 +299,7 @@ fn create_context(cli: &Cli) -> QcContext {
* Returns true if Skyplot view if feasible
*/
fn skyplot_allowed(ctx: &QcContext, cli: &Cli) -> bool {
- if cli.quality_check_only() || cli.positioning_only() {
+ if cli.quality_check_only() || cli.rtk_only() {
/*
* Special modes: no plots allowed
*/
@@ -318,6 +318,7 @@ fn skyplot_allowed(ctx: &QcContext, cli: &Cli) -> bool {
pub fn main() -> Result<(), rinex::Error> {
let mut builder = Builder::from_default_env();
builder
+ .target(Target::Stdout)
.format_timestamp_secs()
.format_module_path(false)
.init();
@@ -329,8 +330,8 @@ pub fn main() -> Result<(), rinex::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;
+ let rtk_only = cli.rtk_only();
+ let rtk = cli.rtk() || rtk_only;
// Initiate plot context
let mut plot_ctx = PlotContext::new();
@@ -343,12 +344,43 @@ pub fn main() -> Result<(), rinex::Error> {
// Position solver
let mut solver = Solver::from(&ctx);
+ if let Ok(ref mut solver) = solver {
+ info!(
+ "provided context is compatible with {} position solver",
+ solver.solver
+ );
+ // custom config ? apply it
+ if let Some(cfg) = cli.rtk_config() {
+ solver.cfg = cfg.clone();
+ }
+ if !rtk {
+ warn!("position solver currently turned off");
+ } else {
+ if cli.forced_spp() {
+ warn!("forced method to spp");
+ solver.solver = SolverType::SPP;
+ }
+ // print config to be used
+ info!("{:#?}", solver.cfg);
+ }
+ } else {
+ warn!("context is not sufficient or not compatible with --rtk");
+ }
// Workspace
let workspace = workspace_path(&ctx);
info!("workspace is \"{}\"", workspace.to_string_lossy());
create_workspace(workspace.clone());
+ /*
+ * Print more info on special primary data cases
+ */
+ if ctx.primary_data().is_meteo_rinex() {
+ info!("meteo special primary data");
+ } else if ctx.primary_data().is_ionex() {
+ info!("ionex special primary data");
+ }
+
/*
* Emphasize which reference position is to be used.
* This will help user make sure everything is correct.
@@ -364,30 +396,6 @@ pub fn main() -> Result<(), rinex::Error> {
} else {
info!("no reference position given or identified");
}
- /*
- * print more info on possible solver to deploy
- */
- if let Ok(ref mut solver) = solver {
- info!(
- "provided context is compatible with {} position solver",
- solver.solver
- );
- if !positioning {
- warn!("position solver currently turned off");
- } else {
- if cli.forced_spp() {
- solver.solver = SolverType::SPP;
- solver.opts = SolverOpts::default(SolverType::SPP);
- warn!("position solver restricted to SPP mode");
- } else if cli.forced_ppp() {
- solver.solver = SolverType::PPP;
- solver.opts = SolverOpts::default(SolverType::PPP);
- warn!("position solver forced to PPP mode");
- }
- }
- } else {
- info!("context is not sufficient for any position solving method");
- }
/*
* Preprocessing
*/
@@ -609,14 +617,14 @@ pub fn main() -> Result<(), rinex::Error> {
* Record analysis / visualization
* analysis depends on the provided record type
*/
- if !qc_only && !positioning_only {
+ if !qc_only && !rtk_only {
info!("entering record analysis");
plot::plot_record(&ctx, &mut plot_ctx);
}
/*
* Render Graphs (HTML)
*/
- if !qc_only && !positioning_only {
+ if !qc_only && !rtk_only {
let html_path = workspace_path(&ctx).join("graphs.html");
let html_path = html_path.to_str().unwrap();
@@ -673,13 +681,43 @@ pub fn main() -> Result<(), rinex::Error> {
}
if let Ok(ref mut solver) = solver {
// position solver is feasible, with provided context
- if positioning {
- info!("entering positioning mode\n");
- while let Some((t, estimate)) = solver.run(&mut ctx) {
- trace!("epoch: {}", t);
- // info!("%%%%%%%%% Iteration : {} %%%%%%%%%%%", iteration +1);
- //info!("%%%%%%%%% Position : {:?}, Time: {:?}", position, time);
- // iteration += 1;
+ let mut solving = true;
+ let mut results: HashMap = HashMap::new();
+
+ if rtk {
+ match solver.init(&mut ctx) {
+ Err(e) => panic!("failed to initialize rtk solver"),
+ Ok(_) => info!("entering rtk mode"),
+ }
+ while solving {
+ match solver.run(&mut ctx) {
+ Ok((t, estimate)) => {
+ trace!(
+ "epoch: {}
+position error: {:.6E}, {:.6E}, {:.6E}
+HDOP {:.5E} | VDOP {:.5E}
+clock offset: {:.6E} | TDOP {:.5E}",
+ t,
+ estimate.dx,
+ estimate.dy,
+ estimate.dz,
+ estimate.hdop,
+ estimate.vdop,
+ estimate.dt,
+ estimate.tdop
+ );
+ results.insert(t, estimate);
+ },
+ Err(SolverError::NoSv(t)) => info!("no SV elected @{}", t),
+ Err(SolverError::LessThan4Sv(t)) => info!("less than 4 SV @{}", t),
+ Err(SolverError::SolvingError(t)) => {
+ error!("failed to invert navigation matrix @ {}", t)
+ },
+ Err(SolverError::EpochDetermination(_)) => {
+ solving = false; // abort
+ },
+ Err(e) => panic!("fatal error {:?}", e),
+ }
}
info!("done");
}
diff --git a/rinex-cli/src/plot/context.rs b/rinex-cli/src/plot/context.rs
index 01e597d0f..6ebaa4502 100644
--- a/rinex-cli/src/plot/context.rs
+++ b/rinex-cli/src/plot/context.rs
@@ -30,8 +30,16 @@ impl PlotContext {
pub fn add_polar2d_plot(&mut self, title: &str) {
self.plots.push(build_default_polar_plot(title));
}
- pub fn add_world_map(&mut self, style: MapboxStyle, center: (f64, f64), zoom: u8) {
- self.plots.push(build_world_map(style, center, zoom));
+ pub fn add_world_map(
+ &mut self,
+ title: &str,
+ show_legend: bool,
+ map_style: MapboxStyle,
+ center: (f64, f64),
+ zoom: u8,
+ ) {
+ self.plots
+ .push(build_world_map(title, show_legend, map_style, center, zoom));
}
pub fn add_trace(&mut self, trace: Box) {
let len = self.plots.len() - 1;
diff --git a/rinex-cli/src/plot/mod.rs b/rinex-cli/src/plot/mod.rs
index 3c3f3ee57..bbc3bcae1 100644
--- a/rinex-cli/src/plot/mod.rs
+++ b/rinex-cli/src/plot/mod.rs
@@ -279,14 +279,22 @@ pub fn build_default_polar_plot(title: &str) -> Plot {
* centered on given locations, in decimal degrees,
* zoom factor
*/
-pub fn build_world_map(style: MapboxStyle, center: (f64, f64), zoom: u8) -> Plot {
+pub fn build_world_map(
+ title: &str,
+ show_legend: bool,
+ map_style: MapboxStyle,
+ center: (f64, f64),
+ zoom: u8,
+) -> Plot {
let mut p = Plot::new();
let layout = Layout::new()
+ .title(Title::new(title).font(Font::default()))
.drag_mode(DragMode::Zoom)
.margin(Margin::new().top(0).left(0).bottom(0).right(0))
+ .show_legend(show_legend)
.mapbox(
Mapbox::new()
- .style(style)
+ .style(map_style)
.center(Center::new(center.0, center.1))
.zoom(zoom),
);
@@ -377,7 +385,7 @@ pub fn build_chart_epoch_axis(
let txt: Vec = epochs.iter().map(|e| e.to_string()).collect();
Scatter::new(epochs.iter().map(|e| e.to_utc_seconds()).collect(), data_y)
.mode(mode)
- .web_gl_mode(true)
+ //.web_gl_mode(true)
.name(name)
.hover_text_array(txt)
.hover_info(HoverInfo::All)
@@ -392,7 +400,7 @@ pub fn plot_record(ctx: &QcContext, plot_ctx: &mut PlotContext) {
} else if ctx.primary_data().is_meteo_rinex() {
record::plot_meteo(ctx, plot_ctx);
} else if ctx.primary_data().is_ionex() {
- if let Some(borders) = ctx.primary_data().ionex_map_borders() {
+ if let Some(borders) = ctx.primary_data().tec_map_borders() {
record::plot_tec_map(ctx, borders, plot_ctx);
}
}
diff --git a/rinex-cli/src/plot/record/ionex.rs b/rinex-cli/src/plot/record/ionex.rs
index 456145846..8160e8da6 100644
--- a/rinex-cli/src/plot/record/ionex.rs
+++ b/rinex-cli/src/plot/record/ionex.rs
@@ -1,13 +1,9 @@
//use itertools::Itertools;
use crate::plot::PlotContext;
-use plotly::{
- color::NamedColor,
- common::{Marker, MarkerSymbol}, //color::Rgba},
- layout::MapboxStyle,
- //scatter_mapbox::Fill,
- ScatterMapbox,
-};
+use plotly::layout::MapboxStyle;
+use rinex::prelude::Epoch;
use rinex_qc::QcContext;
+use std::collections::HashMap;
pub fn plot_tec_map(
ctx: &QcContext,
@@ -15,73 +11,62 @@ pub fn plot_tec_map(
plot_ctx: &mut PlotContext,
) {
let _cmap = colorous::TURBO;
- plot_ctx.add_world_map(MapboxStyle::OpenStreetMap, (32.5, -40.0), 1);
+ //let hover_text: Vec = ctx.primary_data().epoch().map(|e| e.to_string()).collect();
+ /*
+ * TEC map visualization
+ * plotly-rs has no means to animate plots at the moment
+ * therefore.. we create one plot for all existing epochs
+ */
+ for (_index, epoch) in ctx.primary_data().epoch().enumerate() {
+ let content: Vec<_> = ctx
+ .primary_data()
+ .tec()
+ .filter_map(|(t, lat, lon, h, tec)| {
+ if t == epoch {
+ Some((lat, lon, h, tec))
+ } else {
+ None
+ }
+ })
+ .collect();
- let record = ctx.primary_data().record.as_ionex().unwrap(); // cannot fail
+ plot_ctx.add_world_map(
+ &epoch.to_string(),
+ true,
+ MapboxStyle::StamenTerrain,
+ (32.5, -40.0),
+ 1,
+ );
- let mut grid_lat: Vec = Vec::new();
- let mut grid_lon: Vec = Vec::new();
- let mut tec_max = -f64::INFINITY;
- for (e_index, (_e, (tec, _, _))) in record.iter().enumerate() {
- for point in tec {
- if e_index == 0 {
- // grab grid definition
- grid_lat.push(point.latitude.into());
- grid_lon.push(point.longitude.into());
- }
- if point.value > tec_max {
- tec_max = point.value;
- }
+ let mut lat: HashMap = HashMap::new();
+ let mut lon: HashMap = HashMap::new();
+ let mut z: HashMap = HashMap::new();
+ for (tec_lat, tec_lon, _, tec) in content {
+ lat.insert(Epoch::default().to_string(), tec_lat);
+ lon.insert(Epoch::default().to_string(), tec_lon);
+ z.insert(Epoch::default().to_string(), tec);
}
- }
- let grid = ScatterMapbox::new(grid_lat, grid_lon)
- .marker(
- Marker::new()
- .size(5)
- .symbol(MarkerSymbol::Circle)
- .color(NamedColor::Black)
- .opacity(0.5),
- )
- .name("TEC Grid");
- plot_ctx.add_trace(grid);
+ /* plot the map grid */
+ //let grid = ScatterMapbox::new(lat.clone(), lon.clone())
+ // .marker(
+ // Marker::new()
+ // .size(3)
+ // .symbol(MarkerSymbol::Circle)
+ // .color(NamedColor::Black)
+ // .opacity(0.5),
+ // )
+ // .name("grid");
+ //plot_ctx.add_trace(grid);
- /*
- * Build heat map,
- * we have no means to plot several heat map (day course)
- * at the moment, we just plot the 1st epoch
- */
- /*
- for (e_index, (e, (tec, _, _))) in record.iter().enumerate() {
- if e_index == 0 {
- // form the smallest area from that grid
- for rows in
- for i in 0..tec.len() / 4 {
- println!("MAPS {}", i);
- //for i in 0..maps.len() / 2 {
- /*
- let mut row1 = maps[0]
- let mut row2 = maps[1]
- if let Some(points12) = row1.next() {
- if let Some(points34) = row2.next() {
- // average TEC in that area
- let tec = (points12[0].value + points12[1].value
- +points34[0].value + points34[1].value ) / 4.0;
- let color = cmap.eval_continuous(tec / tec_max);
- let area = ScatterMapbox::new(
- vec![points12[0].latitude, points12[1].latitude, points34[0].latitude, points34[1].latitude],
- vec![points12[0].longitude, points12[1].longitude, points34[0].longitude, points34[1].longitude],
- )
- .opacity(0.5)
- .fill(Fill::ToSelf)
- .fill_color(Rgba::new(color.r, color.g, color.b, 0.9));
- ctx.add_trace(area);
- println!("YES");
- }
- }*/
- }
- } else {
- break ; // we need animations for that
- }
- }*/
+ //let map = AnimatedDensityMapbox::new(lat.clone(), lon.clone(), z)
+ // .title("TEST")
+ // .name(epoch.to_string())
+ // .opacity(0.66)
+ // .hover_text_array(hover_text.clone())
+ // .zauto(true)
+ // //.animation_frame("test")
+ // .zoom(3);
+ //plot_ctx.add_trace(map);
+ }
}
diff --git a/rinex-cli/src/plot/record/navigation.rs b/rinex-cli/src/plot/record/navigation.rs
index 2fc67d31e..68efd4e7b 100644
--- a/rinex-cli/src/plot/record/navigation.rs
+++ b/rinex-cli/src/plot/record/navigation.rs
@@ -67,7 +67,7 @@ fn plot_nav_data(rinex: &Rinex, sp3: Option<&SP3>, plot_ctx: &mut PlotContext) {
.collect();
let trace = build_chart_epoch_axis(
- &format!("{}(clk)", sv),
+ &format!("{:X}(clk)", sv),
Mode::LinesMarkers,
sv_epochs.clone(),
sv_clock,
@@ -86,7 +86,7 @@ fn plot_nav_data(rinex: &Rinex, sp3: Option<&SP3>, plot_ctx: &mut PlotContext) {
plot_ctx.add_trace(trace);
let trace = build_chart_epoch_axis(
- &format!("{}(drift)", sv),
+ &format!("{:X}(drift)", sv),
Mode::LinesMarkers,
sv_epochs.clone(),
sv_drift,
@@ -134,7 +134,7 @@ fn plot_nav_data(rinex: &Rinex, sp3: Option<&SP3>, plot_ctx: &mut PlotContext) {
)
.collect();
let trace =
- build_chart_epoch_axis(&format!("{}(sp3_clk)", sv), Mode::Markers, epochs, data)
+ build_chart_epoch_axis(&format!("{:X}(sp3_clk)", sv), Mode::Markers, epochs, data)
.visible({
if sv_index == 0 {
// Clock data differs too much: plot only one to begin with
@@ -182,7 +182,7 @@ fn plot_nav_data(rinex: &Rinex, sp3: Option<&SP3>, plot_ctx: &mut PlotContext) {
)
.collect();
let trace =
- build_chart_epoch_axis(&format!("{}(x)", sv), Mode::Markers, epochs.clone(), x_km)
+ build_chart_epoch_axis(&format!("{:X}(x)", sv), Mode::Markers, epochs.clone(), x_km)
.visible({
if sv_index == 0 {
Visible::True
@@ -206,7 +206,7 @@ fn plot_nav_data(rinex: &Rinex, sp3: Option<&SP3>, plot_ctx: &mut PlotContext) {
.collect();
let trace =
- build_chart_epoch_axis(&format!("{}(y)", sv), Mode::Markers, epochs.clone(), y_km)
+ build_chart_epoch_axis(&format!("{:X}(y)", sv), Mode::Markers, epochs.clone(), y_km)
.y_axis("y2")
.visible({
if sv_index == 0 {
@@ -245,7 +245,7 @@ fn plot_nav_data(rinex: &Rinex, sp3: Option<&SP3>, plot_ctx: &mut PlotContext) {
)
.collect();
let trace = build_chart_epoch_axis(
- &format!("{}(sp3_x)", sv),
+ &format!("{:X}(sp3_x)", sv),
Mode::LinesMarkers,
epochs.clone(),
x,
@@ -271,7 +271,7 @@ fn plot_nav_data(rinex: &Rinex, sp3: Option<&SP3>, plot_ctx: &mut PlotContext) {
)
.collect();
let trace = build_chart_epoch_axis(
- &format!("{}(sp3_y)", sv),
+ &format!("{:X}(sp3_y)", sv),
Mode::LinesMarkers,
epochs.clone(),
y,
@@ -317,7 +317,7 @@ fn plot_nav_data(rinex: &Rinex, sp3: Option<&SP3>, plot_ctx: &mut PlotContext) {
},
)
.collect();
- let trace = build_chart_epoch_axis(&format!("{}(z)", sv), Mode::Markers, epochs, z_km)
+ let trace = build_chart_epoch_axis(&format!("{:X}(z)", sv), Mode::Markers, epochs, z_km)
.visible({
if sv_index == 0 {
Visible::True
@@ -354,15 +354,19 @@ fn plot_nav_data(rinex: &Rinex, sp3: Option<&SP3>, plot_ctx: &mut PlotContext) {
},
)
.collect();
- let trace =
- build_chart_epoch_axis(&format!("{}(sp3_z)", sv), Mode::LinesMarkers, epochs, z_km)
- .visible({
- if sv_index == 0 {
- Visible::True
- } else {
- Visible::LegendOnly
- }
- });
+ let trace = build_chart_epoch_axis(
+ &format!("{:X}(sp3_z)", sv),
+ Mode::LinesMarkers,
+ epochs,
+ z_km,
+ )
+ .visible({
+ if sv_index == 0 {
+ Visible::True
+ } else {
+ Visible::LegendOnly
+ }
+ });
plot_ctx.add_trace(trace);
}
}
diff --git a/rinex-cli/src/plot/record/observation.rs b/rinex-cli/src/plot/record/observation.rs
index 1b89eb254..c333dda98 100644
--- a/rinex-cli/src/plot/record/observation.rs
+++ b/rinex-cli/src/plot/record/observation.rs
@@ -113,7 +113,7 @@ pub fn plot_observation(ctx: &QcContext, plot_context: &mut PlotContext) {
let data_y: Vec = data.iter().map(|(_cs, _e, y)| *y).collect();
let trace = build_chart_epoch_axis(
- &format!("{}({})", sv, observable),
+ &format!("{:X}({})", sv, observable),
Mode::Markers,
data_x,
data_y,
@@ -142,7 +142,7 @@ pub fn plot_observation(ctx: &QcContext, plot_context: &mut PlotContext) {
let epochs: Vec = data.iter().map(|(e, _)| *e).collect();
let elev: Vec = data.iter().map(|(_, f)| *f).collect();
let trace = build_chart_epoch_axis(
- &format!("Elev({})", sv),
+ &format!("Elev({:X})", sv),
Mode::LinesMarkers,
epochs,
elev,
diff --git a/rinex-cli/src/plot/record/sp3.rs b/rinex-cli/src/plot/record/sp3.rs
index d1962a3ad..784784a5a 100644
--- a/rinex-cli/src/plot/record/sp3.rs
+++ b/rinex-cli/src/plot/record/sp3.rs
@@ -81,7 +81,7 @@ pub fn plot_residual_ephemeris(ctx: &QcContext, plot_ctx: &mut PlotContext) {
}
}
let trace =
- build_chart_epoch_axis(&format!("|{}_err|", sv), Mode::Markers, epochs, residuals)
+ build_chart_epoch_axis(&format!("|{:X}_err|", sv), Mode::Markers, epochs, residuals)
.visible({
if sv_index < 4 {
Visible::True
diff --git a/rinex-cli/src/plot/skyplot.rs b/rinex-cli/src/plot/skyplot.rs
index 281c4a470..6129a7b43 100644
--- a/rinex-cli/src/plot/skyplot.rs
+++ b/rinex-cli/src/plot/skyplot.rs
@@ -55,7 +55,7 @@ pub fn skyplot(ctx: &QcContext, plot_context: &mut PlotContext) {
}
})
.connect_gaps(false)
- .name(svnn.to_string());
+ .name(format!("{:X}", svnn));
plot_context.add_trace(trace);
}
}
diff --git a/rinex-qc/Cargo.toml b/rinex-qc/Cargo.toml
index b37fe6639..99659467f 100644
--- a/rinex-qc/Cargo.toml
+++ b/rinex-qc/Cargo.toml
@@ -20,7 +20,7 @@ rustdoc-args = ["--cfg", "docrs", "--generate-link-to-definition"]
[dependencies]
serde = { version = "1.0", optional = true, default-features = false, features = ["derive"] }
-hifitime = "3.8"
+hifitime = "3.8.4"
strum = "0.25"
strum_macros = "0.25"
horrorshow = "0.8"
@@ -28,7 +28,7 @@ itertools = "0.11.0"
statrs = "0.16"
sp3 = { path = "../sp3", features = ["serde"] }
rinex-qc-traits = { path = "../qc-traits", version = "=0.1.1" }
-rinex = { path = "../rinex", features = ["obs", "nav", "qc", "processing", "serde", "flate2"] }
+rinex = { path = "../rinex", version = "=0.14.1", features = ["full"] }
[dev-dependencies]
serde_json = "1"
diff --git a/rinex-qc/src/analysis/mod.rs b/rinex-qc/src/analysis/mod.rs
index dcc6f2bb7..404295f2e 100644
--- a/rinex-qc/src/analysis/mod.rs
+++ b/rinex-qc/src/analysis/mod.rs
@@ -33,7 +33,7 @@ pub struct QcAnalysis {
impl QcAnalysis {
/// Creates a new Analysis Report from given RINEX context.
/// primary : primary file
- pub fn new(primary: &Rinex, nav: &Option, opts: &QcOpts) -> Self {
+ pub fn new(primary: &Rinex, _nav: &Option, opts: &QcOpts) -> Self {
Self {
sv: QcSvAnalysis::new(primary, opts),
sampling: QcSamplingAnalysis::new(primary, opts),
diff --git a/rinex-qc/src/analysis/obs.rs b/rinex-qc/src/analysis/obs.rs
index ce5d1d952..b9f6bab85 100644
--- a/rinex-qc/src/analysis/obs.rs
+++ b/rinex-qc/src/analysis/obs.rs
@@ -5,7 +5,7 @@ use std::str::FromStr;
use crate::{pretty_array, QcOpts};
-use rinex::carrier;
+//use rinex::carrier;
use rinex::carrier::Carrier;
use rinex::observation::Snr;
use rinex::prelude::{Epoch, EpochFlag, Observable, Rinex, Sv};
@@ -115,7 +115,7 @@ fn report_anomalies<'a>(
}
tr {
th {
- : "Cycle slip(s)"
+ : "Cycle slips"
}
@ if cs.is_empty() {
td {
@@ -219,7 +219,7 @@ fn report_epoch_completion(
td {
@ for ((sv, carrier), count) in complete {
b {
- : format!("{} {}/L1", sv, carrier)
+ : format!("{:X} {}/L1", sv, carrier)
}
p {
: format!("{} ({}%)", count, count * 100 / total)
@@ -422,10 +422,8 @@ impl QcObsAnalysis {
total_epochs = r.len();
for ((epoch, _flag), (_clk, svs)) in r {
for (_sv, observables) in svs {
- if !observables.is_empty() {
- if !epoch_with_obs.contains(&epoch) {
- epoch_with_obs.push(*epoch);
- }
+ if !observables.is_empty() && !epoch_with_obs.contains(epoch) {
+ epoch_with_obs.push(*epoch);
}
}
}
@@ -433,7 +431,7 @@ impl QcObsAnalysis {
// append ssi: drop vehicle differentiation
let mut ssi: HashMap> = HashMap::new();
for (_, _, obs, value) in rnx.ssi() {
- if let Some(values) = ssi.get_mut(&obs) {
+ if let Some(values) = ssi.get_mut(obs) {
values.push(value);
} else {
ssi.insert(obs.clone(), vec![value]);
@@ -451,7 +449,7 @@ impl QcObsAnalysis {
let mut snr: HashMap> = HashMap::new();
for ((e, _), _, obs, snr_value) in rnx.snr() {
let snr_f64: f64 = (snr_value as u8).into();
- if let Some(values) = snr.get_mut(&obs) {
+ if let Some(values) = snr.get_mut(obs) {
values.push((e, snr_f64));
} else {
snr.insert(obs.clone(), vec![(e, snr_f64)]);
diff --git a/rinex-qc/src/analysis/sv.rs b/rinex-qc/src/analysis/sv.rs
index bc3ba457e..ae1ea652b 100644
--- a/rinex-qc/src/analysis/sv.rs
+++ b/rinex-qc/src/analysis/sv.rs
@@ -13,7 +13,7 @@ impl QcSvAnalysis {
pub fn new(primary: &Rinex, _opts: &QcOpts) -> Self {
let sv = primary.sv();
Self {
- sv: { sv.map(|sv| sv.to_string()).collect() },
+ sv: { sv.map(|sv| format!("{:X}", sv)).collect() },
}
}
}
diff --git a/rinex-qc/src/context.rs b/rinex-qc/src/context.rs
index 803236e30..003e7984c 100644
--- a/rinex-qc/src/context.rs
+++ b/rinex-qc/src/context.rs
@@ -3,7 +3,7 @@ use rinex_qc_traits::HtmlReport;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
-use rinex::carrier::Carrier;
+//use rinex::carrier::Carrier;
use rinex::observation::Snr;
use rinex::prelude::{Epoch, GroundPosition, Rinex, Sv};
use rinex::Error;
@@ -116,7 +116,7 @@ impl QcContext {
/// Returns reference to SP3 data specifically
pub fn sp3_data(&self) -> Option<&SP3> {
if let Some(ref sp3) = self.sp3 {
- Some(&sp3.data())
+ Some(sp3.data())
} else {
None
}
@@ -204,7 +204,7 @@ impl QcContext {
/* NB: interpolate Complete Epochs only */
let complete_epoch: Vec<_> = self.primary_data().complete_epoch(min_snr).collect();
for (e, sv_signals) in complete_epoch {
- for (sv, carrier) in sv_signals {
+ for (sv, _carrier) in sv_signals {
// if orbit already exists: do not interpolate
// this will make things much quicker for high quality data products
let found = self
@@ -214,17 +214,14 @@ impl QcContext {
if let Some((_, _, (x, y, z))) = found {
// store as is
self.orbits.insert((e, sv), (x, y, z));
- } else {
- if let Some(sp3) = self.sp3_data() {
- if let Some((x_km, y_km, z_km)) = sp3.sv_position_interpolate(sv, e, order)
- {
- self.orbits.insert((e, sv), (x_km, y_km, z_km));
- }
- } else if let Some(nav) = self.navigation_data() {
- if let Some((x_m, y_m, z_m)) = nav.sv_position_interpolate(sv, e, order) {
- self.orbits
- .insert((e, sv), (x_m * 1.0E-3, y_m * 1.0E-3, z_m * 1.0E-3));
- }
+ } else if let Some(sp3) = self.sp3_data() {
+ if let Some((x_km, y_km, z_km)) = sp3.sv_position_interpolate(sv, e, order) {
+ self.orbits.insert((e, sv), (x_km, y_km, z_km));
+ }
+ } else if let Some(nav) = self.navigation_data() {
+ if let Some((x_m, y_m, z_m)) = nav.sv_position_interpolate(sv, e, order) {
+ self.orbits
+ .insert((e, sv), (x_m * 1.0E-3, y_m * 1.0E-3, z_m * 1.0E-3));
}
}
}
diff --git a/rinex-qc/src/lib.rs b/rinex-qc/src/lib.rs
index ac5782d72..3ee92a710 100644
--- a/rinex-qc/src/lib.rs
+++ b/rinex-qc/src/lib.rs
@@ -85,8 +85,7 @@ impl QcReport {
}
},
QcClassification::Physics => {
- let mut observables: Vec<_> =
- ctx.primary_data().observable().map(|o| o.clone()).collect();
+ let mut observables: Vec<_> = ctx.primary_data().observable().cloned().collect();
observables.sort(); // improves report rendering
for obsv in observables {
filter_targets.push(TargetItem::from(obsv));
@@ -103,14 +102,13 @@ impl QcReport {
let subset = ctx.primary_data().filter(mask.clone().into());
// also apply to possible NAV augmentation
- let nav_subset = if let Some(nav) = &ctx.navigation_data() {
- Some(nav.filter(mask.clone().into()))
- } else {
- None
- };
+ let nav_subset = ctx
+ .navigation_data()
+ .as_ref()
+ .map(|nav| nav.filter(mask.clone().into()));
// perform analysis on these subsets
- analysis.push(QcAnalysis::new(&subset, &nav_subset, &opts));
+ analysis.push(QcAnalysis::new(&subset, &nav_subset, opts));
}
analysis
}
diff --git a/rinex/Cargo.toml b/rinex/Cargo.toml
index 17b8fdcac..6b98e61cc 100644
--- a/rinex/Cargo.toml
+++ b/rinex/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rinex"
-version = "0.14.0"
+version = "0.14.1"
license = "MIT OR Apache-2.0"
authors = ["Guillaume W. Bres "]
description = "Package to parse and analyze RINEX data"
@@ -18,6 +18,7 @@ sbas = ["geo", "wkt"]
obs = []
meteo = []
nav = []
+ionex = []
processing = []
qc = ["rinex-qc-traits", "horrorshow"] # rinex Quality Check (mainly OBS RINEX)
@@ -25,6 +26,7 @@ qc = ["rinex-qc-traits", "horrorshow"] # rinex Quality Check (mainly OBS RINEX)
full = [
"flate2",
"horrorshow",
+ "ionex",
"meteo",
"nav",
"obs",
@@ -40,6 +42,7 @@ rustdoc-args = ["--cfg", "docrs", "--generate-link-to-definition"]
[build-dependencies]
serde_json = { version = "1.0", features = ["preserve_order"] }
+serde = { version = "1.0", default-features = false, features = ["derive"] }
[dependencies]
num = "0.4"
@@ -58,7 +61,7 @@ geo = { version = "0.26", optional = true }
wkt = { version = "0.10.0", default-features = false, optional = true }
serde = { version = "1.0", optional = true, default-features = false, features = ["derive"] }
flate2 = { version = "1.0.24", optional = true, default-features = false, features = ["zlib"] }
-hifitime = { version = "3.8", features = ["serde", "std"] }
+hifitime = { version = "3.8.4", features = ["serde", "std"] }
horrorshow = { version = "0.8", optional = true }
rinex-qc-traits = { path = "../qc-traits", version = "=0.1.1", optional = true }
diff --git a/rinex/benches/benchmark.rs b/rinex/benches/benchmark.rs
index 8592b920f..cac03acb4 100644
--- a/rinex/benches/benchmark.rs
+++ b/rinex/benches/benchmark.rs
@@ -3,7 +3,7 @@ use rinex::{
hatanaka::{numdiff::NumDiff, textdiff::TextDiff},
observation::*,
prelude::*,
- processing::*,
+ //processing::*,
reader::BufferedReader,
record::parse_record,
};
@@ -36,7 +36,7 @@ fn profiled() -> Criterion {
}*/
fn parse_file(fp: &str) {
- let _ = Rinex::from_file(fp);
+ let _ = Rinex::from_file(fp).unwrap();
}
fn text_decompression(textdiff: &mut TextDiff, data: &[&str]) {
@@ -192,223 +192,96 @@ fn decompression_benchmark(c: &mut Criterion) {
}
/*
- * Puts record section parsing to the test
- */
+ * Evaluates parsing performance of plain RINEX parsing
fn record_parsing_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("parsing");
- // prepare for OBS/zegv0010.21o
- let mut header = Header::basic_obs().with_observation_fields(HeaderFields {
- crinex: None,
- codes: {
- let mut map: HashMap> = HashMap::new();
- map.insert(
- Constellation::GPS,
- vec![
- Observable::from_str("C1").unwrap(),
- Observable::from_str("C2").unwrap(),
- Observable::from_str("C5").unwrap(),
- Observable::from_str("L1").unwrap(),
- Observable::from_str("L2").unwrap(),
- Observable::from_str("L5").unwrap(),
- Observable::from_str("P1").unwrap(),
- Observable::from_str("P2").unwrap(),
- Observable::from_str("S1").unwrap(),
- Observable::from_str("S2").unwrap(),
- Observable::from_str("S5").unwrap(),
- ],
- );
- map.insert(
- Constellation::Glonass,
- vec![
- Observable::from_str("C1").unwrap(),
- Observable::from_str("C2").unwrap(),
- Observable::from_str("C5").unwrap(),
- Observable::from_str("L1").unwrap(),
- Observable::from_str("L2").unwrap(),
- Observable::from_str("L5").unwrap(),
- Observable::from_str("P1").unwrap(),
- Observable::from_str("P2").unwrap(),
- Observable::from_str("S1").unwrap(),
- Observable::from_str("S2").unwrap(),
- Observable::from_str("S5").unwrap(),
- ],
- );
- map
- },
- clock_offset_applied: false,
- dcb_compensations: Vec::new(),
- scalings: HashMap::new(),
- });
- group.bench_function("OBSv2/zegv0010.21o", |b| {
- b.iter(|| {
- record_parsing("../test_resources/OBS/V2/zegv0010.21o", &mut header);
- })
- });
-
- // prepare for OBS/V3/ACOR00ESP
- let mut header = Header::basic_obs().with_observation_fields(HeaderFields {
- crinex: None,
- codes: {
- let mut map: HashMap> = HashMap::new();
- map.insert(
- Constellation::GPS,
- vec![
- Observable::from_str("C1C").unwrap(),
- Observable::from_str("L1C").unwrap(),
- Observable::from_str("S1C").unwrap(),
- Observable::from_str("C2S").unwrap(),
- Observable::from_str("L2S").unwrap(),
- Observable::from_str("S2S").unwrap(),
- Observable::from_str("C2W").unwrap(),
- Observable::from_str("L2W").unwrap(),
- Observable::from_str("S2W").unwrap(),
- Observable::from_str("C5Q").unwrap(),
- Observable::from_str("L5Q").unwrap(),
- Observable::from_str("S5Q").unwrap(),
- ],
- );
- map.insert(
- Constellation::Glonass,
- vec![
- Observable::from_str("C1C").unwrap(),
- Observable::from_str("L1C").unwrap(),
- Observable::from_str("S1C").unwrap(),
- Observable::from_str("C2P").unwrap(),
- Observable::from_str("L2P").unwrap(),
- Observable::from_str("S2P").unwrap(),
- Observable::from_str("C2C").unwrap(),
- Observable::from_str("L2C").unwrap(),
- Observable::from_str("S2C").unwrap(),
- Observable::from_str("C3Q").unwrap(),
- Observable::from_str("L3Q").unwrap(),
- Observable::from_str("S3Q").unwrap(),
- ],
- );
- map.insert(
- Constellation::Galileo,
- vec![
- Observable::from_str("C1C").unwrap(),
- Observable::from_str("L1C").unwrap(),
- Observable::from_str("S1C").unwrap(),
- Observable::from_str("C5Q").unwrap(),
- Observable::from_str("L5Q").unwrap(),
- Observable::from_str("S5Q").unwrap(),
- Observable::from_str("C6C").unwrap(),
- Observable::from_str("L6C").unwrap(),
- Observable::from_str("S6C").unwrap(),
- Observable::from_str("C7Q").unwrap(),
- Observable::from_str("L7Q").unwrap(),
- Observable::from_str("S7Q").unwrap(),
- Observable::from_str("C8Q").unwrap(),
- Observable::from_str("L8Q").unwrap(),
- Observable::from_str("S8Q").unwrap(),
- ],
- );
- map.insert(
- Constellation::BeiDou,
- vec![
- Observable::from_str("C2I").unwrap(),
- Observable::from_str("L2I").unwrap(),
- Observable::from_str("S2I").unwrap(),
- Observable::from_str("C6I").unwrap(),
- Observable::from_str("L6I").unwrap(),
- Observable::from_str("S6I").unwrap(),
- Observable::from_str("C7I").unwrap(),
- Observable::from_str("L7I").unwrap(),
- Observable::from_str("S7I").unwrap(),
- ],
- );
- map
- },
- clock_offset_applied: false,
- dcb_compensations: Vec::new(),
- scalings: HashMap::new(),
- });
- group.bench_function("OBSv3/ACOR00ESP", |b| {
- b.iter(|| {
- record_parsing(
- "../test_resources/OBS/V3/ACOR00ESP_R_20213550000_01D_30S_MO.rnx",
- &mut header,
- );
- })
- });
-
- //prepare for CRNX/V1/delf0010.21d
- //prepare for CRNX/V3/ESBC00DNK
- //prepare for NAV/V2/ijmu3650.21n.gz
- //prepare for NAV/V3/MOJN00DNK_R_20201770000_01D_MN.rnx.gz
-
- group.finish(); /* concludes record section */
-}
-
-fn processing_benchmark(c: &mut Criterion) {
- let mut group = c.benchmark_group("processing");
- let rinex =
- Rinex::from_file("../test_resources/CRNX/V3/ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz")
- .unwrap();
- let record = rinex.record.as_obs().unwrap();
-
- for filter in vec![
- (Filter::from_str("mask:GPS,GLO,BDS").unwrap(), "mask:gnss"),
- //(Filter::from_str("mask:gt:10 minutes").unwrap(), "mask:dt"),
- (
- Filter::from_str("mask:L1C,C1C,L2P,L2W").unwrap(),
- "mask:obs",
- ),
- (
- Filter::from_str("mask:g08,g15,g19,r03,r09").unwrap(),
- "mask:sv",
- ),
- //(Filter::from_str("mask:2020-06-25 08:00:00UTC").unwrap(), "mask:epoch"),
- (Filter::from_str("smooth:hatch").unwrap(), "smoothing:hatch"),
- (
- Filter::from_str("smooth:hatch:l1c,l2c").unwrap(),
- "smoothing:hatch:l1c,l2c",
- ),
- //(Filter::from_str("smooth:mov:10 minutes").unwrap(), "smoothing:mov:10 mins"),
- ] {
- let (filter, name) = filter;
- group.bench_function(&format!("esbc00dnk_r_2021/{}", name), |b| {
- b.iter(|| record.filter(filter.clone()))
- });
- }
-
- for combination in vec![
- (Combination::GeometryFree, "gf"),
- (Combination::NarrowLane, "nl"),
- (Combination::WideLane, "wl"),
- (Combination::MelbourneWubbena, "mw"),
+ let base_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("..")
+ .join("test_resources");
+ /*
+ * small, medium, large compressed: OBS
+ */
+ for (rev, filename) in vec![
+ ("V2", "del0010.21o"),
] {
- let (combination, name) = combination;
- group.bench_function(&format!("esbc00dnk_r_2021/{}", name), |b| {
+ group.bench_function("OBSv2/zegv0010.21o", |b| {
b.iter(|| {
- record.combine(combination);
+ record_parsing("../test_resources/OBS/V2/zegv0010.21o", &mut header);
})
});
}
- group.bench_function("esbc00dnk_r_2021/dcb", |b| {
- b.iter(|| {
- record.dcb();
- })
- });
- group.bench_function("esbc00dnk_r_2021/ionod", |b| {
- b.iter(|| {
- record.iono_delay_detector(Duration::from_seconds(30.0));
- })
- });
- group.bench_function("esbc00dnk_r_2021/derivative", |b| {
- b.iter(|| {
- let der = record.derivative();
- let mov = der.moving_average(Duration::from_seconds(600.0), None);
- })
- });
+ group.finish(); /* concludes record section */
}
+ */
+
+//fn processing_benchmark(c: &mut Criterion) {
+// let mut group = c.benchmark_group("processing");
+// let rinex =
+// Rinex::from_file("../test_resources/CRNX/V3/ESBC00DNK_R_20201770000_01D_30S_MO.crx.gz")
+// .unwrap();
+// let record = rinex.record.as_obs().unwrap();
+//
+// for filter in vec![
+// (Filter::from_str("mask:GPS,GLO,BDS").unwrap(), "mask:gnss"),
+// //(Filter::from_str("mask:gt:10 minutes").unwrap(), "mask:dt"),
+// (
+// Filter::from_str("mask:L1C,C1C,L2P,L2W").unwrap(),
+// "mask:obs",
+// ),
+// (
+// Filter::from_str("mask:g08,g15,g19,r03,r09").unwrap(),
+// "mask:sv",
+// ),
+// //(Filter::from_str("mask:2020-06-25 08:00:00UTC").unwrap(), "mask:epoch"),
+// (Filter::from_str("smooth:hatch").unwrap(), "smoothing:hatch"),
+// (
+// Filter::from_str("smooth:hatch:l1c,l2c").unwrap(),
+// "smoothing:hatch:l1c,l2c",
+// ),
+// //(Filter::from_str("smooth:mov:10 minutes").unwrap(), "smoothing:mov:10 mins"),
+// ] {
+// let (filter, name) = filter;
+// group.bench_function(&format!("esbc00dnk_r_2021/{}", name), |b| {
+// b.iter(|| record.filter(filter.clone()))
+// });
+// }
+//
+// for combination in vec![
+// (Combination::GeometryFree, "gf"),
+// (Combination::NarrowLane, "nl"),
+// (Combination::WideLane, "wl"),
+// (Combination::MelbourneWubbena, "mw"),
+// ] {
+// let (combination, name) = combination;
+// group.bench_function(&format!("esbc00dnk_r_2021/{}", name), |b| {
+// b.iter(|| {
+// record.combine(combination);
+// })
+// });
+// }
+// group.bench_function("esbc00dnk_r_2021/dcb", |b| {
+// b.iter(|| {
+// record.dcb();
+// })
+// });
+// group.bench_function("esbc00dnk_r_2021/ionod", |b| {
+// b.iter(|| {
+// record.iono_delay_detector(Duration::from_seconds(30.0));
+// })
+// });
+// group.bench_function("esbc00dnk_r_2021/derivative", |b| {
+// b.iter(|| {
+// let der = record.derivative();
+// let mov = der.moving_average(Duration::from_seconds(600.0), None);
+// })
+// });
+//}
fn benchmark(c: &mut Criterion) {
decompression_benchmark(c);
- record_parsing_benchmark(c);
- processing_benchmark(c);
+ //record_parsing_benchmark(c);
+ //processing_benchmark(c);
}
criterion_group!(benches, benchmark);
diff --git a/rinex/build.rs b/rinex/build.rs
index b9294403f..4ff3d4d62 100644
--- a/rinex/build.rs
+++ b/rinex/build.rs
@@ -3,8 +3,8 @@ use std::io::Write;
use std::path::Path;
fn build_nav_database() {
- let out_dir = env::var("OUT_DIR").unwrap();
- let nav_path = Path::new(&out_dir).join("nav_orbits.rs");
+ 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();
// read helper descriptor
@@ -100,6 +100,84 @@ pub struct NavHelper<'a> {
.unwrap();
}
+use serde::Deserialize;
+
+fn default_launch_month() -> u8 {
+ 1 // Jan
+}
+
+fn default_launch_day() -> u8 {
+ 1 // 1st day of month
+}
+
+/*
+ * We use an intermediate struct
+ * and "serde" to allow not to describe the launched
+ * day or month for example
+ */
+#[derive(Deserialize)]
+struct SBASDBEntry<'a> {
+ pub constellation: &'a str,
+ pub prn: u16,
+ pub id: &'a str,
+ #[serde(default = "default_launch_month")]
+ pub launched_month: u8,
+ #[serde(default = "default_launch_day")]
+ pub launched_day: u8,
+ pub launched_year: i32,
+}
+
+fn build_sbas_helper() {
+ let outdir = env::var("OUT_DIR").unwrap();
+ let path = Path::new(&outdir).join("sbas.rs");
+ let mut fd = std::fs::File::create(path).unwrap();
+
+ // read descriptor: parse and dump into a static array
+ let db_content = std::fs::read_to_string("db/SBAS/sbas.json").unwrap();
+
+ let sbas_db: Vec = serde_json::from_str(&db_content).unwrap();
+
+ let content = "use lazy_static::lazy_static;
+
+#[derive(Debug)]
+pub struct SBASHelper<'a> {
+ constellation: &'a str,
+ prn: u16,
+ id: &'a str,
+ launched_day: u8,
+ launched_month: u8,
+ launched_year: i32,
+}
+
+lazy_static! {
+ static ref SBAS_VEHICLES: Vec> = vec![
+\n";
+
+ fd.write_all(content.as_bytes()).unwrap();
+
+ for e in sbas_db {
+ fd.write_all(
+ format!(
+ "SBASHelper {{
+ constellation: \"{}\",
+ prn: {},
+ id: \"{}\",
+ launched_year: {},
+ launched_month: {},
+ launched_day: {}
+ }},",
+ e.constellation, e.prn, e.id, e.launched_year, e.launched_month, e.launched_day,
+ )
+ .as_bytes(),
+ )
+ .unwrap()
+ }
+
+ fd.write_all(" ];".as_bytes()).unwrap();
+ fd.write_all("}\n".as_bytes()).unwrap();
+}
+
fn main() {
build_nav_database();
+ build_sbas_helper();
}
diff --git a/rinex/db/SBAS/sbas.json b/rinex/db/SBAS/sbas.json
new file mode 100644
index 000000000..ffb621810
--- /dev/null
+++ b/rinex/db/SBAS/sbas.json
@@ -0,0 +1,121 @@
+[
+ {
+ "constellation": "AusNZ",
+ "prn": 122,
+ "id": "INMARSAT-4F1",
+ "launched_year": 2020,
+ "launched_month": 1
+ },
+ {
+ "constellation": "EGNOS",
+ "prn": 123,
+ "id": "ASTRA-5B",
+ "launched_year": 2021,
+ "launched_month": 11
+ },
+ {
+ "constellation": "SDCM",
+ "prn": 125,
+ "id": "Luch-5A",
+ "launched_year": 2021,
+ "launched_month": 12
+ },
+ {
+ "constellation": "EGNOS",
+ "prn": 126,
+ "id": "INMARSAT-4F2",
+ "launched_year": 2023,
+ "launched_month": 4
+ },
+ {
+ "constellation": "GAGAN",
+ "prn": 127,
+ "id": "GSAT-8",
+ "launched_year": 2020,
+ "launched_month": 9
+ },
+ {
+ "constellation": "GAGAN",
+ "prn": 128,
+ "id": "GSAT-10",
+ "launched_year": 2020,
+ "launched_month": 9
+ },
+ {
+ "constellation": "BDSBAS",
+ "prn": 130,
+ "id": "G6",
+ "launched_year": 2020,
+ "launched_month": 10
+ },
+ {
+ "constellation": "BDSBAS",
+ "prn": 130,
+ "id": "G6",
+ "launched_year": 2020,
+ "launched_month": 10
+ },
+ {
+ "constellation": "KASS",
+ "prn": 134,
+ "id": "MEASAT-3D",
+ "launched_year": 2021,
+ "launched_month": 6
+ },
+ {
+ "constellation": "EGNOS",
+ "prn": 136,
+ "id": "SES-5",
+ "launched_year": 2021,
+ "launched_month": 11
+ },
+ {
+ "constellation": "WAAS",
+ "prn": 138,
+ "id": "ANIK-F1R",
+ "launched_year": 2022,
+ "launched_month": 7
+ },
+ {
+ "constellation": "SDCM",
+ "prn": 140,
+ "id": "Luch-5B",
+ "launched_year": 2021,
+ "launched_month": 12
+ },
+ {
+ "constellation": "SDCM",
+ "prn": 141,
+ "id": "Luch-4",
+ "launched_year": 2021,
+ "launched_month": 12
+ },
+ {
+ "constellation": "BDSBAS",
+ "prn": 143,
+ "id": "G3",
+ "launched_year": 2020,
+ "launched_month": 10
+ },
+ {
+ "constellation": "BDSBAS",
+ "prn": 144,
+ "id": "G1",
+ "launched_year": 2020,
+ "launched_month": 10
+ },
+ {
+ "constellation": "NSAS",
+ "prn": 147,
+ "id": "NIGCOMSAT-1R",
+ "launched_year": 2021,
+ "launched_month": 1
+ },
+ {
+ "constellation": "ASAL",
+ "prn": 148,
+ "id": "ALCOMSAT-1",
+ "launched_year": 2020,
+ "launched_month": 1
+ }
+]
diff --git a/rinex/src/algorithm/filters/decim.rs b/rinex/src/algorithm/filters/decim.rs
index 0b97d4bba..998647c1b 100644
--- a/rinex/src/algorithm/filters/decim.rs
+++ b/rinex/src/algorithm/filters/decim.rs
@@ -81,7 +81,7 @@ pub trait Decimate {
impl std::str::FromStr for DecimationFilter {
type Err = Error;
fn from_str(content: &str) -> Result {
- let items: Vec<&str> = content.trim().split(":").collect();
+ let items: Vec<&str> = content.trim().split(':').collect();
if let Ok(dt) = Duration::from_str(items[0].trim()) {
Ok(Self {
target: {
@@ -94,7 +94,7 @@ impl std::str::FromStr for DecimationFilter {
},
dtype: DecimationType::DecimByInterval(dt),
})
- } else if let Ok(r) = u32::from_str_radix(items[0].trim(), 10) {
+ } else if let Ok(r) = items[0].trim().parse::() {
Ok(Self {
target: {
if items.len() > 1 {
diff --git a/rinex/src/algorithm/filters/mask.rs b/rinex/src/algorithm/filters/mask.rs
index 6f7195b06..72d0395c0 100644
--- a/rinex/src/algorithm/filters/mask.rs
+++ b/rinex/src/algorithm/filters/mask.rs
@@ -4,13 +4,13 @@ use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("invalid mask target")]
- TargetError(#[from] crate::algorithm::target::Error),
+ InvalidTarget(#[from] crate::algorithm::target::Error),
#[error("missing a mask operand")]
MissingOperand,
#[error("invalid mask operand")]
InvalidOperand,
#[error("invalid mask target \"{0}\"")]
- InvalidTarget(String),
+ NonSupportedTarget(String),
#[error("invalid mask description")]
InvalidDescriptor,
}
@@ -51,13 +51,13 @@ impl std::str::FromStr for MaskOperand {
let c = content.trim();
if c.starts_with(">=") {
Ok(Self::GreaterEquals)
- } else if c.starts_with(">") {
+ } else if c.starts_with('>') {
Ok(Self::GreaterThan)
} else if c.starts_with("<=") {
Ok(Self::LowerEquals)
- } else if c.starts_with("<") {
+ } else if c.starts_with('<') {
Ok(Self::LowerThan)
- } else if c.starts_with("=") {
+ } else if c.starts_with('=') {
Ok(Self::Equals)
} else if c.starts_with("!=") {
Ok(Self::NotEquals)
@@ -256,14 +256,14 @@ impl std::str::FromStr for MaskFilter {
let float_offset = operand_offset + operand.formatted_len() + 2;
Ok(Self {
operand,
- item: TargetItem::from_elevation(&cleanedup[float_offset..].trim())?,
+ item: TargetItem::from_elevation(cleanedup[float_offset..].trim())?,
})
} else if content[0..1].eq("a") {
// --> Azimuth Mask case
let float_offset = operand_offset + operand.formatted_len() + 2;
Ok(Self {
operand,
- item: TargetItem::from_azimuth(&cleanedup[float_offset..].trim())?,
+ item: TargetItem::from_azimuth(cleanedup[float_offset..].trim())?,
})
} else {
// We're only left with SNR mask case
@@ -271,10 +271,10 @@ impl std::str::FromStr for MaskFilter {
if content[0..3].eq("snr") {
Ok(Self {
operand,
- item: TargetItem::from_snr(&cleanedup[float_offset..].trim())?,
+ item: TargetItem::from_snr(cleanedup[float_offset..].trim())?,
})
} else {
- Err(Error::InvalidTarget(
+ Err(Error::NonSupportedTarget(
cleanedup[..operand_offset].to_string(),
))
}
@@ -289,7 +289,7 @@ impl std::str::FromStr for MaskFilter {
Ok(Self {
operand,
- item: TargetItem::from_str(&cleanedup[offset..].trim_start())?,
+ item: TargetItem::from_str(cleanedup[offset..].trim_start())?,
})
}
}
@@ -303,7 +303,7 @@ mod test {
use std::str::FromStr;
#[test]
fn mask_operand() {
- for (descriptor, opposite_desc) in vec![
+ for (descriptor, opposite_desc) in [
(">=", "<="),
(">", "<"),
("=", "!="),
@@ -349,7 +349,7 @@ mod test {
}
#[test]
fn mask_elev() {
- for desc in vec![
+ for desc in [
"e< 40.0",
"e != 30",
" e<40.0",
@@ -368,7 +368,7 @@ mod test {
}
#[test]
fn mask_gnss() {
- for (descriptor, opposite_desc) in vec![
+ for (descriptor, opposite_desc) in [
(" = GPS", "!= GPS"),
("= GAL,GPS", "!= GAL,GPS"),
(" =GLO,GAL", "!= GLO,GAL"),
@@ -412,9 +412,7 @@ mod test {
}
#[test]
fn mask_sv() {
- for (descriptor, opposite_desc) in
- vec![(" = G01", "!= G01"), ("= R03, G31", "!= R03, G31")]
- {
+ for (descriptor, opposite_desc) in [(" = G01", "!= G01"), ("= R03, G31", "!= R03, G31")] {
let mask = MaskFilter::from_str(descriptor);
assert!(
mask.is_ok(),
diff --git a/rinex/src/algorithm/filters/mod.rs b/rinex/src/algorithm/filters/mod.rs
index 4d4116d4f..6f28c2574 100644
--- a/rinex/src/algorithm/filters/mod.rs
+++ b/rinex/src/algorithm/filters/mod.rs
@@ -18,15 +18,15 @@ pub enum Error {
#[error("unknown filter type \"{0}\"")]
UnknownFilterType(String),
#[error("invalid mask filter")]
- MaskFilterParsingError(#[from] mask::Error),
+ MaskFilterParsing(#[from] mask::Error),
#[error("invalid decimation filter")]
- DecimationFilterParsingError(#[from] decim::Error),
+ DecimationFilterParsing(#[from] decim::Error),
#[error("invalid smoothing filter")]
- SmoothingFilterParsingError(#[from] smoothing::Error),
+ SmoothingFilterParsing(#[from] smoothing::Error),
#[error("invalid filter target")]
- TargetItemError(#[from] super::target::Error),
+ TargetItem(#[from] super::target::Error),
#[error("failed to apply filter")]
- FilterError,
+ Filter,
}
/// Preprocessing filters, to preprocess RINEX data prior further analysis.
@@ -75,7 +75,7 @@ impl From for Filter {
impl std::str::FromStr for Filter {
type Err = Error;
fn from_str(content: &str) -> Result {
- let items: Vec<&str> = content.split(":").collect();
+ let items: Vec<&str> = content.split(':').collect();
let identifier = items[0].trim();
if identifier.eq("decim") {
@@ -118,7 +118,7 @@ mod test {
/*
* MASK FILTER description
*/
- for descriptor in vec![
+ for descriptor in [
"GPS",
"=GPS",
" != GPS",
@@ -141,7 +141,7 @@ mod test {
/*
* DECIMATION FILTER description
*/
- for desc in vec![
+ for desc in [
"decim:10",
"decim:10 min",
"decim:1 hour",
@@ -154,7 +154,7 @@ mod test {
/*
* SMOOTHING FILTER description
*/
- for desc in vec![
+ for desc in [
"smooth:mov:10 min",
"smooth:mov:1 hour",
"smooth:mov:1 hour:l1c",
diff --git a/rinex/src/algorithm/filters/smoothing.rs b/rinex/src/algorithm/filters/smoothing.rs
index bf777dd9e..2cd1d5ef7 100644
--- a/rinex/src/algorithm/filters/smoothing.rs
+++ b/rinex/src/algorithm/filters/smoothing.rs
@@ -23,19 +23,19 @@ pub struct SmoothingFilter {
#[derive(Error, Debug)]
pub enum Error {
#[error("invalid description \"{0}\"")]
- InvalidDescriptionError(String),
+ InvalidDescription(String),
#[error("unknown smoothing filter \"{0}\"")]
UnknownFilter(String),
- #[error("unknown smoothing target")]
- TargetError(#[from] crate::algorithm::target::Error),
+ #[error("invalid target")]
+ InvalidTarget(#[from] crate::algorithm::target::Error),
#[error("failed to parse duration")]
- DurationParsingError(#[from] hifitime::Errors),
+ DurationParsing(#[from] hifitime::Errors),
}
impl std::str::FromStr for SmoothingFilter {
type Err = Error;
fn from_str(content: &str) -> Result {
- let items: Vec<&str> = content.trim().split(":").collect();
+ let items: Vec<&str> = content.trim().split(':').collect();
if items[0].trim().eq("hatch") {
Ok(Self {
target: {
@@ -50,7 +50,7 @@ impl std::str::FromStr for SmoothingFilter {
})
} else if items[0].trim().eq("mov") {
if items.len() < 2 {
- return Err(Error::InvalidDescriptionError(format!("{:?}", items)));
+ return Err(Error::InvalidDescription(format!("{:?}", items)));
}
let dt = Duration::from_str(items[1].trim())?;
Ok(Self {
@@ -87,7 +87,7 @@ mod test {
use std::str::FromStr;
#[test]
fn from_str() {
- for desc in vec!["hatch", "hatch:C1C", "hatch:c1c,c2p"] {
+ for desc in ["hatch", "hatch:C1C", "hatch:c1c,c2p"] {
let filter = SmoothingFilter::from_str(desc);
assert!(
filter.is_ok(),
@@ -95,7 +95,7 @@ mod test {
desc
);
}
- for desc in vec![
+ for desc in [
"mov:10 min",
"mov:1 hour",
"mov:10 min:clk",
diff --git a/rinex/src/algorithm/target.rs b/rinex/src/algorithm/target.rs
index 0165c181d..c96ded211 100644
--- a/rinex/src/algorithm/target.rs
+++ b/rinex/src/algorithm/target.rs
@@ -13,7 +13,7 @@ pub enum Error {
#[error("unknown target \"{0}\"")]
UnknownTarget(String),
#[error("type guessing error \"{0}\"")]
- TypeGuessingError(String),
+ TypeGuessing(String),
#[error("expecting two epochs when describing a duration")]
InvalidDuration,
#[error("bad epoch description")]
@@ -29,9 +29,9 @@ pub enum Error {
#[error("constellation parsing error")]
ConstellationParing(#[from] constellation::ParsingError),
#[error("failed to parse epoch flag")]
- EpochFlagParsingError(#[from] crate::epoch::flag::Error),
+ EpochFlagParsing(#[from] crate::epoch::flag::Error),
#[error("failed to parse constellation")]
- ConstellationParsingError,
+ ConstellationParsing,
#[error("invalid nav item")]
InvalidNavItem(#[from] crate::navigation::Error),
#[error("observable parsing error")]
@@ -236,7 +236,7 @@ impl std::str::FromStr for TargetItem {
* when operand comes first in description.
* Otherwise, we muse use other methods
*/
- let items: Vec<&str> = c.split(",").collect();
+ let items: Vec<&str> = c.split(',').collect();
/*
* Epoch and Durations
*/
@@ -309,11 +309,11 @@ impl std::str::FromStr for TargetItem {
.map(|s| s.to_string())
.collect();
- if matched_orbits.len() > 0 {
+ if !matched_orbits.is_empty() {
Ok(Self::OrbitItem(matched_orbits))
} else {
// not a single match
- Err(Error::TypeGuessingError(c.to_string()))
+ Err(Error::TypeGuessing(c.to_string()))
}
}
}
@@ -452,7 +452,7 @@ mod test {
assert_eq!(target, TargetItem::DurationItem(dt));
// test Matching NAV orbits
- for descriptor in vec![
+ for descriptor in [
"iode",
"crc",
"crs",
@@ -469,7 +469,7 @@ mod test {
}
// test non matching NAV orbits
- for descriptor in vec!["oide", "ble", "blah, oide"] {
+ for descriptor in ["oide", "ble", "blah, oide"] {
let target = TargetItem::from_str(descriptor);
assert!(
target.is_err(),
diff --git a/rinex/src/antex/frequency.rs b/rinex/src/antex/frequency.rs
index fcb3461cd..251a391a8 100644
--- a/rinex/src/antex/frequency.rs
+++ b/rinex/src/antex/frequency.rs
@@ -19,10 +19,7 @@ impl Default for Pattern {
impl Pattern {
/// Returns true if this phase pattern is azimuth dependent
pub fn is_azimuth_dependent(&self) -> bool {
- match self {
- Self::AzimuthDependent(_) => true,
- _ => false,
- }
+ matches!(self, Self::AzimuthDependent(_))
}
/// Unwraps pattern values, whether it is
/// Azimuth dependent or not
@@ -124,7 +121,7 @@ impl Frequency {
}
pub fn with_carrier(&self, carrier: Carrier) -> Self {
let mut f = self.clone();
- f.carrier = carrier.clone();
+ f.carrier = carrier;
f
}
pub fn with_northern_eccentricity(&self, north: f64) -> Self {
@@ -156,7 +153,7 @@ mod test {
fn test_pattern() {
let default = Pattern::default();
assert_eq!(default, Pattern::NonAzimuthDependent(Vec::new()));
- assert_eq!(default.is_azimuth_dependent(), false);
+ assert!(!default.is_azimuth_dependent());
}
#[test]
fn test_frequency() {
diff --git a/rinex/src/antex/pcv.rs b/rinex/src/antex/pcv.rs
index ee52a1d6d..5ea4ab113 100644
--- a/rinex/src/antex/pcv.rs
+++ b/rinex/src/antex/pcv.rs
@@ -33,10 +33,7 @@ impl std::str::FromStr for Pcv {
impl Pcv {
pub fn is_relative(&self) -> bool {
- match self {
- Self::Relative(_) => true,
- _ => false,
- }
+ matches!(self, Self::Relative(_))
}
pub fn is_absolute(&self) -> bool {
!self.is_relative()
@@ -58,7 +55,7 @@ mod test {
fn test_pcv() {
assert_eq!(Pcv::default(), Pcv::Absolute);
assert!(Pcv::Absolute.is_absolute());
- assert_eq!(Pcv::Relative(String::from("AOAD/M_T")).is_absolute(), false);
+ assert!(!Pcv::Relative(String::from("AOAD/M_T")).is_absolute());
let pcv = Pcv::from_str("A");
assert!(pcv.is_ok());
diff --git a/rinex/src/antex/record.rs b/rinex/src/antex/record.rs
index 97af7ed2d..245f8d8ab 100644
--- a/rinex/src/antex/record.rs
+++ b/rinex/src/antex/record.rs
@@ -181,16 +181,16 @@ mod test {
#[test]
fn test_new_epoch() {
let content = " START OF ANTENNA";
- assert_eq!(is_new_epoch(content), true);
+ assert!(is_new_epoch(content));
let content =
"TROSAR25.R4 LEIT727259 TYPE / SERIAL NO";
- assert_eq!(is_new_epoch(content), false);
+ assert!(!is_new_epoch(content));
let content =
" 26 # OF FREQUENCIES";
- assert_eq!(is_new_epoch(content), false);
+ assert!(!is_new_epoch(content));
let content =
" G01 START OF FREQUENCY";
- assert_eq!(is_new_epoch(content), false);
+ assert!(!is_new_epoch(content));
}
}
diff --git a/rinex/src/carrier.rs b/rinex/src/carrier.rs
index 715f1b9ef..ad2ed60d2 100644
--- a/rinex/src/carrier.rs
+++ b/rinex/src/carrier.rs
@@ -637,9 +637,14 @@ impl Carrier {
Constellation::Glonass => Self::from_glo_observable(observable),
Constellation::Galileo => Self::from_gal_observable(observable),
Constellation::QZSS => Self::from_qzss_observable(observable),
- Constellation::Geo | Constellation::SBAS(_) => Self::from_geo_observable(observable),
Constellation::IRNSS => Self::from_irnss_observable(observable),
- _ => todo!("from_\"{}:{}\"_observable()", constellation, observable),
+ c => {
+ if c.is_sbas() {
+ Self::from_geo_observable(observable)
+ } else {
+ unreachable!("observable for {}", constellation);
+ }
+ },
}
}
@@ -668,7 +673,7 @@ impl Carrier {
8 => Ok(Self::E5),
_ => Ok(Self::E1),
},
- Constellation::SBAS(_) | Constellation::Geo => match sv.prn {
+ Constellation::SBAS => match sv.prn {
1 => Ok(Self::L1),
5 => Ok(Self::L5),
_ => Ok(Self::L1),
@@ -695,10 +700,7 @@ impl Carrier {
9 => Ok(Self::S),
_ => Ok(Self::L1),
},
- _ => panic!(
- "non supported conversion from {}",
- sv.constellation.to_3_letter_code()
- ),
+ _ => panic!("non supported conversion from {:?}", sv.constellation),
}
}
}
@@ -717,9 +719,9 @@ mod test {
assert_eq!(l1.frequency_mhz(), 1575.42_f64);
assert_eq!(l1.wavelength(), 299792458.0 / 1_575_420_000.0_f64);
- for constell in vec![
+ for constell in [
Constellation::GPS,
- Constellation::Geo,
+ Constellation::SBAS,
Constellation::Glonass,
Constellation::Galileo,
Constellation::BeiDou,
@@ -746,9 +748,9 @@ mod test {
assert_eq!(Carrier::from_observable(constell, &obs), Ok(Carrier::L5),);
}
/*
- * Geo
+ * SBAS
*/
- } else if constell == Constellation::Geo {
+ } else if constell == Constellation::SBAS {
let codes = vec!["C1", "L1C", "D1", "S1", "S1C", "D1C"];
for code in codes {
let obs = Observable::from_str(code).unwrap();
diff --git a/rinex/src/clocks/mod.rs b/rinex/src/clocks/mod.rs
index 949fef590..039d02c26 100644
--- a/rinex/src/clocks/mod.rs
+++ b/rinex/src/clocks/mod.rs
@@ -1,16 +1,16 @@
//! RINEX Clock files parser & analysis
use hifitime::TimeScale;
pub mod record;
-pub use record::{Data, DataType, Error, Record, System};
+pub use record::{ClockData, ClockDataType, Error, Record, System};
/// Clocks `RINEX` specific header fields
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct HeaderFields {
/// Types of observation in this file
- pub codes: Vec,
+ pub codes: Vec,
/// Clock Data analysis production center
- pub agency: Option,
+ pub agency: Option,
/// Reference station
pub station: Option,
/// Reference clock descriptor
@@ -40,7 +40,7 @@ impl HeaderFields {
s
}
/// Set production agency
- pub fn with_agency(&self, agency: Agency) -> Self {
+ pub fn with_agency(&self, agency: ClockAnalysisAgency) -> Self {
let mut s = self.clone();
s.agency = Some(agency);
s
@@ -57,10 +57,10 @@ pub struct Station {
pub id: String,
}
-/// Describes a clock analysis center / agency
+/// Describes a clock analysis agency
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct Agency {
+pub struct ClockAnalysisAgency {
/// IGS AC 3 letter code
pub code: String,
/// agency name
diff --git a/rinex/src/clocks/record.rs b/rinex/src/clocks/record.rs
index d4115ca3e..e19d4dd28 100644
--- a/rinex/src/clocks/record.rs
+++ b/rinex/src/clocks/record.rs
@@ -68,20 +68,25 @@ pub enum Error {
/// Clocks file payload
#[derive(Clone, Debug, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize))]
-pub struct Data {
- /// Clock bias
+pub struct ClockData {
+ /// Clock bias [s]
pub bias: f64,
- pub bias_sigma: Option,
- pub rate: Option,
- pub rate_sigma: Option,
- pub accel: Option,
- pub accel_sigma: Option,
+ /// Clock bias deviation
+ pub bias_dev: Option,
+ /// Clock drift [s/s]
+ pub drift: Option,
+ /// Clock drift deviation
+ pub drift_dev: Option,
+ /// Clock drift change [s/s^2]
+ pub drift_change: Option,
+ /// Clock drift change deviation
+ pub drift_change_dev: Option,
}
/// Clock data observables
#[derive(Debug, PartialEq, Eq, Hash, Clone, EnumString)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub enum DataType {
+pub enum ClockDataType {
/// Data analysis results for receiver clocks
/// derived from a set of network receivers and satellites
AR,
@@ -96,7 +101,7 @@ pub enum DataType {
MS,
}
-impl std::fmt::Display for DataType {
+impl std::fmt::Display for ClockDataType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::AR => f.write_str("AR"),
@@ -109,7 +114,6 @@ impl std::fmt::Display for DataType {
}
/// Clocks RINEX record content.
-/// Data is sorted by [Epoch], by [DataType] and by [System].
/* TODO
/// Example of Clock record browsing:
/// ```
@@ -128,12 +132,12 @@ impl std::fmt::Display for DataType {
/// }
/// ```
*/
-pub type Record = BTreeMap>>;
+pub type Record = BTreeMap>>;
pub(crate) fn is_new_epoch(line: &str) -> bool {
- // first 2 bytes match a DataType code
+ // first 2 bytes match a ClockDataType code
let content = line.split_at(2).0;
- DataType::from_str(content).is_ok()
+ ClockDataType::from_str(content).is_ok()
}
/// Builds `RINEX` record entry for `Clocks` data files.
@@ -142,12 +146,12 @@ pub(crate) fn is_new_epoch(line: &str) -> bool {
pub(crate) fn parse_epoch(
version: Version,
content: &str,
-) -> Result<(Epoch, DataType, System, Data), Error> {
+) -> Result<(Epoch, ClockDataType, System, ClockData), Error> {
let mut lines = content.lines();
let line = lines.next().unwrap();
// Data type code
let (dtype, rem) = line.split_at(3);
- let data_type = DataType::from_str(dtype.trim())?; // must pass
+ let data_type = ClockDataType::from_str(dtype.trim())?; // must pass
let mut rem = rem.clone();
let limit = Version { major: 3, minor: 4 };
@@ -191,15 +195,15 @@ pub(crate) fn parse_epoch(
// nb of data fields
let (n, _) = rem.split_at(4);
- let n = u8::from_str_radix(n.trim(), 10)?;
+ let n = n.trim().parse::()?;
// data fields
- let mut data = Data::default();
+ let mut data = ClockData::default();
let items: Vec<&str> = line.split_ascii_whitespace().collect();
- data.bias = f64::from_str(items[9].trim())?; // bias must pass
+ data.bias = items[9].trim().parse::()?; // bias must pass
if n > 1 {
- if let Ok(f) = f64::from_str(items[10].trim()) {
- data.bias_sigma = Some(f)
+ if let Ok(f) = items[10].trim().parse::() {
+ data.bias_dev = Some(f)
}
}
@@ -207,51 +211,50 @@ pub(crate) fn parse_epoch(
if let Some(l) = lines.next() {
let line = l.clone();
let items: Vec<&str> = line.split_ascii_whitespace().collect();
- for i in 0..items.len() {
- if let Ok(f) = f64::from_str(items[i].trim()) {
+ for (i, item) in items.iter().enumerate() {
+ if let Ok(f) = item.trim().parse::() {
if i == 0 {
- data.rate = Some(f);
+ data.drift = Some(f);
} else if i == 1 {
- data.rate_sigma = Some(f);
+ data.drift_dev = Some(f);
} else if i == 2 {
- data.accel = Some(f);
+ data.drift_change = Some(f);
} else if i == 3 {
- data.accel_sigma = Some(f);
+ data.drift_change_dev = Some(f);
}
}
}
}
}
-
Ok((epoch, data_type, system, data))
}
/// Writes epoch into stream
pub(crate) fn fmt_epoch(
epoch: &Epoch,
- data: &HashMap>,
+ data: &HashMap>,
) -> Result {
let mut lines = String::with_capacity(128);
for (dtype, data) in data.iter() {
for (system, data) in data.iter() {
lines.push_str(&format!("{} {} {} ", dtype, system, epoch));
lines.push_str(&format!("{:.13E} ", data.bias));
- if let Some(sigma) = data.bias_sigma {
+ if let Some(sigma) = data.bias_dev {
lines.push_str(&format!("{:.13E} ", sigma));
}
- if let Some(rate) = data.rate {
- lines.push_str(&format!("{:.13E} ", rate));
+ if let Some(drift) = data.drift {
+ lines.push_str(&format!("{:.13E} ", drift));
}
- if let Some(sigma) = data.rate_sigma {
+ if let Some(sigma) = data.drift_dev {
lines.push_str(&format!("{:.13E} ", sigma));
}
- if let Some(accel) = data.accel {
- lines.push_str(&format!("{:.13E} ", accel));
+ if let Some(drift_change) = data.drift_change {
+ lines.push_str(&format!("{:.13E} ", drift_change));
}
- if let Some(sigma) = data.accel_sigma {
+ if let Some(sigma) = data.drift_change_dev {
lines.push_str(&format!("{:.13E} ", sigma));
}
- lines.push_str("\n");
+ lines.push('\n');
}
}
Ok(lines)
@@ -263,22 +266,22 @@ mod test {
#[test]
fn test_is_new_epoch() {
let c = "AR AREQ 1994 07 14 20 59 0.000000 6 -0.123456789012E+00 -0.123456789012E+01";
- assert_eq!(is_new_epoch(c), true);
+ assert!(is_new_epoch(c));
let c = "RA AREQ 1994 07 14 20 59 0.000000 6 -0.123456789012E+00 -0.123456789012E+01";
- assert_eq!(is_new_epoch(c), false);
+ assert!(!is_new_epoch(c));
let c = "DR AREQ 1994 07 14 20 59 0.000000 6 -0.123456789012E+00 -0.123456789012E+01";
- assert_eq!(is_new_epoch(c), true);
+ assert!(is_new_epoch(c));
let c = "CR AREQ 1994 07 14 20 59 0.000000 6 -0.123456789012E+00 -0.123456789012E+01";
- assert_eq!(is_new_epoch(c), true);
+ assert!(is_new_epoch(c));
let c = "AS AREQ 1994 07 14 20 59 0.000000 6 -0.123456789012E+00 -0.123456789012E+01";
- assert_eq!(is_new_epoch(c), true);
+ assert!(is_new_epoch(c));
let c =
"CR USNO 1995 07 14 20 59 50.000000 2 0.123456789012E+00 -0.123456789012E-01";
- assert_eq!(is_new_epoch(c), true);
+ assert!(is_new_epoch(c));
let c = "AS G16 1994 07 14 20 59 0.000000 2 -0.123456789012E+00 -0.123456789012E+01";
- assert_eq!(is_new_epoch(c), true);
+ assert!(is_new_epoch(c));
let c = "A G16 1994 07 14 20 59 0.000000 2 -0.123456789012E+00 -0.123456789012E+01";
- assert_eq!(is_new_epoch(c), false);
+ assert!(!is_new_epoch(c));
}
}
@@ -298,29 +301,29 @@ impl Merge for Record {
for (system, data) in systems.iter() {
if let Some(ddata) = ssystems.get_mut(system) {
// provide only previously omitted fields
- if let Some(data) = data.bias_sigma {
- if ddata.bias_sigma.is_none() {
- ddata.bias_sigma = Some(data);
+ if let Some(data) = data.bias_dev {
+ if ddata.bias_dev.is_none() {
+ ddata.bias_dev = Some(data);
}
}
- if let Some(data) = data.rate {
- if ddata.rate.is_none() {
- ddata.rate = Some(data);
+ if let Some(data) = data.drift {
+ if ddata.drift.is_none() {
+ ddata.drift = Some(data);
}
}
- if let Some(data) = data.rate_sigma {
- if ddata.rate_sigma.is_none() {
- ddata.rate_sigma = Some(data);
+ if let Some(data) = data.drift_dev {
+ if ddata.drift_dev.is_none() {
+ ddata.drift_dev = Some(data);
}
}
- if let Some(data) = data.accel {
- if ddata.accel.is_none() {
- ddata.accel = Some(data);
+ if let Some(data) = data.drift_change {
+ if ddata.drift_change.is_none() {
+ ddata.drift_change = Some(data);
}
}
- if let Some(data) = data.accel_sigma {
- if ddata.accel_sigma.is_none() {
- ddata.accel_sigma = Some(data);
+ if let Some(data) = data.drift_change_dev {
+ if ddata.drift_change_dev.is_none() {
+ ddata.drift_change_dev = Some(data);
}
}
} else {
@@ -348,7 +351,7 @@ impl Split for Record {
.iter()
.flat_map(|(k, v)| {
if k <= &epoch {
- Some((k.clone(), v.clone()))
+ Some((*k, v.clone()))
} else {
None
}
@@ -358,7 +361,7 @@ impl Split for Record {
.iter()
.flat_map(|(k, v)| {
if k > &epoch {
- Some((k.clone(), v.clone()))
+ Some((*k, v.clone()))
} else {
None
}
@@ -395,9 +398,9 @@ impl Mask for Record {
true // retain other system types
}
});
- systems.len() > 0
+ !systems.is_empty()
});
- dtypes.len() > 0
+ !dtypes.is_empty()
});
},
_ => {}, // TargetItem::
diff --git a/rinex/src/constellation/mod.rs b/rinex/src/constellation/mod.rs
index 2ce18cfb0..37478222b 100644
--- a/rinex/src/constellation/mod.rs
+++ b/rinex/src/constellation/mod.rs
@@ -2,28 +2,22 @@
use hifitime::TimeScale;
use thiserror::Error;
-mod augmentation;
-pub use augmentation::Augmentation;
+//#[cfg(feature = "serde")]
+//use serde::{Deserialize, Serialize};
-#[cfg(feature = "serde")]
-use serde::{Deserialize, Serialize};
+mod sbas;
#[cfg(feature = "sbas")]
-pub use augmentation::sbas_selection_helper;
+pub use sbas::sbas_selection_helper;
/// Constellation parsing & identification related errors
#[derive(Error, Clone, Debug, PartialEq)]
pub enum ParsingError {
#[error("unknown constellation \"{0}\"")]
Unknown(String),
- #[error("unrecognized constellation \"{0}\"")]
- Unrecognized(String),
- #[error("unknown constellation format \"{0}\"")]
- Format(String),
}
/// Describes all known `GNSS` constellations
-/// when manipulating `RINEX`
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Constellation {
@@ -38,14 +32,37 @@ pub enum Constellation {
QZSS,
/// `Galileo` european constellation
Galileo,
- /// `Geo` : stationnary satellite,
- /// also serves as SBAS with unknown augmentation system
- Geo,
- /// `SBAS`
- SBAS(Augmentation),
- /// `IRNSS` constellation,
- /// now officially renamed "NavIC"
+ /// `IRNSS` constellation, renamed "NavIC"
IRNSS,
+ /// American augmentation system,
+ WAAS,
+ /// European augmentation system
+ EGNOS,
+ /// Japanese MTSAT Space Based augmentation system
+ MSAS,
+ /// Indian augmentation system
+ GAGAN,
+ /// Chinese augmentation system
+ BDSBAS,
+ /// South Korean augmentation system
+ KASS,
+ /// Russian augmentation system
+ SDCM,
+ /// South African augmentation system
+ ASBAS,
+ /// Autralia / NZ augmentation system
+ SPAN,
+ /// SBAS is used to describe SBAS (augmentation)
+ /// vehicles without much more information
+ SBAS,
+ /// Australia-NZ Geoscience system
+ AusNZ,
+ /// Group Based SBAS
+ GBAS,
+ /// Nigerian SBAS
+ NSAS,
+ /// Algerian SBAS
+ ASAL,
/// `Mixed` for Mixed constellations
/// RINEX files description
Mixed,
@@ -53,168 +70,156 @@ pub enum Constellation {
impl std::fmt::Display for Constellation {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
- f.write_str(self.to_3_letter_code())
+ write!(f, "{:X}", self)
}
}
impl Constellation {
- /*
- * Identifies GNSS constellation from standard 1 letter code
- * but can insensitive.
- * Mostly used in Self::from_str (public method)
- */
- pub(crate) fn from_1_letter_code(code: &str) -> Result {
- if code.len() != 1 {
- return Err(ParsingError::Format(code.to_string()));
- }
-
- let lower = code.to_lowercase();
- if lower.eq("g") {
- Ok(Self::GPS)
- } else if lower.eq("r") {
- Ok(Self::Glonass)
- } else if lower.eq("c") {
- Ok(Self::BeiDou)
- } else if lower.eq("e") {
- Ok(Self::Galileo)
- } else if lower.eq("j") {
- Ok(Self::QZSS)
- } else if lower.eq("s") {
- Ok(Self::Geo)
- } else if lower.eq("i") {
- Ok(Self::IRNSS)
- } else if lower.eq("m") {
- Ok(Self::Mixed)
- } else {
- Err(ParsingError::Unknown(code.to_string()))
+ /// Returns true if Self is an augmentation system
+ pub fn is_sbas(&self) -> bool {
+ match *self {
+ Constellation::WAAS
+ | Constellation::KASS
+ | Constellation::BDSBAS
+ | Constellation::EGNOS
+ | Constellation::GAGAN
+ | Constellation::SDCM
+ | Constellation::ASBAS
+ | Constellation::SPAN
+ | Constellation::MSAS
+ | Constellation::NSAS
+ | Constellation::ASAL
+ | Constellation::AusNZ
+ | Constellation::SBAS => true,
+ _ => false,
}
}
- /*
- * Identifies Constellation from stanadrd 3 letter code, case insensitive.
- * Used in public Self::from_str, or some place else in that crate.
- */
- pub(crate) fn from_3_letter_code(code: &str) -> Result {
- if code.len() != 3 {
- return Err(ParsingError::Format(code.to_string()));
+ pub(crate) fn is_mixed(&self) -> bool {
+ *self == Constellation::Mixed
+ }
+ /// Returns associated time scale. Returns None
+ /// if related time scale is not supported.
+ pub fn timescale(&self) -> Option {
+ match self {
+ Self::GPS | Self::QZSS => Some(TimeScale::GPST),
+ Self::Galileo => Some(TimeScale::GST),
+ Self::BeiDou => Some(TimeScale::BDT),
+ Self::Glonass => Some(TimeScale::UTC),
+ c => {
+ if c.is_sbas() {
+ Some(TimeScale::GPST)
+ } else {
+ None
+ }
+ },
}
+ }
+}
- let lower = code.to_lowercase();
- if lower.eq("gps") {
+impl std::str::FromStr for Constellation {
+ type Err = ParsingError;
+ fn from_str(string: &str) -> Result {
+ let s = string.trim().to_lowercase();
+ if s.eq("g") || s.contains("gps") {
Ok(Self::GPS)
- } else if lower.eq("glo") {
+ } else if s.eq("r") || s.contains("glo") || s.contains("glonass") {
Ok(Self::Glonass)
- } else if lower.eq("bds") {
+ } else if s.eq("bdsbas") {
+ Ok(Self::BDSBAS)
+ } else if s.eq("c") || s.contains("bds") || s.contains("beidou") {
Ok(Self::BeiDou)
- } else if lower.eq("gal") {
+ } else if s.eq("e") || s.contains("gal") || s.contains("galileo") {
Ok(Self::Galileo)
- } else if lower.eq("qzs") {
+ } else if s.eq("j") || s.contains("qzss") {
Ok(Self::QZSS)
- } else if lower.eq("sbs") | lower.eq("geo") {
- Ok(Self::Geo)
- } else if lower.eq("irn") {
+ } else if s.eq("i") || s.contains("irnss") || s.contains("navic") {
Ok(Self::IRNSS)
- } else {
- Err(ParsingError::Unknown(code.to_string()))
- }
- }
- /*
- * Identifies `gnss` constellation from given standard plain name,
- * like "GPS", or "Galileo". This method is not case sensitive.
- * Used in public Self::from_str, or some place else in that crate.
- */
- pub(crate) fn from_plain_name(code: &str) -> Result {
- let lower = code.to_lowercase();
- if lower.contains("gps") {
- Ok(Self::GPS)
- } else if lower.contains("glonass") {
- Ok(Self::Glonass)
- } else if lower.contains("galileo") {
- Ok(Self::Galileo)
- } else if lower.contains("qzss") {
- Ok(Self::QZSS)
- } else if lower.contains("beidou") {
- Ok(Self::BeiDou)
- } else if lower.contains("sbas") {
- Ok(Self::Geo)
- } else if lower.contains("geo") {
- Ok(Self::Geo)
- } else if lower.contains("irnss") {
- Ok(Self::IRNSS)
- } else if lower.contains("mixed") {
+ } else if s.eq("m") || s.contains("mixed") {
Ok(Self::Mixed)
+ } else if s.eq("ausnz") {
+ Ok(Self::AusNZ)
+ } else if s.eq("egnos") {
+ Ok(Self::EGNOS)
+ } else if s.eq("waas") {
+ Ok(Self::WAAS)
+ } else if s.eq("kass") {
+ Ok(Self::KASS)
+ } else if s.eq("gagan") {
+ Ok(Self::GAGAN)
+ } else if s.eq("asbas") {
+ Ok(Self::ASBAS)
+ } else if s.eq("nsas") {
+ Ok(Self::NSAS)
+ } else if s.eq("asal") {
+ Ok(Self::ASAL)
+ } else if s.eq("msas") {
+ Ok(Self::MSAS)
+ } else if s.eq("span") {
+ Ok(Self::SPAN)
+ } else if s.eq("gbas") {
+ Ok(Self::GBAS)
+ } else if s.eq("sdcm") {
+ Ok(Self::SDCM)
+ } else if s.eq("s") || s.contains("geo") || s.contains("sbas") {
+ Ok(Self::SBAS)
} else {
- Err(ParsingError::Unrecognized(code.to_string()))
- }
- }
- /// Converts self into time scale
- pub fn to_timescale(&self) -> Option {
- match self {
- Self::GPS | Self::QZSS => Some(TimeScale::GPST),
- Self::Galileo => Some(TimeScale::GST),
- Self::BeiDou => Some(TimeScale::BDT),
- Self::Geo | Self::SBAS(_) => Some(TimeScale::GPST),
- // this is wrong but we can't do better
- Self::Glonass | Self::IRNSS => Some(TimeScale::UTC),
- _ => None,
- }
- }
- /// Converts self to 1 letter code (RINEX standard code)
- pub(crate) fn to_1_letter_code(&self) -> &str {
- match self {
- Self::GPS => "G",
- Self::Glonass => "R",
- Self::Galileo => "E",
- Self::BeiDou => "C",
- Self::SBAS(_) | Self::Geo => "S",
- Self::QZSS => "J",
- Self::IRNSS => "I",
- Self::Mixed => "M",
- }
- }
- /* Converts self to 3 letter code (RINEX standard code) */
- pub(crate) fn to_3_letter_code(&self) -> &str {
- match self {
- Self::GPS => "GPS",
- Self::Glonass => "GLO",
- Self::Galileo => "GAL",
- Self::BeiDou => "BDS",
- Self::SBAS(_) | Self::Geo => "GEO",
- Self::QZSS => "QZS",
- Self::IRNSS => "IRN",
- Self::Mixed => "MIX",
+ Err(ParsingError::Unknown(string.to_string()))
}
}
+}
- /// Returns associated time scale. Returns None
- /// if related time scale is not supported.
- pub fn timescale(&self) -> Option {
+impl std::fmt::LowerHex for Constellation {
+ /*
+ * {:x}: formats Self as single letter standard code
+ */
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
- Self::GPS | Self::QZSS => Some(TimeScale::GPST),
- Self::Galileo => Some(TimeScale::GST),
- Self::BeiDou => Some(TimeScale::BDT),
- Self::Geo | Self::SBAS(_) => Some(TimeScale::GPST), // this is correct ?
- _ => None,
+ Self::GPS => write!(f, "G"),
+ Self::Glonass => write!(f, "R"),
+ Self::Galileo => write!(f, "E"),
+ Self::BeiDou => write!(f, "C"),
+ Self::QZSS => write!(f, "J"),
+ Self::IRNSS => write!(f, "I"),
+ c => {
+ if c.is_sbas() {
+ write!(f, "S")
+ } else if c.is_mixed() {
+ write!(f, "M")
+ } else {
+ Err(std::fmt::Error)
+ }
+ },
}
}
}
-impl std::str::FromStr for Constellation {
- type Err = ParsingError;
- /// Identifies `gnss` constellation from given code.
- /// Code should be standard constellation name,
- /// or official 1/3 letter RINEX code.
- /// This method is case insensitive
- fn from_str(code: &str) -> Result {
- if code.len() == 3 {
- Ok(Self::from_3_letter_code(code)?)
- } else if code.len() == 1 {
- Ok(Self::from_1_letter_code(code)?)
- } else if let Ok(s) = Self::from_plain_name(code) {
- Ok(s)
- } else if let Ok(sbas) = Augmentation::from_str(code) {
- Ok(Self::SBAS(sbas))
- } else {
- Err(ParsingError::Unknown(code.to_string()))
+impl std::fmt::UpperHex for Constellation {
+ /*
+ * {:X} formats Self as 3 letter standard code
+ */
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Self::GPS => write!(f, "GPS"),
+ Self::Glonass => write!(f, "GLO"),
+ Self::Galileo => write!(f, "GAL"),
+ Self::BeiDou => write!(f, "BDS"),
+ Self::QZSS => write!(f, "QZSS"),
+ Self::IRNSS => write!(f, "IRNSS"),
+ Self::WAAS => write!(f, "WAAS"),
+ Self::EGNOS => write!(f, "EGNOS"),
+ Self::BDSBAS => write!(f, "BDSBAS"),
+ Self::AusNZ => write!(f, "AUSNZ"),
+ Self::MSAS => write!(f, "MSAS"),
+ Self::NSAS => write!(f, "NSAS"),
+ Self::GBAS => write!(f, "GBAS"),
+ Self::SPAN => write!(f, "SPAN"),
+ Self::GAGAN => write!(f, "GAGAN"),
+ Self::KASS => write!(f, "KASS"),
+ Self::ASBAS => write!(f, "ASBAS"),
+ Self::ASAL => write!(f, "ASAL"),
+ Self::SDCM => write!(f, "SDCM"),
+ Self::Mixed => write!(f, "MIXED"),
+ Self::SBAS => write!(f, "SBAS"),
}
}
}
@@ -225,50 +230,48 @@ mod tests {
use hifitime::TimeScale;
use std::str::FromStr;
#[test]
- fn from_1_letter_code() {
- let c = Constellation::from_1_letter_code("G");
- assert_eq!(c.is_ok(), true);
- assert_eq!(c.unwrap(), Constellation::GPS);
-
- let c = Constellation::from_1_letter_code("R");
- assert_eq!(c.is_ok(), true);
- assert_eq!(c.unwrap(), Constellation::Glonass);
-
- let c = Constellation::from_1_letter_code("M");
- assert_eq!(c.is_ok(), true);
- assert_eq!(c.unwrap(), Constellation::Mixed);
-
- let c = Constellation::from_1_letter_code("J");
- assert_eq!(c.is_ok(), true);
- assert_eq!(c.unwrap(), Constellation::QZSS);
+ fn from_str() {
+ for (desc, expected) in vec![
+ ("G", Ok(Constellation::GPS)),
+ ("GPS", Ok(Constellation::GPS)),
+ ("R", Ok(Constellation::Glonass)),
+ ("GLO", Ok(Constellation::Glonass)),
+ ("J", Ok(Constellation::QZSS)),
+ ("M", Ok(Constellation::Mixed)),
+ ("WAAS", Ok(Constellation::WAAS)),
+ ("KASS", Ok(Constellation::KASS)),
+ ("GBAS", Ok(Constellation::GBAS)),
+ ("NSAS", Ok(Constellation::NSAS)),
+ ("SPAN", Ok(Constellation::SPAN)),
+ ("EGNOS", Ok(Constellation::EGNOS)),
+ ("ASBAS", Ok(Constellation::ASBAS)),
+ ("MSAS", Ok(Constellation::MSAS)),
+ ("GAGAN", Ok(Constellation::GAGAN)),
+ ("BDSBAS", Ok(Constellation::BDSBAS)),
+ ("ASAL", Ok(Constellation::ASAL)),
+ ("SDCM", Ok(Constellation::SDCM)),
+ ] {
+ assert_eq!(
+ Constellation::from_str(desc),
+ expected,
+ "failed to parse constellation from \"{}\"",
+ desc
+ );
+ }
- let c = Constellation::from_1_letter_code("X");
- assert_eq!(c.is_err(), true);
- }
- #[test]
- fn from_3_letter_code() {
- let c = Constellation::from_3_letter_code("GPS");
- assert_eq!(c.is_ok(), true);
- assert_eq!(c.unwrap(), Constellation::GPS);
- let c = Constellation::from_3_letter_code("GLO");
- assert_eq!(c.is_ok(), true);
- assert_eq!(c.unwrap(), Constellation::Glonass);
- let c = Constellation::from_3_letter_code("GPX");
- assert_eq!(c.is_err(), true);
- let c = Constellation::from_3_letter_code("X");
- assert_eq!(c.is_err(), true);
+ for desc in ["X", "x", "GPX", "gpx", "unknown", "blah"] {
+ assert!(Constellation::from_str(desc).is_err());
+ }
}
#[test]
- fn augmentation() {
- let c = Augmentation::from_str("WAAS");
- assert_eq!(c.is_ok(), true);
- assert_eq!(c.unwrap(), Augmentation::WAAS);
- let c = Augmentation::from_str("WASS");
- assert_eq!(c.is_err(), true);
+ fn test_sbas() {
+ for sbas in ["WAAS", "KASS", "EGNOS", "ASBAS", "MSAS", "GAGAN", "ASAL"] {
+ assert!(Constellation::from_str(sbas).unwrap().is_sbas());
+ }
}
#[test]
fn timescale() {
- for (gnss, expected) in vec![
+ for (gnss, expected) in [
(Constellation::GPS, TimeScale::GPST),
(Constellation::Galileo, TimeScale::GST),
(Constellation::BeiDou, TimeScale::BDT),
diff --git a/rinex/src/constellation/augmentation.rs b/rinex/src/constellation/sbas.rs
similarity index 63%
rename from rinex/src/constellation/augmentation.rs
rename to rinex/src/constellation/sbas.rs
index b8edfa3c6..c9c9ce24a 100644
--- a/rinex/src/constellation/augmentation.rs
+++ b/rinex/src/constellation/sbas.rs
@@ -1,37 +1,8 @@
-//! `GNSS` geostationary augmentation systems,
-//! mainly used for high precision positioning
-use strum_macros::EnumString;
-
-#[cfg(feature = "serde")]
-use serde::{Deserialize, Serialize};
-
-#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, EnumString)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-/// GNSS Augmentation systems,
-/// must be used based on current location
-pub enum Augmentation {
- /// Augmentation Unknown
- #[default]
- Unknown,
- /// American augmentation system,
- WAAS,
- /// European augmentation system
- EGNOS,
- /// Japanese augmentation system
- MSAS,
- /// Indian augmentation system
- GAGAN,
- /// Chinese augmentation system
- BDSBAS,
- /// South Korean augmentation system
- KASS,
- /// Russian augmentation system
- SDCM,
- /// South African augmentation system
- ASBAS,
- /// Autralia / NZ augmentation system
- SPAN,
-}
+//! Geostationary augmentation systems
+use crate::prelude::Constellation;
+
+//#[cfg(feature = "serde")]
+//use serde::{Deserialize, Serialize};
#[cfg(feature = "sbas")]
use geo::{point, Contains, LineString};
@@ -68,8 +39,8 @@ where
}
#[cfg(feature = "sbas")]
-fn load_database() -> Vec<(Augmentation, geo::Polygon)> {
- let mut db: Vec<(Augmentation, geo::Polygon)> = Vec::new();
+fn load_database() -> Vec<(Constellation, geo::Polygon)> {
+ let mut db: Vec<(Constellation, geo::Polygon)> = Vec::new();
let db_path = env!("CARGO_MANIFEST_DIR").to_owned() + "/db/SBAS/";
let db_path = std::path::PathBuf::from(db_path);
for entry in std::fs::read_dir(db_path).unwrap() {
@@ -83,7 +54,7 @@ fn load_database() -> Vec<(Augmentation, geo::Polygon)> {
line_string(fullpath), // exterior boundaries
vec![],
); // dont care about interior
- if let Ok(sbas) = Augmentation::from_str(&name.to_uppercase()) {
+ if let Ok(sbas) = Constellation::from_str(&name.to_uppercase()) {
db.push((sbas, poly))
}
}
@@ -101,18 +72,18 @@ fn load_database() -> Vec<(Augmentation, geo::Polygon)> {
///
/// let paris = (48.808378, 2.382682); // lat, lon [ddeg]
/// let sbas = sbas_selection_helper(paris.0, paris.1);
-/// assert_eq!(sbas, Some(Augmentation::EGNOS));
+/// assert_eq!(sbas, Some(Constellation::EGNOS));
///
/// let antartica = (-77.490631, 91.435181); // lat, lon [ddeg]
/// let sbas = sbas_selection_helper(antartica.0, antartica.1);
/// assert_eq!(sbas.is_none(), true);
///```
-pub fn sbas_selection_helper(lat: f64, lon: f64) -> Option {
+pub fn sbas_selection_helper(lat: f64, lon: f64) -> Option {
let db = load_database();
let point: geo::Point = point!(x: lon, y: lat,);
for (sbas, area) in db {
if area.contains(&point) {
- return Some(sbas.clone());
+ return Some(sbas);
}
}
None
@@ -127,63 +98,63 @@ mod test {
fn sbas_helper() {
// PARIS --> EGNOS
let sbas = sbas_selection_helper(48.808378, 2.382682);
- assert_eq!(sbas.is_some(), true);
- assert_eq!(sbas.unwrap(), Augmentation::EGNOS);
+ assert!(sbas.is_some());
+ assert_eq!(sbas.unwrap(), Constellation::EGNOS);
// ANTARICA --> NONE
let sbas = sbas_selection_helper(-77.490631, 91.435181);
- assert_eq!(sbas.is_none(), true);
+ assert!(sbas.is_none());
// LOS ANGELES --> WAAS
let sbas = sbas_selection_helper(33.981431, -118.193601);
- assert_eq!(sbas.is_some(), true);
- assert_eq!(sbas.unwrap(), Augmentation::WAAS);
+ assert!(sbas.is_some());
+ assert_eq!(sbas.unwrap(), Constellation::WAAS);
// ARGENTINA --> NONE
let sbas = sbas_selection_helper(-23.216639, -63.170983);
- assert_eq!(sbas.is_none(), true);
+ assert!(sbas.is_none());
// NIGER --> ASBAS
let sbas = sbas_selection_helper(10.714217, 17.087263);
- assert_eq!(sbas.is_some(), true);
- assert_eq!(sbas.unwrap(), Augmentation::ASBAS);
+ assert!(sbas.is_some());
+ assert_eq!(sbas.unwrap(), Constellation::ASBAS);
// South AFRICA --> None
let sbas = sbas_selection_helper(-32.473320, 21.112770);
- assert_eq!(sbas.is_none(), true);
+ assert!(sbas.is_none());
// India --> GAGAN
let sbas = sbas_selection_helper(19.314290, 76.798953);
- assert_eq!(sbas.is_some(), true);
- assert_eq!(sbas.unwrap(), Augmentation::GAGAN);
+ assert!(sbas.is_some());
+ assert_eq!(sbas.unwrap(), Constellation::GAGAN);
// South Indian Ocean --> None
let sbas = sbas_selection_helper(-29.349172, 72.773447);
- assert_eq!(sbas.is_none(), true);
+ assert!(sbas.is_none());
// Australia --> SPAN
let sbas = sbas_selection_helper(-27.579847, 131.334992);
- assert_eq!(sbas.is_some(), true);
- assert_eq!(sbas.unwrap(), Augmentation::SPAN);
+ assert!(sbas.is_some());
+ assert_eq!(sbas.unwrap(), Constellation::SPAN);
// NZ --> SPAN
let sbas = sbas_selection_helper(-45.113525, 169.864842);
- assert_eq!(sbas.is_some(), true);
- assert_eq!(sbas.unwrap(), Augmentation::SPAN);
+ assert!(sbas.is_some());
+ assert_eq!(sbas.unwrap(), Constellation::SPAN);
// Central China: BDSBAS
let sbas = sbas_selection_helper(34.462967, 98.172480);
- assert_eq!(sbas, Some(Augmentation::BDSBAS));
+ assert_eq!(sbas, Some(Constellation::BDSBAS));
// South Korea: KASS
let sbas = sbas_selection_helper(37.067846, 128.34);
- assert_eq!(sbas, Some(Augmentation::KASS));
+ assert_eq!(sbas, Some(Constellation::KASS));
// Japan: MSAS
let sbas = sbas_selection_helper(36.081095, 138.274859);
- assert_eq!(sbas, Some(Augmentation::MSAS));
+ assert_eq!(sbas, Some(Constellation::MSAS));
// Russia: SDCM
let sbas = sbas_selection_helper(60.004390, 89.090326);
- assert_eq!(sbas, Some(Augmentation::SDCM));
+ assert_eq!(sbas, Some(Constellation::SDCM));
}
}
diff --git a/rinex/src/epoch/mod.rs b/rinex/src/epoch/mod.rs
index d88588cfa..d045a052d 100644
--- a/rinex/src/epoch/mod.rs
+++ b/rinex/src/epoch/mod.rs
@@ -36,7 +36,7 @@ pub enum ParsingError {
* Infaillible `Epoch::now()` call.
*/
pub(crate) fn now() -> Epoch {
- Epoch::now().unwrap_or(Epoch::from_gregorian_utc_at_midnight(2000, 01, 01))
+ Epoch::now().unwrap_or(Epoch::from_gregorian_utc_at_midnight(2000, 1, 1))
}
/*
@@ -141,13 +141,13 @@ pub(crate) fn parse_in_timescale(
let mut mm = 0_u8;
let mut ss = 0_u8;
let mut ns = 0_u32;
- let mut epoch = Epoch::default();
let mut flag = EpochFlag::default();
for (field_index, item) in content.split_ascii_whitespace().enumerate() {
match field_index {
0 => {
- y = i32::from_str_radix(item, 10)
+ y = item
+ .parse::()
.map_err(|_| ParsingError::YearField(item.to_string()))?;
/* old RINEX problem: YY is sometimes encoded on two digits */
@@ -160,29 +160,37 @@ pub(crate) fn parse_in_timescale(
}
},
1 => {
- m = u8::from_str_radix(item, 10)
+ m = item
+ .parse::()
.map_err(|_| ParsingError::MonthField(item.to_string()))?;
},
2 => {
- d = u8::from_str_radix(item, 10)
+ d = item
+ .parse::()
.map_err(|_| ParsingError::DayField(item.to_string()))?;
},
3 => {
- hh = u8::from_str_radix(item, 10)
+ hh = item
+ .parse::()
.map_err(|_| ParsingError::HoursField(item.to_string()))?;
},
4 => {
- mm = u8::from_str_radix(item, 10)
+ mm = item
+ .parse::()
.map_err(|_| ParsingError::MinutesField(item.to_string()))?;
},
5 => {
- if let Some(dot) = item.find(".") {
+ if let Some(dot) = item.find('.') {
let is_nav = item.trim().len() < 7;
- ss = u8::from_str_radix(item[..dot].trim(), 10)
+ ss = item[..dot]
+ .trim()
+ .parse::()
.map_err(|_| ParsingError::SecondsField(item.to_string()))?;
- ns = u32::from_str_radix(item[dot + 1..].trim(), 10)
+ ns = item[dot + 1..]
+ .trim()
+ .parse::()
.map_err(|_| ParsingError::NanosecondsField(item.to_string()))?;
if is_nav {
@@ -193,7 +201,9 @@ pub(crate) fn parse_in_timescale(
ns *= 100;
}
} else {
- ss = u8::from_str_radix(item.trim(), 10)
+ ss = item
+ .trim()
+ .parse::()
.map_err(|_| ParsingError::SecondsField(item.to_string()))?;
}
},
@@ -214,7 +224,9 @@ pub(crate) fn parse_in_timescale(
if y == 0 {
return Err(ParsingError::FormatError);
}
- epoch = Epoch::from_gregorian_utc(y, m, d, hh, mm, ss, ns);
+
+ let epoch = Epoch::from_gregorian_utc(y, m, d, hh, mm, ss, ns);
+ Ok((epoch, flag))
},
_ => {
// in case provided content is totally invalid,
@@ -222,14 +234,20 @@ pub(crate) fn parse_in_timescale(
if y == 0 {
return Err(ParsingError::FormatError);
}
- epoch = Epoch::from_str(&format!(
+ let epoch = Epoch::from_str(&format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09} {}",
- y, m, d, hh, mm, ss, ns, ts
+ y,
+ m,
+ d,
+ hh,
+ mm,
+ ss,
+ ns / 100_000_000,
+ ts
))?;
+ Ok((epoch, flag))
},
}
-
- Ok((epoch, flag))
}
pub(crate) fn parse_utc(s: &str) -> Result<(Epoch, EpochFlag), ParsingError> {
@@ -243,7 +261,7 @@ mod test {
#[test]
fn epoch_parse_nav_v2() {
let e = parse_utc("20 12 31 23 45 0.0");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, flag) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2020);
@@ -261,7 +279,7 @@ mod test {
);
let e = parse_utc("21 1 1 16 15 0.0");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, flag) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2021);
@@ -281,7 +299,7 @@ mod test {
#[test]
fn epoch_parse_nav_v2_nanos() {
let e = parse_utc("20 12 31 23 45 0.1");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, _) = e.unwrap();
let (_, _, _, _, _, ss, ns) = e.to_gregorian_utc();
assert_eq!(ss, 0);
@@ -294,7 +312,7 @@ mod test {
#[test]
fn epoch_parse_nav_v3() {
let e = parse_utc("2021 01 01 00 00 00 ");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, _) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2021);
@@ -311,7 +329,7 @@ mod test {
);
let e = parse_utc("2021 01 01 09 45 00 ");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, _) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2021);
@@ -327,7 +345,7 @@ mod test {
);
let e = parse_utc("2020 06 25 00 00 00");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, _) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2020);
@@ -343,7 +361,7 @@ mod test {
);
let e = parse_utc("2020 06 25 09 49 04");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, _) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2020);
@@ -361,7 +379,7 @@ mod test {
#[test]
fn epoch_parse_obs_v2() {
let e = parse_utc(" 21 12 21 0 0 0.0000000 0");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, flag) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2021);
@@ -379,7 +397,7 @@ mod test {
);
let e = parse_utc(" 21 12 21 0 0 30.0000000 0");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, flag) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2021);
@@ -396,38 +414,38 @@ mod test {
);
let e = parse_utc(" 21 12 21 0 0 30.0000000 1");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (_e, flag) = e.unwrap();
assert_eq!(flag, EpochFlag::PowerFailure);
//assert_eq!(format!("{:o}", e), "21 12 21 0 0 30.0000000 1");
let e = parse_utc(" 21 12 21 0 0 30.0000000 2");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (_e, flag) = e.unwrap();
assert_eq!(flag, EpochFlag::AntennaBeingMoved);
let e = parse_utc(" 21 12 21 0 0 30.0000000 3");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (_e, flag) = e.unwrap();
assert_eq!(flag, EpochFlag::NewSiteOccupation);
let e = parse_utc(" 21 12 21 0 0 30.0000000 4");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (_e, flag) = e.unwrap();
assert_eq!(flag, EpochFlag::HeaderInformationFollows);
let e = parse_utc(" 21 12 21 0 0 30.0000000 5");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (_e, flag) = e.unwrap();
assert_eq!(flag, EpochFlag::ExternalEvent);
let e = parse_utc(" 21 12 21 0 0 30.0000000 6");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (_e, flag) = e.unwrap();
assert_eq!(flag, EpochFlag::CycleSlip);
let e = parse_utc(" 21 1 1 0 0 0.0000000 0");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, flag) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2021);
@@ -441,7 +459,7 @@ mod test {
//assert_eq!(format!("{:o}", e), "21 1 1 0 0 0.0000000 0");
let e = parse_utc(" 21 1 1 0 7 30.0000000 0");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, flag) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2021);
@@ -457,7 +475,7 @@ mod test {
#[test]
fn epoch_parse_obs_v3() {
let e = parse_utc(" 2022 01 09 00 00 0.0000000 0");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, flag) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2022);
@@ -471,7 +489,7 @@ mod test {
//assert_eq!(format!("{}", e), "2022 01 09 00 00 0.0000000 0");
let e = parse_utc(" 2022 01 09 00 13 30.0000000 0");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, flag) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2022);
@@ -485,7 +503,7 @@ mod test {
//assert_eq!(format!("{}", e), "2022 01 09 00 13 30.0000000 0");
let e = parse_utc(" 2022 03 04 00 52 30.0000000 0");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, flag) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2022);
@@ -499,7 +517,7 @@ mod test {
//assert_eq!(format!("{}", e), "2022 03 04 00 52 30.0000000 0");
let e = parse_utc(" 2022 03 04 00 02 30.0000000 0");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, flag) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2022);
@@ -515,7 +533,7 @@ mod test {
#[test]
fn epoch_parse_obs_v2_nanos() {
let e = parse_utc(" 21 1 1 0 7 39.1234567 0");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, _) = e.unwrap();
let (_, _, _, _, _, ss, ns) = e.to_gregorian_utc();
assert_eq!(ss, 39);
@@ -524,7 +542,7 @@ mod test {
#[test]
fn epoch_parse_obs_v3_nanos() {
let e = parse_utc("2022 01 09 00 00 0.1000000 0");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, _) = e.unwrap();
let (_, _, _, _, _, ss, ns) = e.to_gregorian_utc();
assert_eq!(ss, 0);
@@ -532,7 +550,7 @@ mod test {
//assert_eq!(format!("{}", e), "2022 01 09 00 00 0.1000000 0");
let e = parse_utc(" 2022 01 09 00 00 0.1234000 0");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, _) = e.unwrap();
let (_, _, _, _, _, ss, ns) = e.to_gregorian_utc();
assert_eq!(ss, 0);
@@ -540,7 +558,7 @@ mod test {
//assert_eq!(format!("{}", e), "2022 01 09 00 00 0.1234000 0");
let e = parse_utc(" 2022 01 09 00 00 8.7654321 0");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, _) = e.unwrap();
let (_, _, _, _, _, ss, ns) = e.to_gregorian_utc();
assert_eq!(ss, 8);
@@ -550,7 +568,7 @@ mod test {
#[test]
fn epoch_parse_meteo_v2() {
let e = parse_utc(" 22 1 4 0 0 0 ");
- assert_eq!(e.is_ok(), true);
+ assert!(e.is_ok());
let (e, _) = e.unwrap();
let (y, m, d, hh, mm, ss, ns) = e.to_gregorian_utc();
assert_eq!(y, 2022);
diff --git a/rinex/src/ground_position.rs b/rinex/src/ground_position.rs
index 6e9d613e2..64dad7410 100644
--- a/rinex/src/ground_position.rs
+++ b/rinex/src/ground_position.rs
@@ -13,9 +13,9 @@ impl From<(f64, f64, f64)> for GroundPosition {
}
}
-impl Into<(f64, f64, f64)> for GroundPosition {
- fn into(self) -> (f64, f64, f64) {
- (self.0, self.1, self.2)
+impl From for (f64, f64, f64) {
+ fn from(val: GroundPosition) -> Self {
+ (val.0, val.1, val.2)
}
}
diff --git a/rinex/src/hatanaka/compressor.rs b/rinex/src/hatanaka/compressor.rs
index 95b9bd83d..55c87f0e6 100644
--- a/rinex/src/hatanaka/compressor.rs
+++ b/rinex/src/hatanaka/compressor.rs
@@ -1,6 +1,6 @@
//! RINEX compression module
use super::{numdiff::NumDiff, textdiff::TextDiff, Error};
-use crate::is_comment;
+use crate::is_rinex_comment;
use crate::{Constellation, Observable, Sv};
use std::collections::HashMap;
use std::str::FromStr;
@@ -49,17 +49,16 @@ pub struct Compressor {
fn format_epoch_descriptor(content: &str) -> String {
let mut result = String::new();
- result.push_str("&");
+ result.push('&');
for line in content.lines() {
result.push_str(line.trim()) // removes all \tab
}
- result.push_str("\n");
+ result.push('\n');
result
}
-impl Compressor {
- /// Creates a new compression structure
- pub fn new() -> Self {
+impl Default for Compressor {
+ fn default() -> Self {
Self {
first_epoch: true,
epoch_ptr: 0,
@@ -75,7 +74,9 @@ impl Compressor {
forced_init: HashMap::new(),
}
}
+}
+impl Compressor {
/// Identifies amount of vehicles to be provided in next iterations
/// by analyzing epoch descriptor
fn determine_nb_vehicles(&self, content: &str) -> Result {
@@ -83,7 +84,7 @@ impl Compressor {
Err(Error::MalformedEpochDescriptor)
} else {
let nb = &content[30..32];
- if let Ok(u) = u16::from_str_radix(nb.trim(), 10) {
+ if let Ok(u) = nb.trim().parse::() {
//println!("Identified {} vehicles", u); //DEBUG
Ok(u.into())
} else {
@@ -104,9 +105,9 @@ impl Compressor {
if constell_id.is_ascii_digit() {
// in old RINEX + mono constell context
// it is possible that constellation ID is omitted..
- vehicle.insert_str(0, constellation.to_1_letter_code());
+ vehicle.insert_str(0, &format!("{:x}", constellation));
}
- let sv = Sv::from_str(&vehicle)?;
+ let sv = Sv::from_str(vehicle)?;
//println!("VEHICULE: {}", sv); //DEBUG
Ok(sv)
} else {
@@ -120,10 +121,10 @@ impl Compressor {
//println!(">>> VEHICULE CONCLUDED"); //DEBUG
// conclude line with lli/ssi flags
let flags = self.flags_descriptor.trim_end();
- if flags.len() > 0 {
+ if !flags.is_empty() {
result.push_str(flags);
}
- result.push_str("\n");
+ result.push('\n');
self.flags_descriptor.clear();
// move to next vehicle
self.obs_ptr = 0;
@@ -177,7 +178,7 @@ impl Compressor {
loop {
let line: &str = match lines.next() {
Some(l) => {
- if l.trim().len() == 0 {
+ if l.trim().is_empty() {
// line completely empty
// ==> determine if we were expecting content
if self.state == State::Body {
@@ -185,14 +186,14 @@ impl Compressor {
if self.obs_ptr > 0 {
// previously active
// identify current Sv
- if let Ok(sv) = self.current_vehicle(&constellation) {
+ if let Ok(sv) = self.current_vehicle(constellation) {
// nb of obs for this constellation
let sv_nb_obs = observables[&sv.constellation].len();
let nb_missing = std::cmp::min(5, sv_nb_obs - self.obs_ptr);
//println!("Early empty line - missing {} field(s)", nb_missing); //DEBUG
for i in 0..nb_missing {
- result.push_str(" "); // empty whitespace, on each missing observable
- // to remain retro compatible with official tools
+ result.push(' '); // empty whitespace, on each missing observable
+ // to remain retro compatible with official tools
self.flags_descriptor.push_str(" "); // both missing
self.schedule_kernel_init(&sv, self.obs_ptr + i);
}
@@ -217,7 +218,7 @@ impl Compressor {
// println!("\nWorking from LINE : \"{}\"", line); //DEBUG
// [0] : COMMENTS (special case)
- if is_comment!(line) {
+ if is_rinex_comment(line) {
if line.contains("RINEX FILE SPLICE") {
// [0*] SPLICE special comments
// merged RINEX Files
@@ -227,7 +228,7 @@ impl Compressor {
result // feed content as is
.push_str(line);
result // \n dropped by .lines()
- .push_str("\n");
+ .push('\n');
continue;
}
@@ -251,7 +252,7 @@ impl Compressor {
// if we did have clock offset,
// append in a new line
// otherwise append a BLANK
- self.epoch_descriptor.push_str("\n");
+ self.epoch_descriptor.push('\n');
let nb_lines = num_integer::div_ceil(self.nb_vehicles, 12) as u8;
if self.epoch_ptr == nb_lines {
@@ -267,19 +268,19 @@ impl Compressor {
//missing clock offset field here
//next line should not always be empty
/////////////////////////////////////
- result.push_str("\n");
+ result.push('\n');
self.first_epoch = false;
} else {
result.push_str(
- &self.epoch_diff.compress(&self.epoch_descriptor).trim_end(),
+ self.epoch_diff.compress(&self.epoch_descriptor).trim_end(),
);
- result.push_str("\n");
+ result.push('\n');
/////////////////////////////////////
//TODO
//missing clock offset field here
//next line should not always be empty
/////////////////////////////////////
- result.push_str("\n");
+ result.push('\n');
}
self.obs_ptr = 0;
@@ -292,7 +293,7 @@ impl Compressor {
// nb of obs in this line
let nb_obs_line = num_integer::div_ceil(line.len(), 17);
// identify current satellite using stored epoch description
- if let Ok(sv) = self.current_vehicle(&constellation) {
+ if let Ok(sv) = self.current_vehicle(constellation) {
// nb of obs for this constellation
let sv_nb_obs = observables[&sv.constellation].len();
if self.obs_ptr + nb_obs_line > sv_nb_obs {
@@ -302,9 +303,9 @@ impl Compressor {
//println!("SV {} final fields were omitted", sv); //DEBUG
for index in self.obs_ptr..sv_nb_obs + 1 {
self.schedule_kernel_init(&sv, index);
- result.push_str(" "); // put an empty space on missing observables
- // this is how RNX2CRX (official) behaves,
- // if we don't do this we break retro compatibility
+ result.push(' '); // put an empty space on missing observables
+ // this is how RNX2CRX (official) behaves,
+ // if we don't do this we break retro compatibility
self.flags_descriptor.push_str(" ");
}
result = self.conclude_vehicle(&result);
@@ -314,7 +315,7 @@ impl Compressor {
self.nb_vehicles = self.determine_nb_vehicles(line)?;
self.epoch_ptr = 1; // we already have a new descriptor
self.epoch_descriptor.push_str(line);
- self.epoch_descriptor.push_str("\n");
+ self.epoch_descriptor.push('\n');
continue; // avoid end of this loop,
// as this vehicle is now concluded
}
@@ -329,9 +330,9 @@ impl Compressor {
let (data, rem) = observables.split_at(index);
let (obsdata, flags) = data.split_at(14);
observables = rem.clone();
- if let Ok(obsdata) = f64::from_str(obsdata.trim()) {
+ if let Ok(obsdata) = obsdata.trim().parse::() {
let obsdata = f64::round(obsdata * 1000.0) as i64;
- if flags.trim().len() == 0 {
+ if flags.trim().is_empty() {
// Both Flags ommited
//println!("OBS \"{}\" LLI \"X\" SSI \"X\"", obsdata); //DEBUG
// data compression
@@ -357,7 +358,7 @@ impl Compressor {
break;
}
}
- if indexes.len() == 0 {
+ if indexes.is_empty() {
self.forced_init.remove(&sv);
}
} else {
@@ -432,7 +433,7 @@ impl Compressor {
break;
}
}
- if indexes.len() == 0 {
+ if indexes.is_empty() {
self.forced_init.remove(&sv);
}
} else {
@@ -460,17 +461,17 @@ impl Compressor {
diff.1.init(lli);
diff.2.init(ssi);
result.push_str(&format!("3&{} ", obsdata)); //append obs
- if lli.len() > 0 {
+ if !lli.is_empty() {
self.flags_descriptor.push_str(lli);
} else {
- self.flags_descriptor.push_str(" ");
+ self.flags_descriptor.push(' ');
}
- if ssi.len() > 0 {
+ if !ssi.is_empty() {
self.flags_descriptor.push_str(ssi);
} else {
// SSI omitted
- self.flags_descriptor.push_str(" ");
+ self.flags_descriptor.push(' ');
}
sv_diffs.insert(self.obs_ptr, diff);
}
@@ -486,12 +487,12 @@ impl Compressor {
diff.1.init(lli);
diff.2.init(ssi);
self.flags_descriptor.push_str(lli);
- if ssi.len() > 0 {
+ if !ssi.is_empty() {
self.flags_descriptor.push_str(ssi);
} else {
// SSI omitted
diff.2.init(" "); // BLANK
- self.flags_descriptor.push_str(" ");
+ self.flags_descriptor.push(' ');
}
let mut map: HashMap =
HashMap::new();
@@ -503,9 +504,9 @@ impl Compressor {
//obsdata::f64::from_str()
// when floating point parsing is in failure,
// we know this observable is omitted
- result.push_str(" "); // put an empty space on missing observables
- // this is how RNX2CRX (official) behaves,
- // if we don't do this we break retro compatibility
+ result.push(' '); // put an empty space on missing observables
+ // this is how RNX2CRX (official) behaves,
+ // if we don't do this we break retro compatibility
self.flags_descriptor.push_str(" ");
self.schedule_kernel_init(&sv, self.obs_ptr);
}
diff --git a/rinex/src/hatanaka/decompressor.rs b/rinex/src/hatanaka/decompressor.rs
index a82945e39..aa3c2da4a 100644
--- a/rinex/src/hatanaka/decompressor.rs
+++ b/rinex/src/hatanaka/decompressor.rs
@@ -1,6 +1,6 @@
//! RINEX decompression module
use super::{numdiff::NumDiff, textdiff::TextDiff, Error};
-use crate::{is_comment, prelude::*};
+use crate::{is_rinex_comment, prelude::*};
use std::collections::HashMap;
use std::str::FromStr;
@@ -55,7 +55,7 @@ fn format_epoch(
}
let (epoch, systems) = content.split_at(32); // grab epoch
- result.push_str(&epoch.replace("&", " ")); // rework
+ result.push_str(&epoch.replace('&', " ")); // rework
//CRINEX has systems squashed in a single line
// we just split it to match standard definitions
@@ -99,7 +99,7 @@ fn format_epoch(
return Err(Error::FaultyRecoveredEpoch);
}
let (epoch, _) = content.split_at(35);
- result.push_str(&epoch.replace("&", " "));
+ result.push_str(&epoch.replace('&', " "));
//TODO clock offset
if let Some(value) = clock_offset {
result.push_str(&format!(" {:3.12}", (value as f64) / 1000.0_f64))
@@ -183,10 +183,10 @@ impl Decompressor {
) -> Option {
let epoch = &self.epoch_descriptor;
let offset: usize = match crx_major {
- 1 => std::cmp::min((32 + 3 * (sv_ptr + 1)).into(), epoch.len()), // overflow protection
- _ => std::cmp::min((41 + 3 * (sv_ptr + 1)).into(), epoch.len()), // overflow protection
+ 1 => std::cmp::min(32 + 3 * (sv_ptr + 1), epoch.len()), // overflow protection
+ _ => std::cmp::min(41 + 3 * (sv_ptr + 1), epoch.len()), // overflow protection
};
- let system = epoch.split_at(offset.into()).0;
+ let system = epoch.split_at(offset).0;
let (_, svnn) = system.split_at(system.len() - 3); // last 3 XXX
let svnn = svnn.trim();
match crx_major > 2 {
@@ -203,7 +203,7 @@ impl Decompressor {
},
constellation => {
// OLD + FIXED: constellation might be omitted.......
- if let Ok(prn) = u8::from_str_radix(&svnn[1..].trim(), 10) {
+ if let Ok(prn) = u8::from_str_radix(svnn[1..].trim(), 10) {
Some(Sv {
prn,
constellation: *constellation,
@@ -250,7 +250,7 @@ impl Decompressor {
//println!("state: {:?}", self.state);
// [0] : COMMENTS (special case)
- if is_comment!(line) {
+ if is_rinex_comment(line) {
//if line.contains("RINEX FILE SPLICE") {
// [0*] SPLICE special comments
// merged RINEX Files
@@ -258,8 +258,7 @@ impl Decompressor {
//}
result // feed content as is
.push_str(line);
- result // \n dropped by .lines()
- .push_str("\n");
+ result.push('\n');
continue; // move to next line
}
@@ -269,8 +268,7 @@ impl Decompressor {
if line.starts_with("> ") && !self.first_epoch {
result // feed content as is
.push_str(line);
- result // \n dropped by .lines()
- .push_str("\n");
+ result.push('\n');
continue; // move to next line
}
@@ -279,12 +277,12 @@ impl Decompressor {
if self.first_epoch {
match crx_major {
1 => {
- if !line.starts_with("&") {
+ if !line.starts_with('&') {
return Err(Error::FaultyCrx1FirstEpoch);
}
},
3 => {
- if !line.starts_with(">") {
+ if !line.starts_with('>') {
return Err(Error::FaultyCrx3FirstEpoch);
}
},
@@ -312,7 +310,7 @@ impl Decompressor {
* this line is dedicated to clock offset description
*/
let mut clock_offset: Option = None;
- if line.contains("&") {
+ if line.contains('&') {
// clock offset kernel (re)init
let (n, rem) = line.split_at(1);
if let Ok(order) = u8::from_str_radix(n, 10) {
@@ -368,8 +366,7 @@ impl Decompressor {
/*
* identify satellite we're dealing with
*/
- if let Some(sv) = self.current_satellite(crx_major, &crx_constell, self.sv_ptr)
- {
+ if let Some(sv) = self.current_satellite(crx_major, crx_constell, self.sv_ptr) {
//println!("SV: {:?}", sv); //DEBUG
self.sv_ptr += 1; // increment for next time
// vehicles are always described in a single line
@@ -384,7 +381,11 @@ impl Decompressor {
let mut inner: Vec<(NumDiff, TextDiff, TextDiff)> =
Vec::with_capacity(16);
// this protects from malformed Headers or malformed Epoch descriptions
- if let Some(codes) = observables.get(&sv.constellation) {
+ let codes = match sv.constellation.is_sbas() {
+ true => observables.get(&Constellation::SBAS),
+ false => observables.get(&sv.constellation),
+ };
+ if let Some(codes) = codes {
for _ in codes {
let mut kernels = (
NumDiff::new(NumDiff::MAX_COMPRESSION_ORDER)?,
@@ -402,12 +403,16 @@ impl Decompressor {
* iterate over entire line
*/
let mut line = line.trim_end();
- if let Some(codes) = observables.get(&sv.constellation) {
+ let codes = match sv.constellation.is_sbas() {
+ true => observables.get(&Constellation::SBAS),
+ false => observables.get(&sv.constellation),
+ };
+ if let Some(codes) = codes {
while obs_ptr < codes.len() {
if let Some(pos) = line.find(' ') {
let content = &line[..pos];
//println!("OBS \"{}\" - CONTENT \"{}\"", codes[obs_ptr], content); //DEBUG
- if content.len() == 0 {
+ if content.is_empty() {
/*
* missing observation
*/
@@ -417,7 +422,7 @@ impl Decompressor {
* regular progression
*/
if let Some(sv_diff) = self.sv_diff.get_mut(&sv) {
- if let Some(marker) = content.find("&") {
+ if let Some(marker) = content.find('&') {
// kernel (re)initialization
let (order, rem) = content.split_at(marker);
let order = u8::from_str_radix(order.trim(), 10)?;
@@ -453,7 +458,7 @@ impl Decompressor {
*/
//println!("OBS \"{}\" - CONTENT \"{}\"", codes[obs_ptr], line); //DEBUG
if let Some(sv_diff) = self.sv_diff.get_mut(&sv) {
- if let Some(marker) = line.find("&") {
+ if let Some(marker) = line.find('&') {
// kernel (re)initliaization
let (order, rem) = line.split_at(marker);
let order = u8::from_str_radix(order.trim(), 10)?;
@@ -482,7 +487,7 @@ impl Decompressor {
/*
* Flags field
*/
- if line.len() > 0 {
+ if !line.is_empty() {
// can parse at least 1 flag
self.parse_flags(&sv, line);
}
@@ -522,7 +527,7 @@ impl Decompressor {
// old RINEX
if (index + 1).rem_euclid(5) == 0 {
// maximal nb of OBS per line
- result.push_str("\n")
+ result.push('\n')
}
}
}
diff --git a/rinex/src/hatanaka/textdiff.rs b/rinex/src/hatanaka/textdiff.rs
index a22710936..1c0f20b99 100644
--- a/rinex/src/hatanaka/textdiff.rs
+++ b/rinex/src/hatanaka/textdiff.rs
@@ -43,7 +43,7 @@ impl TextDiff {
if s1_len > s0_len {
// got new bytes to latch
- let new_slice = &data[min..s1_len].replace("&", " ");
+ let new_slice = &data[min..s1_len].replace('&', " ");
self.buffer.push_str(new_slice);
}
@@ -63,7 +63,7 @@ impl TextDiff {
if c != &inner[i] {
result.push_str(&c.to_string());
} else {
- result.push_str(" ");
+ result.push(' ');
}
}
}
@@ -71,8 +71,8 @@ impl TextDiff {
for i in inner.len()..data.len() {
if let Some(c) = to_compress.get(i) {
if c.is_ascii_whitespace() {
- self.buffer.push_str("&");
- result.push_str("&");
+ self.buffer.push('&');
+ result.push('&');
} else {
self.buffer.push_str(&c.to_string());
result.push_str(&c.to_string());
diff --git a/rinex/src/header.rs b/rinex/src/header.rs
index d75046869..8e8238d10 100644
--- a/rinex/src/header.rs
+++ b/rinex/src/header.rs
@@ -3,6 +3,7 @@
use super::*;
use crate::{
antex, clocks,
+ clocks::{ClockAnalysisAgency, ClockDataType},
ground_position::GroundPosition,
hardware::{Antenna, Rcvr, SvAntenna},
ionex, leap, meteo, observation,
@@ -19,6 +20,8 @@ use std::str::FromStr;
use strum_macros::EnumString;
use thiserror::Error;
+use crate::{fmt_comment, fmt_rinex};
+
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
@@ -348,11 +351,11 @@ impl Header {
));
}
- let date: Vec<&str> = items[0].split("-").collect();
- let time: Vec<&str> = items[1].split(":").collect();
+ let date: Vec<&str> = items[0].split('-').collect();
+ let time: Vec<&str> = items[1].split(':').collect();
let day = date[0].trim();
- let day = u8::from_str_radix(day, 10).or(Err(ParsingError::DateTimeParsing(
+ let day = day.parse::().or(Err(ParsingError::DateTimeParsing(
String::from("day"),
day.to_string(),
)))?;
@@ -361,19 +364,19 @@ impl Header {
let month = parse_formatted_month(month)?;
let y = date[2].trim();
- let mut y = i32::from_str_radix(y, 10).or(Err(ParsingError::DateTimeParsing(
+ let mut y = y.parse::().or(Err(ParsingError::DateTimeParsing(
String::from("year"),
y.to_string(),
)))?;
let h = time[0].trim();
- let h = u8::from_str_radix(h, 10).or(Err(ParsingError::DateTimeParsing(
+ let h = h.parse::().or(Err(ParsingError::DateTimeParsing(
String::from("hour"),
h.to_string(),
)))?;
let m = time[1].trim();
- let m = u8::from_str_radix(m, 10).or(Err(ParsingError::DateTimeParsing(
+ let m = m.parse::().or(Err(ParsingError::DateTimeParsing(
String::from("minute"),
m.to_string(),
)))?;
@@ -405,13 +408,13 @@ impl Header {
if let Ok(mut pcv) = antex::Pcv::from_str(pcv_str.trim()) {
if pcv.is_relative() {
// try to parse "Relative Type"
- if rel_type.trim().len() > 0 {
+ if !rel_type.trim().is_empty() {
pcv = pcv.with_relative_type(rel_type.trim());
}
}
antex = antex.with_pcv(pcv);
}
- if ref_sn.trim().len() > 0 {
+ if !ref_sn.trim().is_empty() {
antex = antex.with_serial_number(ref_sn.trim())
}
} else if marker.contains("TYPE / SERIAL NO") {
@@ -481,17 +484,17 @@ impl Header {
rinex_type = Type::from_str(type_str.trim())?;
if type_str.contains("GLONASS") {
// old GLONASS NAV : no constellation field
- constellation = Some(Constellation::Glonass)
+ constellation = Some(Constellation::Glonass);
} else if type_str.contains("GPS NAV DATA") {
// old GPS NAV: no constellation field
- constellation = Some(Constellation::GPS)
+ constellation = Some(Constellation::GPS);
} else if type_str.contains("METEOROLOGICAL DATA") {
// these files are not tied to a constellation system,
// therefore, do not have this field
} else {
// regular files
if let Ok(constell) = Constellation::from_str(constell_str.trim()) {
- constellation = Some(constell)
+ constellation = Some(constell);
}
}
/*
@@ -499,7 +502,7 @@ impl Header {
*/
let vers = vers.trim();
version = Version::from_str(vers).or(Err(ParsingError::VersionParsing(
- format!("RINEX VERSION / TYPE \"{}\"", vers.to_string()),
+ format!("RINEX VERSION / TYPE \"{}\"", vers),
)))?;
if !version.is_supported() {
@@ -514,7 +517,7 @@ impl Header {
false => rb.trim().to_string(),
};
let (date_str, _) = rem.split_at(20);
- date = date_str.trim().to_string()
+ date = date_str.trim().to_string();
} else if marker.contains("MARKER NAME") {
station = content.split_at(20).0.trim().to_string()
} else if marker.contains("MARKER NUMBER") {
@@ -522,9 +525,7 @@ impl Header {
} else if marker.contains("MARKER TYPE") {
let code = content.split_at(20).0.trim();
if let Ok(marker) = MarkerType::from_str(code) {
- marker_type = Some(marker)
- } else {
- return Err(ParsingError::MarkerType(code.to_string()));
+ marker_type = Some(marker);
}
} else if marker.contains("OBSERVER / AGENCY") {
let (obs, ag) = content.split_at(20);
@@ -532,7 +533,7 @@ impl Header {
agency = ag.trim().to_string();
} else if marker.contains("REC # / TYPE / VERS") {
if let Ok(receiver) = Rcvr::from_str(content) {
- rcvr = Some(receiver)
+ rcvr = Some(receiver);
}
} else if marker.contains("SYS / PCVS APPLIED") {
let (gnss, rem) = content.split_at(2);
@@ -551,7 +552,7 @@ impl Header {
program.to_string()
}
},
- constellation: gnss.clone(),
+ constellation: gnss,
url: {
let url = url.trim();
if url.eq("") {
@@ -580,7 +581,7 @@ impl Header {
program.to_string()
}
},
- constellation: gnss.clone(),
+ constellation: gnss,
url: {
let url = url.trim();
if url.eq("") {
@@ -601,7 +602,8 @@ impl Header {
let (factor, rem) = rem.split_at(6);
let factor = factor.trim();
- let scaling = u16::from_str_radix(factor, 10)
+ let scaling = factor
+ .parse::()
.or(Err(parse_int_error!("SYS / SCALE FACTOR", factor)))?;
let (_num, rem) = rem.split_at(3);
@@ -800,10 +802,30 @@ impl Header {
// {},
+ Some(c) => {
+ // in case of OLD RINEX : fixed constellation
+ // use that information, as it may be omitted in the TIME OF OBS header
+ time_of_first_obs.time_scale = c
+ .timescale()
+ .ok_or(ParsingError::TimescaleParsing(c.to_string()))?;
+ },
+ }
observation = observation.with_time_of_first_obs(time_of_first_obs);
} else if marker.contains("TIME OF LAST OBS") {
- let time_of_last_obs = Self::parse_time_of_obs(content)?;
+ let mut time_of_last_obs = Self::parse_time_of_obs(content)?;
+ match constellation {
+ Some(Constellation::Mixed) | None => {},
+ Some(c) => {
+ // in case of OLD RINEX : fixed constellation
+ // use that information, as it may be omitted in the TIME OF OBS header
+ time_of_last_obs.time_scale = c
+ .timescale()
+ .ok_or(ParsingError::TimescaleParsing(c.to_string()))?;
+ },
+ }
observation = observation.with_time_of_last_obs(time_of_last_obs);
} else if marker.contains("TYPES OF OBS") {
// these observations can serve both Observation & Meteo RINEX
@@ -814,17 +836,17 @@ impl Header {
match constellation {
Some(Constellation::Mixed) => {
lazy_static! {
- static ref KNOWN_CONSTELLS: Vec = vec![
+ static ref KNOWN_CONSTELLS: [Constellation; 6] = [
Constellation::GPS,
Constellation::Glonass,
Constellation::Galileo,
Constellation::BeiDou,
Constellation::QZSS,
- Constellation::Geo,
+ Constellation::SBAS,
];
}
for c in KNOWN_CONSTELLS.iter() {
- if let Some(codes) = observation.codes.get_mut(&c) {
+ if let Some(codes) = observation.codes.get_mut(c) {
codes.push(observable.clone());
} else {
observation.codes.insert(*c, vec![observable.clone()]);
@@ -849,10 +871,10 @@ impl Header {
}
}
} else if marker.contains("SYS / # / OBS TYPES") {
- let (possible_content, content) = content.split_at(6);
- if possible_content.len() > 0 {
- let code = &possible_content[..1];
- if let Ok(c) = Constellation::from_1_letter_code(code) {
+ let (possible_counter, content) = content.split_at(6);
+ if !possible_counter.is_empty() {
+ let code = &possible_counter[..1];
+ if let Ok(c) = Constellation::from_str(code) {
current_constell = Some(c);
}
}
@@ -863,7 +885,7 @@ impl Header {
let obscode =
&content[i * 4..std::cmp::min((i + 1) * 4, content.len())].trim();
if let Ok(observable) = Observable::from_str(obscode) {
- if obscode.len() > 0 {
+ if !obscode.is_empty() {
if let Some(codes) = observation.codes.get_mut(&constell) {
codes.push(observable);
} else {
@@ -875,20 +897,21 @@ impl Header {
}
} else if marker.contains("ANALYSIS CENTER") {
let (code, agency) = content.split_at(3);
- clocks = clocks.with_agency(clocks::Agency {
+ clocks = clocks.with_agency(ClockAnalysisAgency {
code: code.trim().to_string(),
name: agency.trim().to_string(),
});
} else if marker.contains("# / TYPES OF DATA") {
let (n, r) = content.split_at(6);
let n = n.trim();
- let n =
- u8::from_str_radix(n, 10).or(Err(parse_int_error!("# / TYPES OF DATA", n)))?;
+ let n = n
+ .parse::()
+ .or(Err(parse_int_error!("# / TYPES OF DATA", n)))?;
let mut rem = r.clone();
for _ in 0..n {
let (code, r) = rem.split_at(6);
- if let Ok(c) = clocks::DataType::from_str(code.trim()) {
+ if let Ok(c) = ClockDataType::from_str(code.trim()) {
clocks.codes.push(c);
}
rem = r.clone()
@@ -914,18 +937,22 @@ impl Header {
}
}
} else if marker.contains("GLONASS SLOT / FRQ #") {
+ //TODO
+ // This should be used when dealing with Glonass carriers
+
let slots = content.split_at(4).1.trim();
for i in 0..num_integer::div_ceil(slots.len(), 7) {
let svnn = &slots[i * 7..i * 7 + 4];
let chx = &slots[i * 7 + 4..std::cmp::min(i * 7 + 4 + 3, slots.len())];
if let Ok(svnn) = Sv::from_str(svnn.trim()) {
- if let Ok(chx) = i8::from_str_radix(chx.trim(), 10) {
+ if let Ok(chx) = chx.trim().parse::() {
glo_channels.insert(svnn, chx);
}
}
}
} else if marker.contains("GLONASS COD/PHS/BIS") {
//TODO
+ // This will help RTK solving against GLONASS SV
} else if marker.contains("ION ALPHA") {
//TODO
//0.7451D-08 -0.1490D-07 -0.5960D-07 0.1192D-06 ION ALPHA
@@ -975,19 +1002,19 @@ impl Header {
}
} else if marker.contains("# OF STATIONS") {
// IONEX
- if let Ok(u) = u32::from_str_radix(content.trim(), 10) {
+ if let Ok(u) = content.trim().parse::() {
ionex = ionex.with_nb_stations(u)
}
} else if marker.contains("# OF SATELLITES") {
// IONEX
- if let Ok(u) = u32::from_str_radix(content.trim(), 10) {
+ if let Ok(u) = content.trim().parse::() {
ionex = ionex.with_nb_satellites(u)
}
/*
* Initial TEC map scaling
*/
} else if marker.contains("EXPONENT") {
- if let Ok(e) = i8::from_str_radix(content.trim(), 10) {
+ if let Ok(e) = content.trim().parse::() {
ionex = ionex.with_exponent(e);
}
@@ -1157,411 +1184,6 @@ impl Header {
})
}
- /// Combines self and rhs header into a new header.
- /// Self's attribute are always preferred.
- /// Behavior:
- /// - self's attributes are always preferred (in case of unique attributes)
- /// - observables are concatenated
- /// This fails if :
- /// - RINEX types do not match
- /// - IONEX: map dimensions do not match and grid definitions do not strictly match
- pub fn merge(&self, header: &Self) -> Result {
- if self.rinex_type != header.rinex_type {
- return Err(merge::Error::FileTypeMismatch);
- }
- if self.rinex_type == Type::IonosphereMaps {
- if let Some(i0) = &self.ionex {
- if let Some(i1) = &header.ionex {
- if i0.map_dimension != i1.map_dimension {
- panic!("can only merge ionex files with identical map dimensions")
- }
- }
- }
- }
- Ok(Self {
- version: {
- // retains oldest rev
- if self.version < header.version {
- self.version.clone()
- } else {
- header.version.clone()
- }
- },
- rinex_type: self.rinex_type.clone(),
- comments: {
- self.comments.clone() //TODO: append rhs too!
- },
- leap: {
- if let Some(leap) = self.leap {
- Some(leap.clone())
- } else if let Some(leap) = header.leap {
- Some(leap.clone())
- } else {
- None
- }
- },
- glo_channels: {
- let mut channels = self.glo_channels.clone();
- for (svnn, channel) in &header.glo_channels {
- channels.insert(*svnn, *channel);
- }
- channels
- },
- run_by: self.run_by.clone(),
- program: self.program.clone(),
- observer: self.observer.clone(),
- date: self.date.clone(),
- station: self.station.clone(),
- station_id: self.station_id.clone(),
- station_url: self.station_url.clone(),
- agency: self.agency.clone(),
- license: self.license.clone(),
- doi: self.doi.clone(),
- dcb_compensations: {
- /*
- * DCBs compensations are marked, only if
- * both compensated for in A & B.
- * In this case, resulting data, for a given constellation,
- * is still 100% compensated for.
- */
- if self.dcb_compensations.len() == 0 || header.dcb_compensations.len() == 0 {
- Vec::new() // drop everything
- } else {
- let rhs_constellations: Vec<_> = header
- .dcb_compensations
- .iter()
- .map(|dcb| dcb.constellation.clone())
- .collect();
- let dcbs: Vec = self
- .dcb_compensations
- .clone()
- .iter()
- .filter(|dcb| rhs_constellations.contains(&dcb.constellation))
- .map(|dcb| dcb.clone())
- .collect();
- dcbs
- }
- },
- pcv_compensations: {
- /*
- * Same logic as .dcb_compensations
- */
- if self.pcv_compensations.len() == 0 || header.pcv_compensations.len() == 0 {
- Vec::new() // drop everything
- } else {
- let rhs_constellations: Vec<_> = header
- .pcv_compensations
- .iter()
- .map(|pcv| pcv.constellation.clone())
- .collect();
- let pcvs: Vec = self
- .pcv_compensations
- .clone()
- .iter()
- .filter(|pcv| rhs_constellations.contains(&pcv.constellation))
- .map(|pcv| pcv.clone())
- .collect();
- pcvs
- }
- },
- marker_type: {
- if let Some(mtype) = &self.marker_type {
- Some(mtype.clone())
- } else if let Some(mtype) = &header.marker_type {
- Some(mtype.clone())
- } else {
- None
- }
- },
- gps_utc_delta: {
- if let Some(d) = self.gps_utc_delta {
- Some(d)
- } else if let Some(d) = header.gps_utc_delta {
- Some(d)
- } else {
- None
- }
- },
- data_scaling: {
- if let Some(d) = self.data_scaling {
- Some(d)
- } else if let Some(d) = header.data_scaling {
- Some(d)
- } else {
- None
- }
- },
- constellation: {
- if let Some(c0) = self.constellation {
- if let Some(c1) = header.constellation {
- if c0 != c1 {
- Some(Constellation::Mixed)
- } else {
- Some(c0.clone())
- }
- } else {
- Some(c0.clone())
- }
- } else if let Some(constellation) = header.constellation {
- Some(constellation.clone())
- } else {
- None
- }
- },
- rcvr: {
- if let Some(rcvr) = &self.rcvr {
- Some(rcvr.clone())
- } else if let Some(rcvr) = &header.rcvr {
- Some(rcvr.clone())
- } else {
- None
- }
- },
- rcvr_antenna: {
- if let Some(a) = &self.rcvr_antenna {
- Some(a.clone())
- } else if let Some(a) = &header.rcvr_antenna {
- Some(a.clone())
- } else {
- None
- }
- },
- sv_antenna: {
- if let Some(a) = &self.sv_antenna {
- Some(a.clone())
- } else if let Some(a) = &header.sv_antenna {
- Some(a.clone())
- } else {
- None
- }
- },
- wavelengths: {
- if let Some(wv) = &self.wavelengths {
- Some(wv.clone())
- } else if let Some(wv) = &header.wavelengths {
- Some(wv.clone())
- } else {
- None
- }
- },
- sampling_interval: {
- if let Some(interval) = self.sampling_interval {
- Some(interval.clone())
- } else if let Some(interval) = header.sampling_interval {
- Some(interval.clone())
- } else {
- None
- }
- },
- ground_position: {
- if let Some(pos) = &self.ground_position {
- Some(pos.clone())
- } else if let Some(pos) = &header.ground_position {
- Some(pos.clone())
- } else {
- None
- }
- },
- obs: {
- if let Some(d0) = &self.obs {
- if let Some(d1) = &header.obs {
- Some(observation::HeaderFields {
- time_of_first_obs: std::cmp::min(
- d0.time_of_first_obs,
- d1.time_of_first_obs,
- ),
- time_of_last_obs: std::cmp::max(
- d0.time_of_last_obs,
- d1.time_of_last_obs,
- ),
- crinex: d0.crinex.clone(),
- codes: {
- let mut map = d0.codes.clone();
- for (constellation, obscodes) in d1.codes.iter() {
- if let Some(codes) = map.get_mut(&constellation) {
- for obs in obscodes {
- if !codes.contains(&obs) {
- codes.push(obs.clone());
- }
- }
- } else {
- map.insert(constellation.clone(), obscodes.clone());
- }
- }
- map
- },
- clock_offset_applied: d0.clock_offset_applied
- && d1.clock_offset_applied,
- scalings: HashMap::new(), //TODO
- })
- } else {
- Some(d0.clone())
- }
- } else if let Some(data) = &header.obs {
- Some(data.clone())
- } else {
- None
- }
- },
- meteo: {
- if let Some(m0) = &self.meteo {
- if let Some(m1) = &header.meteo {
- Some(meteo::HeaderFields {
- sensors: {
- let mut sensors = m0.sensors.clone();
- for sens in m1.sensors.iter() {
- if !sensors.contains(&sens) {
- sensors.push(sens.clone())
- }
- }
- sensors
- },
- codes: {
- let mut observables = m0.codes.clone();
- for obs in m1.codes.iter() {
- if !observables.contains(&obs) {
- observables.push(obs.clone())
- }
- }
- observables
- },
- })
- } else {
- Some(m0.clone())
- }
- } else if let Some(meteo) = &header.meteo {
- Some(meteo.clone())
- } else {
- None
- }
- },
- clocks: {
- if let Some(d0) = &self.clocks {
- if let Some(d1) = &header.clocks {
- Some(clocks::HeaderFields {
- codes: {
- let mut codes = d0.codes.clone();
- for code in d1.codes.iter() {
- if !codes.contains(&code) {
- codes.push(code.clone())
- }
- }
- codes
- },
- agency: {
- if let Some(agency) = &d0.agency {
- Some(agency.clone())
- } else if let Some(agency) = &d1.agency {
- Some(agency.clone())
- } else {
- None
- }
- },
- station: {
- if let Some(station) = &d0.station {
- Some(station.clone())
- } else if let Some(station) = &d1.station {
- Some(station.clone())
- } else {
- None
- }
- },
- clock_ref: {
- if let Some(clk) = &d0.clock_ref {
- Some(clk.clone())
- } else if let Some(clk) = &d1.clock_ref {
- Some(clk.clone())
- } else {
- None
- }
- },
- timescale: {
- if let Some(ts) = &d0.timescale {
- Some(ts.clone())
- } else if let Some(ts) = &d1.timescale {
- Some(ts.clone())
- } else {
- None
- }
- },
- })
- } else {
- Some(d0.clone())
- }
- } else if let Some(d1) = &header.clocks {
- Some(d1.clone())
- } else {
- None
- }
- },
- antex: {
- if let Some(d0) = &self.antex {
- Some(d0.clone())
- } else if let Some(data) = &header.antex {
- Some(data.clone())
- } else {
- None
- }
- },
- ionex: {
- if let Some(d0) = &self.ionex {
- if let Some(d1) = &header.ionex {
- Some(ionex::HeaderFields {
- reference: d0.reference.clone(),
- description: {
- if let Some(description) = &d0.description {
- Some(description.clone())
- } else if let Some(description) = &d1.description {
- Some(description.clone())
- } else {
- None
- }
- },
- exponent: std::cmp::min(d0.exponent, d1.exponent), // TODO: this is not correct,
- mapping: {
- if let Some(map) = &d0.mapping {
- Some(map.clone())
- } else if let Some(map) = &d1.mapping {
- Some(map.clone())
- } else {
- None
- }
- },
- map_dimension: d0.map_dimension,
- base_radius: d0.base_radius,
- grid: d0.grid.clone(),
- elevation_cutoff: d0.elevation_cutoff,
- observables: {
- if let Some(obs) = &d0.observables {
- Some(obs.clone())
- } else if let Some(obs) = &d1.observables {
- Some(obs.clone())
- } else {
- None
- }
- },
- nb_stations: std::cmp::max(d0.nb_stations, d1.nb_stations),
- nb_satellites: std::cmp::max(d0.nb_satellites, d1.nb_satellites),
- dcbs: {
- let mut dcbs = d0.dcbs.clone();
- for (b, dcb) in &d1.dcbs {
- dcbs.insert(b.clone(), *dcb);
- }
- dcbs
- },
- })
- } else {
- Some(d0.clone())
- }
- } else if let Some(d1) = &header.ionex {
- Some(d1.clone())
- } else {
- None
- }
- },
- })
- }
-
/// Returns true if self is a `Compressed RINEX`
pub fn is_crinex(&self) -> bool {
if let Some(obs) = &self.obs {
@@ -1675,315 +1297,452 @@ impl Header {
let (ns, rem) = rem.split_at(8);
// println!("Y \"{}\" M \"{}\" D \"{}\" HH \"{}\" MM \"{}\" SS \"{}\" NS \"{}\"", y, m, d, hh, mm, ss, ns); // DEBUG
- let y = u32::from_str_radix(y.trim(), 10)
+ let y = y
+ .trim()
+ .parse::()
.map_err(|_| ParsingError::DateTimeParsing(String::from("year"), y.to_string()))?;
- let m = u8::from_str_radix(m.trim(), 10)
+ let m = m
+ .trim()
+ .parse::()
.map_err(|_| ParsingError::DateTimeParsing(String::from("months"), m.to_string()))?;
- let d = u8::from_str_radix(d.trim(), 10)
+ let d = d
+ .trim()
+ .parse::()
.map_err(|_| ParsingError::DateTimeParsing(String::from("days"), d.to_string()))?;
- let hh = u8::from_str_radix(hh.trim(), 10)
+ let hh = hh
+ .trim()
+ .parse::()
.map_err(|_| ParsingError::DateTimeParsing(String::from("hours"), hh.to_string()))?;
- let mm = u8::from_str_radix(mm.trim(), 10)
+ let mm = mm
+ .trim()
+ .parse::()
.map_err(|_| ParsingError::DateTimeParsing(String::from("minutes"), mm.to_string()))?;
- let ss = u8::from_str_radix(ss.trim(), 10)
+ let ss = ss
+ .trim()
+ .parse::()
.map_err(|_| ParsingError::DateTimeParsing(String::from("seconds"), ss.to_string()))?;
- let ns = u32::from_str_radix(ns.trim(), 10)
+ let ns = ns
+ .trim()
+ .parse::()
.map_err(|_| ParsingError::DateTimeParsing(String::from("nanos"), ns.to_string()))?;
/* timescale might be missing in OLD RINEX: we handle that externally */
let mut ts = TimeScale::TAI;
let rem = rem.trim();
- if rem.len() > 0 {
- // println!("TS \"{}\"", rem); // DBEUG
+ if !rem.is_empty() {
+ // println!("TS \"{}\"", rem); // DBEUGts = TimeScale::from_str(rem.trim()).map_err(|_| {
ts = TimeScale::from_str(rem.trim()).map_err(|_| {
ParsingError::DateTimeParsing(String::from("timescale"), rem.to_string())
})?;
}
- Ok(Epoch::from_str(&format!(
+ Epoch::from_str(&format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:08} {}",
y, m, d, hh, mm, ss, ns, ts
))
- .map_err(|_| ParsingError::DateTimeParsing(String::from("timescale"), rem.to_string()))?)
+ .map_err(|_| ParsingError::DateTimeParsing(String::from("timescale"), rem.to_string()))
}
-}
-impl std::fmt::Display for Header {
- /// `Header` formatter, mainly for RINEX file production purposes
- fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
- // start with CRINEX attributes, if need be
- if let Some(obs) = &self.obs {
- if let Some(crinex) = &obs.crinex {
- write!(f, "{}\n", crinex)?;
- }
- }
- // RINEX VERSION / TYPE
- write!(
- f,
- "{:6}.{:02} ",
- self.version.major, self.version.minor
- )?;
+ /*
+ * Format VERSION/TYPE field
+ */
+ pub(crate) fn fmt_rinex_version_type(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ let major = self.version.major;
+ let minor = self.version.minor;
match self.rinex_type {
- Type::NavigationData => {
- match self.constellation {
- Some(Constellation::Glonass) => {
- // Glonass Special case
- write!(f, "{:<20}", "G: GLONASS NAV DATA")?;
- write!(f, "{:<20}", "")?;
- write!(f, "{}", "RINEX VERSION / TYPE\n")?
- },
- Some(c) => {
- write!(f, "{:<20}", "NAVIGATION DATA")?;
- write!(f, "{:<20}", c.to_1_letter_code())?;
- write!(f, "{:<20}", "RINEX VERSION / TYPE\n")?
- },
- _ => panic!("constellation must be specified when formatting a NavigationData"),
- }
+ Type::NavigationData => match self.constellation {
+ Some(Constellation::Glonass) => {
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!("{:6}.{:02} G: GLONASS NAV DATA", major, minor),
+ "RINEX VERSION / TYPE"
+ )
+ )
+ },
+ Some(c) => {
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!(
+ "{:6}.{:02} NAVIGATION DATA {:X<20}",
+ major, minor, c
+ ),
+ "RINEX VERSION / TYPE"
+ )
+ )
+ },
+ _ => panic!("constellation must be specified when formatting a NavigationData"),
},
Type::ObservationData => match self.constellation {
Some(c) => {
- write!(f, "{:<20}", "OBSERVATION DATA")?;
- write!(f, "{:<20}", c.to_1_letter_code())?;
- write!(f, "{:<20}", "RINEX VERSION / TYPE\n")?
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!(
+ "{:6}.{:02} OBSERVATION DATA {:x<20}",
+ major, minor, c
+ ),
+ "RINEX VERSION / TYPE"
+ )
+ )
},
_ => panic!("constellation must be specified when formatting ObservationData"),
},
Type::MeteoData => {
- write!(f, "{:<20}", "METEOROLOGICAL DATA")?;
- write!(f, "{:<20}", "")?;
- write!(f, "{:<20}", "RINEX VERSION / TYPE\n")?;
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!("{:6}.{:02} METEOROLOGICAL DATA", major, minor),
+ "RINEX VERSION / TYPE"
+ )
+ )
},
Type::ClockData => {
- write!(f, "{:<20}", "CLOCK DATA")?;
- write!(f, "{:<20}", "")?;
- write!(f, "{:<20}", "RINEX VERSION / TYPE\n")?;
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!("{:6}.{:02} CLOCK DATA", major, minor),
+ "RINEX VERSION / TYPE"
+ )
+ )
},
Type::AntennaData => todo!(),
Type::IonosphereMaps => todo!(),
}
- // COMMENTS
- for comment in self.comments.iter() {
- write!(f, "{:<60}", comment)?;
- write!(f, "COMMENT\n")?
+ }
+ /*
+ * Format rinex type dependent stuff
+ */
+ pub(crate) fn fmt_rinex_dependent(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self.rinex_type {
+ Type::ObservationData => self.fmt_observation_rinex(f),
+ Type::MeteoData => self.fmt_meteo_rinex(f),
+ Type::NavigationData => Ok(()),
+ Type::ClockData => self.fmt_clock_rinex(f),
+ Type::IonosphereMaps => self.fmt_ionex(f),
+ Type::AntennaData => Ok(()),
}
- // PGM / RUN BY / DATE
- write!(f, "{:<20}", self.program)?;
- write!(f, "{:<20}", self.run_by)?;
- write!(f, "{:<20}", self.date)?; //TODO
- write!(f, "{}", "PGM / RUN BY / DATE\n")?;
- // OBSERVER / AGENCY
- if self.observer.len() + self.agency.len() > 0 {
- write!(f, "{:<20}", self.observer)?;
- write!(f, "{:<40}", self.agency)?;
- write!(f, "OBSERVER / AGENCY\n")?;
+ }
+ /*
+ * Clock Data fields formatting
+ */
+ fn fmt_clock_rinex(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ if let Some(clocks) = &self.clocks {
+ // Types of data: observables equivalent
+ let mut descriptor = String::new();
+ descriptor.push_str(&format!("{:6}", clocks.codes.len()));
+ for (i, observable) in clocks.codes.iter().enumerate() {
+ if (i % 9) == 0 && i > 0 {
+ descriptor.push_str(" "); // TAB
+ }
+ descriptor.push_str(&format!("{:6}", observable));
+ }
+ writeln!(f, "{}", fmt_rinex(&descriptor, "# / TYPES OF DATA"))?;
+
+ // possible timescale
+ if let Some(ts) = clocks.timescale {
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(&format!(" {:x}", ts), "TIME SYSTEM ID")
+ )?;
+ }
+
+ // possible agency
+ if let Some(agency) = &clocks.agency {
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!("{:<5} {}", agency.code, agency.name),
+ "ANALYSIS CENTER"
+ )
+ )?;
+ }
}
- // MARKER NAME
- if self.station.len() > 0 {
- write!(f, "{:<20}", self.station)?;
- write!(f, "{:<40}", " ")?;
- write!(f, "{}", "MARKER NAME\n")?;
+ Ok(())
+ }
+ /*
+ * IONEX fields formatting
+ */
+ fn fmt_ionex(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ if let Some(ionex) = &self.ionex {
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(&format!("{:6}", ionex.map_dimension), "MAP DIMENSION")
+ )?;
+ // h grid
+ let (start, end, spacing) = (
+ ionex.grid.height.start,
+ ionex.grid.height.end,
+ ionex.grid.height.spacing,
+ );
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!("{} {} {}", start, end, spacing),
+ "HGT1 / HGT2 / DHGT"
+ )
+ )?;
+ // lat grid
+ let (start, end, spacing) = (
+ ionex.grid.latitude.start,
+ ionex.grid.latitude.end,
+ ionex.grid.latitude.spacing,
+ );
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!("{} {} {}", start, end, spacing),
+ "LAT1 / LAT2 / DLAT"
+ )
+ )?;
+ // lon grid
+ let (start, end, spacing) = (
+ ionex.grid.longitude.start,
+ ionex.grid.longitude.end,
+ ionex.grid.longitude.spacing,
+ );
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!("{} {} {}", start, end, spacing),
+ "LON1 / LON2 / DLON"
+ )
+ )?;
+ // elevation cutoff
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(&format!("{}", ionex.elevation_cutoff), "ELEVATION CUTOFF")
+ )?;
+ // mapping func
+ if let Some(func) = &ionex.mapping {
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(&format!("{:?}", func), "MAPPING FUNCTION")
+ )?;
+ } else {
+ writeln!(f, "{}", fmt_rinex("NONE", "MAPPING FUNCTION"))?;
+ }
+ // time of first map
+ writeln!(f, "{}", fmt_rinex("TODO", "EPOCH OF FIRST MAP"))?;
+ // time of last map
+ writeln!(f, "{}", fmt_rinex("TODO", "EPOCH OF LAST MAP"))?;
}
- // MARKER NUMBER
- if self.station_id.len() > 0 {
- // has been parsed
- write!(f, "{:<20}", self.station_id)?;
- write!(f, "{:<40}", " ")?;
- write!(f, "{}", "MARKER NUMBER\n")?;
+ Ok(())
+ }
+ /*
+ * Meteo Data fields formatting
+ */
+ fn fmt_meteo_rinex(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ if let Some(meteo) = &self.meteo {
+ /*
+ * List of observables
+ */
+ let mut descriptor = String::new();
+ descriptor.push_str(&format!("{:6}", meteo.codes.len()));
+ for (i, observable) in meteo.codes.iter().enumerate() {
+ if (i % 9) == 0 && i > 0 {
+ descriptor.push_str(" "); // TAB
+ }
+ descriptor.push_str(&format!(" {}", observable));
+ }
+ writeln!(f, "{}", fmt_rinex(&descriptor, "# / TYPES OF OBSERV"))?;
+ for sensor in &meteo.sensors {
+ write!(f, "{}", sensor)?;
+ }
}
+ Ok(())
+ }
+ /*
+ * Observation Data fields formatting
+ */
+ fn fmt_observation_rinex(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ if let Some(obs) = &self.obs {
+ if let Some(e) = obs.time_of_first_obs {
+ //TODO: hifitime does not have a gregorian decomposition method at the moment
+ //let offset = match time_of_first_obs.time_scale {
+ // TimeScale::GPST => Duration::from_seconds(19.0),
+ // TimeScale::GST => Duration::from_seconds(35.0),
+ // TimeScale::BDT => Duration::from_seconds(35.0),
+ // _ => Duration::default(),
+ //};
+ let (y, m, d, hh, mm, ss, nanos) = e.to_gregorian_utc();
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!(
+ " {:04} {:02} {:02} {:02} {:02} {:02}.{:07} {:x}",
+ y, m, d, hh, mm, ss, nanos, e.time_scale
+ ),
+ "TIME OF FIRST OBS"
+ )
+ )?;
+ }
+ if let Some(e) = obs.time_of_last_obs {
+ let (y, m, d, hh, mm, ss, nanos) = e.to_gregorian_utc();
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!(
+ " {:04} {:02} {:02} {:02} {:02} {:02}.{:07} {:x}",
+ y, m, d, hh, mm, ss, nanos, e.time_scale
+ ),
+ "TIME OF LAST OBS"
+ )
+ )?;
+ }
+ /*
+ * Form the observables list
+ */
+ match self.version.major {
+ 1 | 2 => {
+ /*
+ * List of observables
+ */
+ let mut descriptor = String::new();
+ if let Some((_constell, observables)) = obs.codes.iter().next() {
+ descriptor.push_str(&format!("{:6}", observables.len()));
+ for (i, observable) in observables.iter().enumerate() {
+ if (i % 9) == 0 && i > 0 {
+ descriptor.push_str(" "); // TAB
+ }
+ descriptor.push_str(&format!("{:>6}", observable));
+ }
+ writeln!(f, "{}", fmt_rinex(&descriptor, "# / TYPES OF OBSERV"))?;
+ }
+ },
+ _ => {},
+ }
+ // must take place after list of observables:
+ // TODO scaling factor
+ // TODO DCBS compensations
+ // TODO PCVs compensations
+ }
+ Ok(())
+ }
+ /*
+ * Format all comments
+ */
+ pub(crate) fn fmt_comments(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ for comment in self.comments.iter() {
+ writeln!(f, "{}", fmt_comment(comment))?;
+ }
+ Ok(())
+ }
+}
+
+impl std::fmt::Display for Header {
+ /// `Header` formatter, mainly for RINEX file production purposes
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ // start with CRINEX attributes, if need be
+ if let Some(obs) = &self.obs {
+ if let Some(crinex) = &obs.crinex {
+ writeln!(f, "{}", crinex)?;
+ }
+ }
+
+ self.fmt_rinex_version_type(f)?;
+ self.fmt_comments(f)?;
+
+ // PGM / RUN BY / DATE
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!("{:<20}{:<20}{:<20}", self.program, self.run_by, self.date),
+ "PGM / RUN BY / DATE"
+ )
+ )?;
+
+ // OBSERVER / AGENCY
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!("{:<20}{}", self.observer, self.agency),
+ "OBSERVER /AGENCY"
+ )
+ )?;
+
+ writeln!(f, "{}", fmt_rinex(&self.station, "MARKER NAME"))?;
+ writeln!(f, "{}", fmt_rinex(&self.station_id, "MARKER NUMBER"))?;
+
// ANT
if let Some(antenna) = &self.rcvr_antenna {
- write!(f, "{:<20}", antenna.model)?;
- write!(f, "{:<40}", antenna.sn)?;
- write!(f, "{}", "ANT # / TYPE\n")?;
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!("{:<20}{}", antenna.model, antenna.sn),
+ "ANT # / TYPE"
+ )
+ )?;
if let Some(coords) = &antenna.coords {
- write!(f, "{:14.4}", coords.0)?;
- write!(f, "{:14.4}", coords.1)?;
- write!(f, "{:14.4}", coords.2)?;
- write!(f, "{}", "APPROX POSITION XYZ\n")?
- }
- if let Some(h) = &antenna.height {
- write!(f, "{:14.4}", h)?;
- if let Some(e) = &antenna.eastern {
- write!(f, "{:14.4}", e)?;
- } else {
- write!(f, "{:14.4}", 0.0)?;
- }
- if let Some(n) = &antenna.northern {
- write!(f, "{:14.4}", n)?;
- } else {
- write!(f, "{:14.4}", 0.0)?;
- }
- write!(f, "{:18}", "")?;
- write!(f, "{}", "ANTENNA: DELTA H/E/N\n")?
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!("{:14.4}{:14.4}{:14.4}", coords.0, coords.1, coords.2),
+ "APPROX POSITION XYZ"
+ )
+ )?;
}
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!(
+ "{:14.4}{:14.4}{:14.4}",
+ antenna.height.unwrap_or(0.0),
+ antenna.eastern.unwrap_or(0.0),
+ antenna.northern.unwrap_or(0.0)
+ ),
+ "ANTENNA: DELTA H/E/N"
+ )
+ )?;
}
// RCVR
if let Some(rcvr) = &self.rcvr {
- write!(f, "{:<20}", rcvr.sn)?;
- write!(f, "{:<20}", rcvr.model)?;
- write!(f, "{:<20}", rcvr.firmware)?;
- write!(f, "REC # / TYPE / VERS\n")?
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(
+ &format!("{:<20}{:<20}{}", rcvr.sn, rcvr.model, rcvr.firmware),
+ "REC # / TYPE / VERS"
+ )
+ )?;
}
// INTERVAL
if let Some(interval) = &self.sampling_interval {
- write!(f, "{:6}", interval.to_seconds())?;
- write!(f, "{:<54}", "")?;
- write!(f, "INTERVAL\n")?
- }
- // List of Observables
- match self.rinex_type {
- Type::ObservationData => {
- if let Some(obs) = &self.obs {
- if let Some(time_of_first_obs) = obs.time_of_first_obs {
- //TODO: hifitime does not have a gregorian decomposition method at the moment
- let offset = match time_of_first_obs.time_scale {
- TimeScale::GPST => Duration::from_seconds(19.0),
- TimeScale::GST => Duration::from_seconds(35.0),
- TimeScale::BDT => Duration::from_seconds(35.0),
- _ => Duration::default(),
- };
- let (y, m, d, hh, mm, ss, nanos) = (time_of_first_obs).to_gregorian_utc();
- let mut descriptor = format!(
- " {:04} {:02} {:02} {:02} {:02} {:02}.{:07} {:x}",
- y, m, d, hh, mm, ss, nanos, time_of_first_obs.time_scale
- );
- descriptor.push_str(&format!(
- "{: Duration::from_seconds(19.0),
- TimeScale::GST => Duration::from_seconds(35.0),
- TimeScale::BDT => Duration::from_seconds(35.0),
- _ => Duration::default(),
- };
- let (y, m, d, hh, mm, ss, nanos) =
- (time_of_last_obs + offset).to_gregorian_utc();
- let mut descriptor = format!(
- " {:04} {:02} {:02} {:02} {:02} {:02}.{:08} {:x}",
- y, m, d, hh, mm, ss, nanos, time_of_last_obs.time_scale
- );
- descriptor.push_str(&format!(
- "{: {
- // old revisions
- for (_, observables) in obs.codes.iter() {
- write!(f, "{:6}", observables.len())?;
- let mut descriptor = String::new();
- for i in 0..observables.len() {
- if (i % 9) == 0 && i > 0 {
- //ADD LABEL
- descriptor.push_str("# / TYPES OF OBSERV\n");
- descriptor.push_str(&format!("{:<6}", ""));
- //TAB
- }
- // this will not work if observable
- // does not fit on 2 characters
- descriptor.push_str(&format!(" {}", observables[i]));
- }
- //ADD BLANK on last line
- if observables.len() <= 9 {
- // fits on one line
- descriptor.push_str(&format!(
- "{: {
- // modern revisions
- for (constell, codes) in obs.codes.iter() {
- let mut line = format!("{:<4}", constell.to_1_letter_code());
- line.push_str(&format!("{:2}", codes.len()));
- for i in 0..codes.len() {
- if (i + 1) % 14 == 0 {
- line.push_str(&format!(
- "{: {
- if let Some(obs) = &self.meteo {
- write!(f, "{:6}", obs.codes.len())?;
- let mut description = String::new();
- for i in 0..obs.codes.len() {
- if (i % 9) == 0 && i > 0 {
- description.push_str("# / TYPES OF OBSERV\n");
- write!(f, "{}", description)?;
- description.clear();
- description.push_str(&format!("{:<6}", "")); //TAB
- }
- description.push_str(&format!(" {}", obs.codes[i]));
- }
- description.push_str(&format!(
- "{: {},
+ writeln!(
+ f,
+ "{}",
+ fmt_rinex(&format!("{:6}", interval.to_seconds()), "INTERVAL")
+ )?;
}
- // Must take place after list of Observables:
- //TODO: scale factor, if any
- //TODO: DCBS compensation, if any
- //TODO: PCVs compensation, if any
+
// LEAP
if let Some(leap) = &self.leap {
let mut line = String::new();
@@ -2005,83 +1764,35 @@ impl std::fmt::Display for Header {
));
write!(f, "{}", line)?
}
- // Custom Meteo fields
- if let Some(meteo) = &self.meteo {
- let sensors = &meteo.sensors;
- for sensor in sensors {
- write!(f, "{}", sensor)?
- }
- }
- // Custom Clock fields
- if let Some(clocks) = &self.clocks {
- // Types of data: is the equivalent of Observation codes
- write!(f, "{:6}", clocks.codes.len())?;
- for code in &clocks.codes {
- write!(f, " {}", code)?;
- }
- write!(
- f,
- "{:>width$}\n",
- "# / TYPES OF DATA\n",
- width = 80 - 6 - 6 * clocks.codes.len() - 2
- )?;
- // possible timescale
- if let Some(ts) = clocks.timescale {
- write!(
- f,
- " {:x} TIME SYSTEM ID\n",
- ts
- )?;
- }
- // possible reference agency
- if let Some(agency) = &clocks.agency {
- write!(f, "{:<5} ", agency.code)?;
- write!(f, "{}", agency.name)?;
- write!(f, "ANALYSIS CENTER\n")?;
- }
- // possible reference clock information
- }
- // Custom IONEX fields
- if let Some(ionex) = &self.ionex {
- //TODO:
- // EPOCH OF FIRST and LAST MAP
- // with epoch::format(Ionex)
- let _ = write!(f, "{:6} MAP DIMENSION\n", ionex.map_dimension);
- let h = &ionex.grid.height;
- let _ = write!(
- f,
- "{} {} {} HGT1 / HGT2 / DHGT\n",
- h.start, h.end, h.spacing
- );
- let lat = &ionex.grid.latitude;
- let _ = write!(
- f,
- "{} {} {} LAT1 / LON2 / DLAT\n",
- lat.start, lat.end, lat.spacing
- );
- let lon = &ionex.grid.longitude;
- let _ = write!(
- f,
- "{} {} {} LON1 / LON2 / DLON\n",
- lon.start, lon.end, lon.spacing
- );
- let _ = write!(f, "{} ELEVATION CUTOFF\n", ionex.elevation_cutoff);
- if let Some(func) = &ionex.mapping {
- let _ = write!(f, "{:?} MAPPING FUNCTION\n", func);
- } else {
- let _ = write!(f, "NONE MAPPING FUNCTION\n");
- }
- let _ = write!(f, "{} EXPONENT\n", ionex.exponent);
- if let Some(desc) = &ionex.description {
- for line in 0..desc.len() / 60 {
- let max = std::cmp::min((line + 1) * 60, desc.len());
- let _ = write!(f, "{} COMMENT\n", &desc[line * 60..max]);
- }
- }
- }
- // END OF HEADER
- write!(f, "{:>74}", "END OF HEADER\n")
+ // RINEX Type dependent header
+ self.fmt_rinex_dependent(f)?;
+
+ //TODO
+ // things that could be nice to squeeze in:
+ // [+] SBAS contained (detailed vehicles)
+ // [+] RINEX 3 -> 2 observables conversion (see OBS/V2/rovn as an example)
+ writeln!(f, "{}", fmt_rinex("", "END OF HEADER"))
+ }
+}
+
+impl Header {
+ /*
+ * Macro to be used when marking Self as Merged file
+ */
+ fn merge_comment(timestamp: Epoch) -> String {
+ let (y, m, d, hh, mm, ss, _) = timestamp.to_gregorian_utc();
+ format!(
+ "rustrnx-{:<11} FILE MERGE {}{}{} {}{}{} {:x}",
+ env!("CARGO_PKG_VERSION"),
+ y,
+ m,
+ d,
+ hh,
+ mm,
+ ss,
+ timestamp.time_scale
+ )
}
}
@@ -2111,9 +1822,22 @@ impl Merge for Header {
let (a_rev, b_rev) = (self.version, rhs.version);
self.version = std::cmp::min(a_rev, b_rev);
+ // sampling interval special case
+ match self.sampling_interval {
+ None => {
+ if rhs.sampling_interval.is_some() {
+ self.sampling_interval = rhs.sampling_interval;
+ }
+ },
+ Some(lhs) => {
+ if let Some(rhs) = rhs.sampling_interval {
+ self.sampling_interval = Some(std::cmp::min(lhs, rhs));
+ }
+ },
+ }
+
merge::merge_mut_vec(&mut self.comments, &rhs.comments);
merge::merge_mut_option(&mut self.marker_type, &rhs.marker_type);
- merge::merge_mut_option(&mut self.sampling_interval, &rhs.sampling_interval);
merge::merge_mut_option(&mut self.license, &rhs.license);
merge::merge_mut_option(&mut self.data_scaling, &rhs.data_scaling);
merge::merge_mut_option(&mut self.doi, &rhs.doi);
@@ -2124,6 +1848,41 @@ impl Merge for Header {
merge::merge_mut_option(&mut self.sv_antenna, &rhs.sv_antenna);
merge::merge_mut_option(&mut self.ground_position, &rhs.ground_position);
merge::merge_mut_option(&mut self.wavelengths, &rhs.wavelengths);
+ merge::merge_mut_option(&mut self.gps_utc_delta, &rhs.gps_utc_delta);
+
+ // DCBS compensation is preserved, only if both A&B both have it
+ if self.dcb_compensations.is_empty() || rhs.dcb_compensations.is_empty() {
+ self.dcb_compensations.clear(); // drop everything
+ } else {
+ let rhs_constellations: Vec<_> = rhs
+ .dcb_compensations
+ .iter()
+ .map(|dcb| dcb.constellation)
+ .collect();
+ self.dcb_compensations
+ .iter_mut()
+ .filter(|dcb| rhs_constellations.contains(&dcb.constellation))
+ .count();
+ }
+
+ // PCV compensation : same logic
+ // only preserve compensations present in both A & B
+ if self.pcv_compensations.is_empty() || rhs.pcv_compensations.is_empty() {
+ self.pcv_compensations.clear(); // drop everything
+ } else {
+ let rhs_constellations: Vec<_> = rhs
+ .pcv_compensations
+ .iter()
+ .map(|pcv| pcv.constellation)
+ .collect();
+ self.dcb_compensations
+ .iter_mut()
+ .filter(|pcv| rhs_constellations.contains(&pcv.constellation))
+ .count();
+ }
+
+ //TODO :
+ //merge::merge_mut(&mut self.glo_channels, &rhs.glo_channels);
// RINEX specific operation
if let Some(lhs) = &mut self.antex {
@@ -2151,6 +1910,7 @@ impl Merge for Header {
if let Some(rhs) = &rhs.obs {
merge::merge_mut_option(&mut lhs.crinex, &rhs.crinex);
merge::merge_mut_unique_map2d(&mut lhs.codes, &rhs.codes);
+ // TODO: manage that
lhs.clock_offset_applied |= rhs.clock_offset_applied;
}
}
@@ -2174,6 +1934,10 @@ impl Merge for Header {
if lhs.base_radius != rhs.base_radius {
return Err(merge::Error::IonexBaseRadiusMismatch);
}
+
+ //TODO: this is not enough, need to take into account and rescale..
+ lhs.exponent = std::cmp::min(lhs.exponent, rhs.exponent);
+
merge::merge_mut_option(&mut lhs.description, &rhs.description);
merge::merge_mut_option(&mut lhs.mapping, &rhs.mapping);
if lhs.elevation_cutoff == 0.0 {
@@ -2188,6 +1952,10 @@ impl Merge for Header {
}
}
}
+ // add special comment
+ let now = Epoch::now()?;
+ let merge_comment = Self::merge_comment(now);
+ self.comments.push(merge_comment);
Ok(())
}
}
@@ -2260,7 +2028,7 @@ mod test {
use super::parse_formatted_month;
#[test]
fn formatted_month_parser() {
- for (desc, expected) in vec![("Jan", 1), ("Feb", 2), ("Mar", 3), ("Nov", 11), ("Dec", 12)] {
+ for (desc, expected) in [("Jan", 1), ("Feb", 2), ("Mar", 3), ("Nov", 11), ("Dec", 12)] {
let month = parse_formatted_month(desc);
assert!(month.is_ok(), "failed to parse month from \"{}\"", desc);
let month = month.unwrap();
diff --git a/rinex/src/ionex/grid.rs b/rinex/src/ionex/grid.rs
index eecd067a3..2947ec2c8 100644
--- a/rinex/src/ionex/grid.rs
+++ b/rinex/src/ionex/grid.rs
@@ -112,6 +112,6 @@ mod test {
);
let grid = GridLinspace::new(1.0, 10.0, 1.0).unwrap();
assert_eq!(grid.length(), 10);
- assert_eq!(grid.is_single_point(), false);
+ assert!(!grid.is_single_point());
}
}
diff --git a/rinex/src/ionex/mod.rs b/rinex/src/ionex/mod.rs
index 600fbd9fc..d9694a3ce 100644
--- a/rinex/src/ionex/mod.rs
+++ b/rinex/src/ionex/mod.rs
@@ -1,10 +1,11 @@
//! IONEX module
use super::Sv;
+use hifitime::Epoch;
use std::collections::HashMap;
use strum_macros::EnumString;
pub mod record;
-pub use record::{Map, Record};
+pub use record::{Record, TECPlane, TEC};
pub mod grid;
pub use grid::{Grid, GridLinspace};
@@ -41,6 +42,10 @@ pub enum BiasSource {
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct HeaderFields {
+ /// Epoch of first map
+ pub epoch_of_first_map: Epoch,
+ /// Epoch of last map
+ pub epoch_of_last_map: Epoch,
/// Reference system used for following TEC maps,
/// cf. [system::RefSystem].
pub reference: RefSystem,
@@ -76,6 +81,8 @@ pub struct HeaderFields {
impl Default for HeaderFields {
fn default() -> Self {
Self {
+ epoch_of_first_map: Epoch::default(),
+ epoch_of_last_map: Epoch::default(),
reference: RefSystem::default(),
exponent: -1, // very important: allows missing EXPONENT fields
map_dimension: 2, // 2D map by default
@@ -109,7 +116,7 @@ impl HeaderFields {
pub fn with_description(&self, desc: &str) -> Self {
let mut s = self.clone();
if let Some(ref mut d) = s.description {
- d.push_str(" ");
+ d.push(' ');
d.push_str(desc)
} else {
s.description = Some(desc.to_string())
@@ -129,7 +136,7 @@ impl HeaderFields {
}
pub fn with_observables(&self, o: &str) -> Self {
let mut s = self.clone();
- if o.len() > 0 {
+ if !o.is_empty() {
s.observables = Some(o.to_string())
}
s
@@ -197,14 +204,14 @@ mod test {
fn test_mapping_func() {
let content = "COSZ";
let func = MappingFunction::from_str(content);
- assert_eq!(func.is_ok(), true);
+ assert!(func.is_ok());
assert_eq!(func.unwrap(), MappingFunction::CosZ);
let content = "QFAC";
let func = MappingFunction::from_str(content);
- assert_eq!(func.is_ok(), true);
+ assert!(func.is_ok());
assert_eq!(func.unwrap(), MappingFunction::QFac);
let content = "DONT";
let func = MappingFunction::from_str(content);
- assert_eq!(func.is_err(), true);
+ assert!(func.is_err());
}
}
diff --git a/rinex/src/ionex/record.rs b/rinex/src/ionex/record.rs
index db29edd57..a1abeae75 100644
--- a/rinex/src/ionex/record.rs
+++ b/rinex/src/ionex/record.rs
@@ -1,71 +1,41 @@
use crate::{merge, merge::Merge, prelude::*, split, split::Split};
-use super::{grid, GridLinspace};
+use super::grid;
+use crate::epoch;
use hifitime::Duration;
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashMap};
use std::str::FromStr;
use thiserror::Error;
-pub(crate) fn is_new_tec_map(line: &str) -> bool {
+pub(crate) fn is_new_tec_plane(line: &str) -> bool {
line.contains("START OF TEC MAP")
}
-pub(crate) fn is_new_rms_map(line: &str) -> bool {
+pub(crate) fn is_new_rms_plane(line: &str) -> bool {
line.contains("START OF RMS MAP")
}
-pub(crate) fn is_new_height_map(line: &str) -> bool {
- line.contains("START OF HEIGHT MAP")
-}
-
-/// Returns true if given content describes the start of
-/// a Ionosphere map.
-pub(crate) fn is_new_map(line: &str) -> bool {
- is_new_tec_map(line) || is_new_rms_map(line) || is_new_height_map(line)
-}
+/*
+ * Don't know what Height maps are actually
+ */
+// pub(crate) fn is_new_height_map(line: &str) -> bool {
+// line.contains("START OF HEIGHT MAP")
+// }
-/// A Map is a list of estimates for
-/// a given Latitude, Longitude, Altitude
#[derive(Debug, Clone, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct MapPoint {
- /// Latitude of this estimate
- pub latitude: f64,
- /// Longitude of this estimate
- pub longitude: f64,
- /// Altitude of this estimate
- pub altitude: f64,
- /// Actual estimate (scaling applied)
- pub value: f64,
+pub struct TEC {
+ /// TEC value
+ pub tec: f64,
+ /// RMS(tec)
+ pub rms: Option,
}
-pub type Map = Vec;
+pub type TECPlane = HashMap<(i32, i32), TEC>;
-/*
- * Merges `rhs` into `lhs` in up to 3 dimensions
- */
-fn map_merge3d_mut(lhs: &mut Map, rhs: &Map) {
- for rhs_p in rhs {
- let mut found = false;
- for lhs_p in lhs.into_iter() {
- found |= (lhs_p.latitude == rhs_p.latitude)
- && (lhs_p.longitude == rhs_p.longitude)
- && (lhs_p.altitude == rhs_p.altitude);
- if found {
- break;
- }
- }
- if !found {
- lhs.push(rhs_p.clone());
- }
- }
-}
-
-/// `IONEX` record is sorted by epoch.
-/// For each epoch, a TEC map is always given.
-/// Possible RMS map and Height map may exist at a given epoch.
-/// Ionosphere maps are always given in Earth fixed reference frames.
+/// IONEX contains 2D (fixed altitude) or 3D Ionosphere Maps.
+/// See [Rinex::ionex] and related feature for more information.
/// ```
/// use rinex::prelude::*;
/// use rinex::ionex::*;
@@ -87,33 +57,32 @@ fn map_merge3d_mut(lhs: &mut Map, rhs: &Map) {
/// assert_eq!(params.elevation_cutoff, 0.0);
/// assert_eq!(params.mapping, None); // no mapping function
/// }
-/// let record = rinex.record.as_ionex()
-/// .unwrap();
-/// for (epoch, (tec, rms, height)) in record {
-/// // RMS map never provided in this file
-/// assert_eq!(rms.is_none(), true);
-/// // 2D IONEX: height maps never provided
-/// assert_eq!(height.is_none(), true);
-/// // We only get TEC maps
-/// // when using TEC values, we previously applied all required scalings
-/// for point in tec {
-/// let lat = point.latitude; // in ddeg
-/// let lon = point.longitude; // in ddeg
-/// let alt = point.altitude; // in km
-/// let value = point.value; // correctly scaled ("exponent")
-/// }
-/// }
/// ```
-pub type Record = BTreeMap, Option