1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
//! Observation RINEX module
use super::{epoch, prelude::*, version::Version};
use std::collections::HashMap;
pub mod record;
mod snr;
#[cfg(docrs)]
use crate::Bibliography;
pub use record::{LliFlags, ObservationData, Record};
pub use snr::Snr;
macro_rules! fmt_month {
    ($m: expr) => {
        match $m {
            1 => "Jan",
            2 => "Feb",
            3 => "Mar",
            4 => "Apr",
            5 => "May",
            6 => "Jun",
            7 => "Jul",
            8 => "Aug",
            9 => "Sep",
            10 => "Oct",
            11 => "Nov",
            _ => "Dec",
        }
    };
}
#[cfg(feature = "serde")]
use serde::Serialize;
/// Describes `Compact RINEX` specific information
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Crinex {
    /// Compression program version
    pub version: Version,
    /// Compression program name
    pub prog: String,
    /// Date of compression
    pub date: Epoch,
}
impl Crinex {
    /// Sets compression algorithm revision
    pub fn with_version(&self, version: Version) -> Self {
        let mut s = self.clone();
        s.version = version;
        s
    }
    /// Sets compression program name
    pub fn with_prog(&self, prog: &str) -> Self {
        let mut s = self.clone();
        s.prog = prog.to_string();
        s
    }
    /// Sets compression date
    pub fn with_date(&self, e: Epoch) -> Self {
        let mut s = self.clone();
        s.date = e;
        s
    }
}
impl Default for Crinex {
    fn default() -> Self {
        Self {
            version: Version { major: 3, minor: 0 },
            prog: format!("rust-rinex-{}", env!("CARGO_PKG_VERSION")),
            date: epoch::now(),
        }
    }
}
impl std::fmt::Display for Crinex {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let version = self.version.to_string();
        write!(f, "{:<width$}", version, width = 20)?;
        write!(f, "{:<width$}", "COMPACT RINEX FORMAT", width = 20)?;
        write!(
            f,
            "{value:<width$} CRINEX VERS   / TYPE\n",
            value = "",
            width = 19
        )?;
        write!(f, "{:<width$}", self.prog, width = 20)?;
        write!(f, "{:20}", "")?;
        let (y, m, d, hh, mm, _, _) = self.date.to_gregorian_utc();
        let m = fmt_month!(m);
        let date = format!("{:02}-{}-{} {:02}:{:02}", d, m, y - 2000, hh, mm);
        write!(f, "{:<width$}", date, width = 20)?;
        f.write_str("CRINEX PROG / DATE")
    }
}
/// Describes known marker types
/// Observation Record specific header fields
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct HeaderFields {
    /// Optional CRINEX information
    pub crinex: Option<Crinex>,
    /// Observables per constellation basis
    pub codes: HashMap<Constellation, Vec<Observable>>,
    /// True if local clock drift is compensated for
    pub clock_offset_applied: bool,
    /// DCBs compensation per constellation basis
    pub dcb_compensations: Vec<Constellation>,
    /// Optionnal data scalings
    pub scalings: HashMap<Constellation, HashMap<Observable, f64>>,
}
impl HeaderFields {
    /// Add an optionnal data scaling
    pub fn with_scaling(&self, c: Constellation, observable: Observable, scaling: f64) -> Self {
        let mut s = self.clone();
        if let Some(scalings) = s.scalings.get_mut(&c) {
            scalings.insert(observable, scaling);
        } else {
            let mut map: HashMap<Observable, f64> = HashMap::new();
            map.insert(observable, scaling);
            s.scalings.insert(c, map);
        }
        s
    }
    /// Returns given scaling to apply for given GNSS system
    /// and given observation. Returns 1.0 by default, so it always applies
    pub fn scaling(&self, c: &Constellation, observable: Observable) -> f64 {
        if let Some(scalings) = self.scalings.get(c) {
            if let Some(scaling) = scalings.get(&observable) {
                return *scaling;
            }
        }
        1.0
    }
    /// Emphasize that DCB is compensated for
    pub fn with_dcb_compensation(&self, c: Constellation) -> Self {
        let mut s = self.clone();
        s.dcb_compensations.push(c);
        s
    }
    /// Returns true if DCB compensation was applied for given constellation.
    /// If constellation is None: we test against all encountered constellation
    pub fn dcb_compensation(&self, c: Option<Constellation>) -> bool {
        if let Some(c) = c {
            for comp in &self.dcb_compensations {
                if *comp == c {
                    return true;
                }
            }
            false
        } else {
            for (cst, _) in &self.codes {
                // all encountered constellations
                let mut found = false;
                for ccst in &self.dcb_compensations {
                    // all compensated constellations
                    if ccst == cst {
                        found = true;
                        break;
                    }
                }
                if !found {
                    return false;
                }
            }
            true
        }
    }
}
#[cfg(feature = "obs")]
#[derive(Debug, Clone, Copy)]
pub(crate) enum StatisticalOps {
    Min,
    Max,
    Mean,
    StdDev,
    StdVar,
}
#[cfg(feature = "obs")]
use std::collections::BTreeMap;
/// OBS RINEX specific analysis trait.
/// Include this trait to unlock Observation analysis, mainly statistical analysis.
#[cfg(feature = "obs")]
#[cfg_attr(docrs, doc(cfg(feature = "obs")))]
pub trait Observation {
    /// Returns minimum value observed, throughout all epochs, sorted by Observable.
    /// This also applies to clock receiver estimate,
    /// when requested on OBS RINEX files, not METEO files.
    /// ```
    /// use rinex::*; // prelude + macros
    /// use rinex::prelude::*;
    /// use std::str::FromStr; // observable!
    /// use rinex::observation::Observation; // .min_observable()
    ///
    /// // OBS RINEX example
    /// let rinex = Rinex::from_file("../test_resources/OBS/V3/DUTH0630.22O")
    ///     .unwrap();
    /// let min_values = rinex.min_observable();
    /// for (observable, min_value) in min_values {
    ///     if observable == observable!("S1C") {
    ///         // minimum signal strength for carrier 1
    ///         assert_eq!(min_value, 37.75); // L1 carrier min (worst) RSSI
    ///     }
    /// }
    ///
    /// // METEO RINEX example
    /// let rinex = Rinex::from_file("../test_resources/MET/V2/clar0020.00m")
    ///     .unwrap();
    /// let min_values = rinex.min_observable();
    /// for (observable, min_value) in min_values {
    ///     if observable == Observable::Temperature {
    ///         assert_eq!(min_value, 8.4); // min value encountered on that day
    ///     }
    /// }
    /// ```
    fn min_observable(&self) -> HashMap<Observable, f64>;
    /// Returns maximal value observed, throughout all epochs, sorted by Observable.
    /// See [Self::min_observable()] for API example.
    fn max_observable(&self) -> HashMap<Observable, f64>;
    /// Returns mean observation, throughout all epochs, sorted by Observable.
    /// See [Self::min_observable()] for API example.
    fn mean_observable(&self) -> HashMap<Observable, f64>;
    /// Returns standard deviation for all Observables.
    /// See [Self::min_observable()] for API example.
    fn std_dev_observable(&self) -> HashMap<Observable, f64>;
    /// Returns standard variance for all Observables.
    /// See [Self::min_observable()] for API example.
    fn std_var_observable(&self) -> HashMap<Observable, f64>;
    /// Returns minimum value observed throughout all epochs sorted by
    /// Satellite vehicle and Observable. This does not apply to METEO
    /// RINEX files.
    /// ```
    /// use rinex::*;
    /// use rinex::prelude::*; // basics
    /// use std::str::FromStr; // sv!, observable!
    /// use rinex::observation::Observation; // .min()
    ///
    /// // OBS RINEX example
    /// let rinex = Rinex::from_file("../test_resources/OBS/V3/DUTH0630.22O")
    ///     .unwrap();
    ///
    /// let (min_clock, min_values) = rinex.min();
    /// assert!(min_clock.is_none()); // we don't have an example file with such information yet
    ///
    /// for (sv, observables) in min_values {
    ///     if sv == sv!("G08") {
    ///         for (observable, min_value) in observables {
    ///             if observable == observable!("S1C") {
    ///                 // minimum signal strength for carrier 1
    ///                 // for that particular vehicle
    ///             }
    ///         }
    ///     }
    /// }
    /// ```
    fn min(&self) -> (Option<f64>, HashMap<Sv, HashMap<Observable, f64>>);
    /// Returns maximal value observed throughout all epochs sorted by
    /// Satellite vehicle and Observable. This does not apply to METEO
    /// RINEX files. See [Self::min()] for API example.
    fn max(&self) -> (Option<f64>, HashMap<Sv, HashMap<Observable, f64>>);
    /// Returns mean value observed throughout all epochs sorted by
    /// Satellite vehicle and Observable. This does not apply to METEO
    /// RINEX files. See [Self::min()] for API example.
    fn mean(&self) -> (Option<f64>, HashMap<Sv, HashMap<Observable, f64>>);
    /// Returns observations deviation throughout all epochs sorted by
    /// Satellite vehicle and Observable. This does not apply to METEO
    /// RINEX files. See [Self::min()] for API example.
    fn std_dev(&self) -> (Option<f64>, HashMap<Sv, HashMap<Observable, f64>>);
    /// Returns observations variance throughout all epochs sorted by
    /// Satellite vehicle and Observable. This does not apply to METEO
    /// RINEX files. See [Self::min()] for API example.
    fn std_var(&self) -> (Option<f64>, HashMap<Sv, HashMap<Observable, f64>>);
}
/// GNSS signal recombination trait.    
/// Import this to recombine OBS RINEX with usual recombination methods.   
/// This only applies to OBS RINEX records.  
/// Refer to [Bibliography::ESAGnssCombination] and [Bibliography::ESABookVol1]
/// for more information.
#[cfg(feature = "obs")]
#[cfg_attr(docrs, doc(cfg(feature = "obs")))]
pub trait Combine {
    /// Perform Geometry Free signal recombination on all phase
    /// and pseudo range observations, for each individual Sv
    /// and individual Epoch.   
    /// Geometry Free (Gf) recombination cancels out geometric
    /// biases and leaves frequency dependent terms out,
    /// like Ionospheric induced time delay.  
    /// ```
    /// use rinex::prelude::*;
    /// use rinex::observation::*;
    ///
    /// let rinex = Rinex::from_file("../test_resources/OBS/V3/DUTH0630.22O")
    ///		.unwrap();
    ///
    /// let gf = rinex.geo_free();
    /// for ((ref_observable, rhs_observable), data) in gf {
    ///     // for each recombination that we were able to form,
    ///     // a "reference" observable was chosen,
    ///     // and RHS observable is compared to it.
    ///     // For example "L2C-L1C" : L1C is the reference observable
    ///     for (sv, epochs) in data {
    ///         // applied to all possible Sv
    ///         for ((epoch, _flag), value) in epochs {
    ///             // value: actual recombination result
    ///         }
    ///     }
    /// }
    /// ```
    fn geo_free(
        &self,
    ) -> HashMap<(Observable, Observable), BTreeMap<Sv, BTreeMap<(Epoch, EpochFlag), f64>>>;
    /// Perform Wide Lane recombination.   
    /// See [Self::geo_free] for API example.
    fn wide_lane(
        &self,
    ) -> HashMap<(Observable, Observable), BTreeMap<Sv, BTreeMap<(Epoch, EpochFlag), f64>>>;
    /// Perform Narrow Lane recombination.   
    /// See [Self::geo_free] for API example.
    fn narrow_lane(
        &self,
    ) -> HashMap<(Observable, Observable), BTreeMap<Sv, BTreeMap<(Epoch, EpochFlag), f64>>>;
    /// Perform Melbourne-Wübbena recombination.   
    /// See [`Self::geo_free`] for API example.
    fn melbourne_wubbena(
        &self,
    ) -> HashMap<(Observable, Observable), BTreeMap<Sv, BTreeMap<(Epoch, EpochFlag), f64>>>;
}
/// GNSS code bias estimation trait.
/// Refer to [Bibliography::ESAGnssCombination] and [Bibliography::ESABookVol1].
#[cfg(feature = "obs")]
#[cfg_attr(docrs, doc(cfg(feature = "obs")))]
pub trait Dcb {
    /// Returns Differential Code Bias estimates, sorted per (unique)
    /// signals combinations and for each individual Sv.
    /// ```
    /// use rinex::prelude::*;
    /// use rinex::observation::*; // .dcb()
    ///
    /// let rinex = Rinex::from_file("../test_resources/OBS/V3/DUTH0630.22O")
    ///		.unwrap();
    /// let dcb = rinex.dcb();
    /// ```
    fn dcb(&self) -> HashMap<String, BTreeMap<Sv, BTreeMap<(Epoch, EpochFlag), f64>>>;
}
/// Multipath biases estimation.
/// Refer to [Bibliography::ESABookVol1] and [Bibliography::MpTaoglas].
#[cfg(feature = "obs")]
#[cfg_attr(docrs, doc(cfg(feature = "obs")))]
pub trait Mp {
    /// Returns Multipath bias estimates,
    /// sorted per (unique) signal combinations and for each individual Sv.
    fn mp(&self) -> HashMap<String, BTreeMap<Sv, BTreeMap<(Epoch, EpochFlag), f64>>>;
}
/// Ionospheric Delay estimation trait.
#[cfg(feature = "obs")]
#[cfg_attr(docrs, doc(cfg(feature = "obs")))]
pub trait IonoDelay {
    /// The Iono delay estimator is the derivative of the [Combine::geo_free]
    /// recombination. One can then use a peak detector for example,
    /// to determine signal perturbations, due to ionospheric activity.
    /// To improve behavior and avoid discontinuities on data gaps,
    /// we perform the derivative only if the previous point was sampled at worst
    /// `max_dt` prior current point.  
    /// This is intended to be used on raw Phase data only,
    /// but can be evaluated on PR too (if such data is passed).  
    /// In that scenario, ideally the user used a smoothing algorithm,
    /// prior to invoking this method: see the preprocessing toolkit.
    fn iono_delay(
        &self,
        max_dt: Duration,
    ) -> HashMap<Observable, HashMap<Sv, BTreeMap<Epoch, f64>>>;
}
#[cfg(test)]
mod crinex {
    use super::*;
    #[test]
    fn test_fmt_month() {
        assert_eq!(fmt_month!(1), "Jan");
        assert_eq!(fmt_month!(2), "Feb");
        assert_eq!(fmt_month!(3), "Mar");
        assert_eq!(fmt_month!(10), "Oct");
        assert_eq!(fmt_month!(11), "Nov");
        assert_eq!(fmt_month!(12), "Dec");
    }
    #[test]
    fn test_display() {
        let crinex = Crinex::default();
        let now = Epoch::now().unwrap();
        let (_y, _m, _d, _hh, _mm, _, _) = now.to_gregorian_utc();
        let content = crinex.to_string();
        let lines: Vec<&str> = content.lines().collect();
        assert_eq!(lines.len(), 2); // main title should span 2 lines
        // test first line
        let expected =
            "3.0                 COMPACT RINEX FORMAT                    CRINEX VERS   / TYPE";
        assert_eq!(expected, lines[0]);
        // test second line width : must follow RINEX standards
        //assert_eq!(lines[1].len(), 80);
    }
}