#![doc(
html_logo_url = "https://raw.githubusercontent.com/nav-solutions/.github/master/logos/logo2.jpg"
)]
#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(clippy::type_complexity)]
extern crate num_derive;
#[cfg(feature = "serde")]
#[macro_use]
extern crate serde;
extern crate gnss_rs as gnss;
extern crate num;
pub mod bias;
pub mod coordinates;
pub mod error;
pub mod grid;
pub mod header;
pub mod key;
pub mod linspace;
pub mod mapf;
pub mod production;
pub mod quantized;
pub mod system;
pub mod tec;
pub mod version;
mod epoch;
mod formatting;
mod ionosphere;
mod parsing;
#[cfg(test)]
mod tests;
use std::{
fs::File,
io::{BufReader, BufWriter, Read, Write},
path::Path,
str::FromStr,
};
use geo::{coord, Rect};
#[cfg(feature = "flate2")]
use flate2::{read::GzDecoder, write::GzEncoder, Compression as GzCompression};
use std::collections::BTreeMap;
use hifitime::prelude::Epoch;
use crate::{
error::{FormattingError, ParsingError},
formatting::format_record,
header::Header,
key::Key,
parsing::parse_record,
production::{ProductionAttributes, Region},
tec::TEC,
};
pub mod prelude {
pub use crate::{
bias::BiasSource,
coordinates::QuantizedCoordinates,
error::{FormattingError, ParsingError},
grid::Grid,
header::Header,
key::Key,
linspace::Linspace,
mapf::MappingFunction,
production::*,
quantized::Quantized,
system::ReferenceSystem,
tec::TEC,
version::Version,
Comments, Record, IONEX,
};
pub use gnss::prelude::{Constellation, SV};
pub use hifitime::{Duration, Epoch, TimeScale, TimeSeries};
}
pub type Comments = Vec<String>;
pub type Record = BTreeMap<Key, TEC>;
pub(crate) fn is_comment(content: &str) -> bool {
content.len() > 60 && content.trim_end().ends_with("COMMENT")
}
pub(crate) fn fmt_ionex(content: &str, marker: &str) -> String {
if content.len() < 60 {
format!("{:<padding$}{}", content, marker, padding = 60)
} else {
let mut string = String::new();
let nb_lines = num_integer::div_ceil(content.len(), 60);
for i in 0..nb_lines {
let start_off = i * 60;
let end_off = std::cmp::min(start_off + 60, content.len());
let chunk = &content[start_off..end_off];
string.push_str(&format!("{:<padding$}{}", chunk, marker, padding = 60));
if i < nb_lines - 1 {
string.push('\n');
}
}
string
}
}
pub(crate) fn fmt_comment(content: &str) -> String {
fmt_ionex(content, "COMMENT")
}
#[derive(Clone, Debug)]
pub struct IONEX {
pub header: Header,
pub comments: Comments,
pub record: Record,
pub production: Option<ProductionAttributes>,
}
impl IONEX {
pub fn new(header: Header, record: Record) -> Self {
Self {
header,
record,
production: None,
comments: Default::default(),
}
}
pub fn with_header(&self, header: Header) -> Self {
Self {
header,
record: self.record.clone(),
comments: self.comments.clone(),
production: self.production.clone(),
}
}
pub fn replace_header(&mut self, header: Header) {
self.header = header.clone();
}
pub fn with_record(&self, record: Record) -> Self {
IONEX {
record,
header: self.header.clone(),
comments: self.comments.clone(),
production: self.production.clone(),
}
}
pub fn replace_record(&mut self, record: Record) {
self.record = record.clone();
}
pub fn is_2d(&self) -> bool {
self.header.map_dimension == 2
}
pub fn is_3d(&self) -> bool {
!self.is_2d()
}
pub fn map_borders(&self) -> Rect {
Rect::new(coord!( x: 0.0, y: 0.0 ), coord!( x: 0.0, y: 0.0))
}
pub fn altitude_width_km(&self) -> f64 {
self.header.grid.altitude.width()
}
pub fn tec_maps_iter(&self) -> Box<dyn Iterator<Item = (&Key, &TEC)> + '_> {
Box::new(self.record.iter())
}
pub fn standardized_filename(&self) -> String {
let (agency, region, year, doy) = if let Some(production) = &self.production {
(
production.agency.clone(),
production.region,
production.year - 2000,
production.doy,
)
} else {
("XXX".to_string(), Region::default(), 0, 0)
};
let extension = if let Some(production) = &self.production {
#[cfg(feature = "flate2")]
if production.gzip_compressed {
".gz"
} else {
""
}
} else {
""
};
format!("{}{}{:03}.{:02}I{}", agency, region, doy, year, extension)
}
pub fn guess_production_attributes(&self, agency: &str) -> Option<ProductionAttributes> {
if agency.len() < 3 {
return None;
}
let first_epoch = self.first_epoch()?;
let year = first_epoch.year();
let doy = first_epoch.day_of_year().round() as u32;
let region = Region::Global;
Some(ProductionAttributes {
doy,
region,
year: year as u32,
agency: agency.to_string(),
#[cfg(feature = "flate2")]
gzip_compressed: if let Some(attributes) = &self.production {
attributes.gzip_compressed
} else {
false
},
})
}
pub fn parse<R: Read>(reader: &mut BufReader<R>) -> Result<Self, ParsingError> {
let mut header = Header::parse(reader)?;
let (record, comments) = parse_record(&mut header, reader)?;
Ok(Self {
header,
comments,
record,
production: Default::default(),
})
}
pub fn format<W: Write>(&self, writer: &mut BufWriter<W>) -> Result<(), FormattingError> {
self.header.format(writer)?;
format_record(writer, &self.record, &self.header)?;
writer.flush()?;
Ok(())
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<IONEX, ParsingError> {
let path = path.as_ref();
let file_attributes = match path.file_name() {
Some(filename) => {
let filename = filename.to_string_lossy().to_string();
if let Ok(prod) = ProductionAttributes::from_str(&filename) {
Some(prod)
} else {
None
}
},
_ => None,
};
let fd = File::open(path)?;
let mut reader = BufReader::new(fd);
let mut ionex = Self::parse(&mut reader)?;
ionex.production = file_attributes;
Ok(ionex)
}
pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), FormattingError> {
let fd = File::create(path)?;
let mut writer = BufWriter::new(fd);
self.format(&mut writer)?;
Ok(())
}
#[cfg(feature = "flate2")]
#[cfg_attr(docsrs, doc(cfg(feature = "flate2")))]
pub fn from_gzip_file<P: AsRef<Path>>(path: P) -> Result<IONEX, ParsingError> {
let path = path.as_ref();
let file_attributes = match path.file_name() {
Some(filename) => {
let filename = filename.to_string_lossy().to_string();
if let Ok(prod) = ProductionAttributes::from_str(&filename) {
Some(prod)
} else {
None
}
},
_ => None,
};
let fd = File::open(path)?;
let reader = GzDecoder::new(fd);
let mut reader = BufReader::new(reader);
let mut ionex = Self::parse(&mut reader)?;
ionex.production = file_attributes;
Ok(ionex)
}
#[cfg(feature = "flate2")]
#[cfg_attr(docsrs, doc(cfg(feature = "flate2")))]
pub fn to_gzip_file<P: AsRef<Path>>(&self, path: P) -> Result<(), FormattingError> {
let fd = File::create(path)?;
let compression = GzCompression::new(5);
let mut writer = BufWriter::new(GzEncoder::new(fd, compression));
self.format(&mut writer)?;
Ok(())
}
pub fn is_merged(&self) -> bool {
for comment in self.header.comments.iter() {
if comment.contains("FILE MERGE") {
return true;
}
}
false
}
pub fn epoch_iter(&self) -> Box<dyn Iterator<Item = Epoch> + '_> {
Box::new(self.record.iter().map(|(k, _)| k.epoch))
}
pub fn first_epoch(&self) -> Option<Epoch> {
self.epoch_iter().nth(0)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{fmt_comment, is_comment};
#[test]
fn fmt_comments_singleline() {
for desc in [
"test",
"just a basic comment",
"just another lengthy comment blahblabblah",
] {
let comment = fmt_comment(desc);
assert!(
comment.len() >= 60,
"comments should be at least 60 byte long"
);
assert_eq!(
comment.find("COMMENT"),
Some(60),
"comment marker should located @ 60"
);
assert!(is_comment(&comment), "should be valid comment");
}
}
#[test]
fn fmt_wrapped_comments() {
for desc in ["just trying to form a very lengthy comment that will overflow since it does not fit in a single line",
"just trying to form a very very lengthy comment that will overflow since it does fit on three very meaningful lines. Imazdmazdpoakzdpoakzpdokpokddddddddddddddddddaaaaaaaaaaaaaaaaaaaaaaa"] {
let nb_lines = num_integer::div_ceil(desc.len(), 60);
let comments = fmt_comment(desc);
assert_eq!(comments.lines().count(), nb_lines);
for line in comments.lines() {
assert!(line.len() >= 60, "comment line should be at least 60 byte long");
assert_eq!(line.find("COMMENT"), Some(60), "comment marker should located @ 60");
assert!(is_comment(line), "should be valid comment");
}
}
}
}