diff --git a/Cargo.toml b/Cargo.toml index af26ff9e0..87015a616 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["rinex", "crx2rnx", "rnx2crx", "rinex-cli", "ublox-rnx", "sinex"] +members = ["rinex", "crx2rnx", "rnx2crx", "rinex-cli", "ublox-rnx", "sinex", "ionex2kml"] diff --git a/ionex2kml/Cargo.toml b/ionex2kml/Cargo.toml new file mode 100644 index 000000000..a699d64b6 --- /dev/null +++ b/ionex2kml/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ionex2kml" +version = "0.0.1" +license = "MIT OR Apache-2.0" +authors = ["Guillaume W. Bres "] +description = "IONEX to KML converter" +homepage = "https://github.com/georust/ionex2kml" +repository = "https://github.com/georust/ionex2kml" +keywords = ["rinex", "gps", "gpx"] +categories = ["parser", "science::geo", "command-line-interface"] +edition = "2021" +readme = "README.md" + +[dependencies] +log = "0.4" +kml = "0.8.3" +thiserror = "1" +pretty_env_logger = "0.5" +clap = { version = "4", features = ["derive", "color"] } +rinex = { path = "../rinex", features = ["flate2", "serde"] } diff --git a/ionex2kml/README.md b/ionex2kml/README.md new file mode 100644 index 000000000..ff86d82ae --- /dev/null +++ b/ionex2kml/README.md @@ -0,0 +1,51 @@ +ionex2kml +========= + +`ionex2kml` is a small application to convert a IONEX file into KML format. + +It is a convenient method to visualize TEC maps using third party tools. + +Getting started +=============== + +Provide a IONEX file with `-i`: + +```bash + ionex2kml -i /tmp/ionex.txt +``` + +Input IONEX files do not have to follow naming conventions. + +This tool will preserve the input file name by default, just changing the internal +format and file extension. In the previous example we would get `/tmp/ionex.kml`. + +Define the output file yourself with `-o`: + +```bash + ionex2kml -i jjim12.12i -o /tmp/test.kml +``` + +Each Epoch is put in a dedicated KML folder. + +Equipotential TEC values +======================== + +When converting to KML, we round the TEC values and shrink it to N equipotential areas. +In other words, the granularity on the TEC visualization you get is max|tec|/N where max|tec| +is the absolute maximal TEC value in given file, through all epochs and all altitudes. + +Another way to see this, is N defines the number of distinct colors in the color map + +Visualizing KML maps +==================== + +Google maps is one way to visualize a KML file. + +KML content customization +========================= + +Define a specific KML revision with `-v` + +```bash + ionex2kml -i jjim12.12i -v http://www.opengis.net/kml/2.2 +``` diff --git a/ionex2kml/src/cli.rs b/ionex2kml/src/cli.rs new file mode 100644 index 000000000..5dbebd6c4 --- /dev/null +++ b/ionex2kml/src/cli.rs @@ -0,0 +1,119 @@ +use clap::{ + Arg, //ArgAction, + ArgMatches, + ColorChoice, + Command, +}; +use log::error; +use std::collections::HashMap; +use std::str::FromStr; +use thiserror::Error; + +use kml::types::KmlVersion; + +#[derive(Debug, Error)] +pub enum CliError { + #[error("file type \"{0}\" is not supported")] + FileTypeError(String), + #[error("failed to parse ionex file")] + IonexError(#[from] rinex::Error), + #[error("failed to generate kml content")] + GpxError(#[from] kml::Error), +} + +#[derive(Debug, Clone, Default)] +pub struct Cli { + matches: ArgMatches, +} + +impl Cli { + pub fn new() -> Self { + Self { + matches: { + Command::new("ionex2kml") + .author("Guillaume W. Bres, ") + .version(env!("CARGO_PKG_VERSION")) + .about("IONEX to KML conveter") + .arg_required_else_help(true) + .color(ColorChoice::Always) + .arg( + Arg::new("ionex") + .short('i') + .value_name("FILEPATH") + .help("Input IONEX file") + .required(true), + ) + .arg( + Arg::new("output") + .short('o') + .value_name("FILEPATH") + .help("Output KML file"), + ) + //.arg( + // Arg::new("equipotiential") + // .short('n') + // .value_name("N") + // .help("Number of isosurfaces allowed")) + .next_help_heading("KML content") + .arg( + Arg::new("version") + .short('v') + .value_name("VERSION") + .help("Define specific KML Revision"), + ) + .arg( + Arg::new("attributes") + .short('a') + .value_name("[NAME,VALUE]") + .help("Define custom file attributes"), + ) + .get_matches() + }, + } + } + /// Returns KML version to use, based on user command line + pub fn kml_version(&self) -> KmlVersion { + if let Some(version) = self.matches.get_one::("version") { + if let Ok(version) = KmlVersion::from_str(version) { + version + } else { + let default = KmlVersion::default(); + error!("invalid KML version, using default value \"{:?}\"", default); + default + } + } else { + KmlVersion::default() + } + } + /// Returns optional "KML attributes" + pub fn kml_attributes(&self) -> Option> { + if let Some(attributes) = self.matches.get_one::("attributes") { + let content: Vec<&str> = attributes.split(",").collect(); + if content.len() > 1 { + Some( + vec![(content[0].to_string(), content[1].to_string())] + .into_iter() + .collect(), + ) + } else { + error!("invalid csv, need a \"field\",\"value\" description"); + None + } + } else { + None + } + } + // /// Returns nb of equipotential TEC map we will exhibit, + // /// the maximal error on resulting TEC is defined as max|tec_u| / n + // pub fn nb_tec_potentials(&self) -> usize { + // //if let Some(n) = self.matches.get_one::("equipoential") { + // // *n as usize + // //} else { + // 20 // default value + // //} + // } + /// Returns ionex filename + pub fn ionex_filepath(&self) -> &str { + self.matches.get_one::("ionex").unwrap() + } +} diff --git a/ionex2kml/src/main.rs b/ionex2kml/src/main.rs new file mode 100644 index 000000000..638200a27 --- /dev/null +++ b/ionex2kml/src/main.rs @@ -0,0 +1,151 @@ +use kml::{Kml, KmlWriter}; +use rinex::prelude::*; + +mod cli; +use cli::{Cli, CliError}; +use log::{info, warn}; + +use kml::{ + types::{AltitudeMode, Coord, LineString, LinearRing, Polygon, Placemark, Geometry}, + KmlDocument, +}; +use std::collections::HashMap; + + + +//use std::io::Write; + +fn main() -> Result<(), CliError> { + pretty_env_logger::init_timed(); + + let cli = Cli::new(); + + let fp = cli.ionex_filepath(); + info!("reading {}", fp); + let rinex = Rinex::from_file(fp)?; + + if !rinex.is_ionex() { + warn!("input file is not a ionex file"); + return Err(CliError::FileTypeError(format!( + "{:?}", + rinex.header.rinex_type + ))); + } + + let mut kml_doc: KmlDocument = KmlDocument::default(); + kml_doc.version = cli.kml_version(); + if let Some(attrs) = cli.kml_attributes() { + kml_doc.attrs = attrs; + } + + let record = rinex.record.as_ionex().unwrap(); + + let mut buf = std::io::stdout().lock(); + let mut writer = KmlWriter::<_, f64>::from_writer(&mut buf); + + // We wrap each Epoch in separate "Folders" + for (epoch, (_map, _, _)) in record { + let mut epoch_folder: Vec> = Vec::new(); + let epoch_folder_attrs = vec![(String::from("Epoch"), epoch.to_string())] + .into_iter() + .collect::>(); + + //test a linestring to describe equipoential TECu area + let polygon = Polygon:: { + inner: vec![], + outer: { + LinearRing:: { + coords: vec![ + Coord { + x: 4.119067147539055, + y: 43.73425044812969, + z: None, + }, + Coord { + x: 4.11327766588697, + y: 43.73124529989733, + z: None, + }, + Coord { + x: 4.119067147539055, + y: 43.73425044812969, + z: None, + }, + Coord { + x: 4.129067147539055, + y: 44.73425044812969, + z: None, + }, + Coord { + x: 4.109067147539055, + y: 44.73425044812969, + z: None, + }, + ], + extrude: false, + tessellate: true, + altitude_mode: AltitudeMode::RelativeToGround, + attrs: vec![(String::from("name"), String::from("test"))] + .into_iter() + .collect(), + } + }, + extrude: false, + tessellate: true, + altitude_mode: AltitudeMode::RelativeToGround, + attrs: vec![(String::from("name"), String::from("test"))] + .into_iter() + .collect(), + }; + + let placemark = Placemark:: { + name: Some(String::from("This is a test")), + description: Some(String::from("Great description")), + geometry: Some(Geometry::Polygon(polygon)), + children: vec![], + attrs: HashMap::new(), + }; + + epoch_folder.push(Kml::Placemark(placemark)); + + // // We wrap equipotential TECu areas in + // // we wrap altitude levels in separate "Folders" + // // in 2D IONEX (single altitude value): you only get one folder per Epoch + // for (h, maps) in rinex.ionex() { + // let folder = Folder::default(); + // folder.attrs = + // attrs: vec![("elevation", format!("{:.3e}", h)].into_iter().collect() + // }; + // for (potential, boundaries) in maps.tec_equipotential(10) { + // // define one color for following areas + // let color = colorous::linear(percentile); + // let style = LineStyle { + // id: Some(percentile.to_string()), + // width: 1.0_f64, + // color_mode: ColorMode::Default, + // attrs: vec![("percentile", format!("{}", percentile)].into_iter().collect(), + // }; + // folder.elements.push(style); + // folder.elements.push(boundaries); + // } + // kml.elements.push(folder); + // } + //folder_content..push(epoch_folder); + + let epoch_folder: Kml = Kml::Folder { + attrs: epoch_folder_attrs, + elements: epoch_folder, + }; + // add folder to document + kml_doc.elements.push(epoch_folder); + + break;//TODO + } + + // generate document + let kml = Kml::KmlDocument(kml_doc); + writer.write(&kml)?; + info!("kml generated"); + + Ok(()) +}