From 9fd33a70395ffeaa050ae156ca6a34a1c0b341fb Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sun, 6 Aug 2023 21:22:21 +0200 Subject: [PATCH 1/4] introduce ionex2kml tool Signed-off-by: Guillaume W. Bres --- Cargo.toml | 2 +- ionex2kml/Cargo.toml | 20 +++++++ ionex2kml/README.md | 51 ++++++++++++++++++ ionex2kml/src/cli.rs | 119 ++++++++++++++++++++++++++++++++++++++++++ ionex2kml/src/main.rs | 108 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 ionex2kml/Cargo.toml create mode 100644 ionex2kml/README.md create mode 100644 ionex2kml/src/cli.rs create mode 100644 ionex2kml/src/main.rs 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..6a88821b6 --- /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 separate color the color map will be able to represent. + +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..bf0cef813 --- /dev/null +++ b/ionex2kml/src/main.rs @@ -0,0 +1,108 @@ +use kml::{Kml, KmlWriter}; +use rinex::prelude::*; + +mod cli; +use cli::{Cli, CliError}; +use log::{info, warn}; + +use kml::{ + types::{AltitudeMode, Coord, LineString}, + 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: KmlDocument = KmlDocument::default(); + kml.version = cli.kml_version(); + if let Some(attrs) = cli.kml_attributes() { + kml.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 linestring = Kml::LineString(LineString:: { + coords: vec![ + Coord { + x: 0.0_f64, + y: 1.0_f64, + z: Some(2.0_f64), + }, + Coord { + x: 0.0_f64, + y: 1.0_f64, + z: Some(2.0_f64), + }, + ], + extrude: false, + tessellate: false, + altitude_mode: AltitudeMode::RelativeToGround, + attrs: vec![(String::from("Test"), String::from("test"))] + .into_iter() + .collect(), + }); + + epoch_folder.push(linestring); + + // // 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, + }; + kml.elements.push(epoch_folder); + } + let kml = Kml::KmlDocument(kml); + writer.write(&kml)?; + info!("kml generated"); + Ok(()) +} From d9b4787cccef6ff630d0ab053053843a9f4081af Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Mon, 7 Aug 2023 14:39:25 +0200 Subject: [PATCH 2/4] working on kml basic structure Signed-off-by: Guillaume W. Bres --- ionex2kml/src/main.rs | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/ionex2kml/src/main.rs b/ionex2kml/src/main.rs index bf0cef813..a381f0d3e 100644 --- a/ionex2kml/src/main.rs +++ b/ionex2kml/src/main.rs @@ -6,11 +6,13 @@ use cli::{Cli, CliError}; use log::{info, warn}; use kml::{ - types::{AltitudeMode, Coord, LineString}, + types::{AltitudeMode, Coord, LineString, LinearRing}, KmlDocument, }; use std::collections::HashMap; + + //use std::io::Write; fn main() -> Result<(), CliError> { @@ -30,10 +32,10 @@ fn main() -> Result<(), CliError> { ))); } - let mut kml: KmlDocument = KmlDocument::default(); - kml.version = cli.kml_version(); + let mut kml_doc: KmlDocument = KmlDocument::default(); + kml_doc.version = cli.kml_version(); if let Some(attrs) = cli.kml_attributes() { - kml.attrs = attrs; + kml_doc.attrs = attrs; } let record = rinex.record.as_ionex().unwrap(); @@ -49,17 +51,22 @@ fn main() -> Result<(), CliError> { .collect::>(); //test a linestring to describe equipoential TECu area - let linestring = Kml::LineString(LineString:: { + let linestring = Kml::LinearRing(LinearRing:: { coords: vec![ Coord { - x: 0.0_f64, - y: 1.0_f64, - z: Some(2.0_f64), + x: 4.119067147539055, + y: 43.73425044812969, + z: None, }, Coord { - x: 0.0_f64, - y: 1.0_f64, - z: Some(2.0_f64), + x: 4.11327766588697, + y: 43.73124529989733, + z: None, + }, + Coord { + x: 4.119067147539055, + y: 43.73425044812969, + z: None, }, ], extrude: false, @@ -99,10 +106,14 @@ fn main() -> Result<(), CliError> { attrs: epoch_folder_attrs, elements: epoch_folder, }; - kml.elements.push(epoch_folder); + // add folder to document + kml_doc.elements.push(epoch_folder); } - let kml = Kml::KmlDocument(kml); + + // generate document + let kml = Kml::KmlDocument(kml_doc); writer.write(&kml)?; info!("kml generated"); + Ok(()) } From 439be33bc5484d2a8476fe6a5429b53de33131c8 Mon Sep 17 00:00:00 2001 From: gwbres Date: Mon, 14 Aug 2023 09:16:10 +0200 Subject: [PATCH 3/4] Update ionex2kml/README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Laurențiu Nicola --- ionex2kml/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ionex2kml/README.md b/ionex2kml/README.md index 6a88821b6..ff86d82ae 100644 --- a/ionex2kml/README.md +++ b/ionex2kml/README.md @@ -34,7 +34,7 @@ When converting to KML, we round the TEC values and shrink it to N equipotential 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 separate color the color map will be able to represent. +Another way to see this, is N defines the number of distinct colors in the color map Visualizing KML maps ==================== From c03ef474773e5b6b072a8970f604a883fa9c19b6 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Thu, 24 Aug 2023 19:03:22 +0200 Subject: [PATCH 4/4] placemark Signed-off-by: Guillaume W. Bres --- ionex2kml/src/main.rs | 84 +++++++++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/ionex2kml/src/main.rs b/ionex2kml/src/main.rs index a381f0d3e..638200a27 100644 --- a/ionex2kml/src/main.rs +++ b/ionex2kml/src/main.rs @@ -6,7 +6,7 @@ use cli::{Cli, CliError}; use log::{info, warn}; use kml::{ - types::{AltitudeMode, Coord, LineString, LinearRing}, + types::{AltitudeMode, Coord, LineString, LinearRing, Polygon, Placemark, Geometry}, KmlDocument, }; use std::collections::HashMap; @@ -43,7 +43,7 @@ fn main() -> Result<(), CliError> { let mut buf = std::io::stdout().lock(); let mut writer = KmlWriter::<_, f64>::from_writer(&mut buf); - //// We wrap each Epoch in separate "Folders" + // 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())] @@ -51,33 +51,62 @@ fn main() -> Result<(), CliError> { .collect::>(); //test a linestring to describe equipoential TECu area - let linestring = Kml::LinearRing(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, - }, - ], + 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: false, + tessellate: true, altitude_mode: AltitudeMode::RelativeToGround, - attrs: vec![(String::from("Test"), String::from("test"))] - .into_iter() - .collect(), - }); + 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(linestring); + epoch_folder.push(Kml::Placemark(placemark)); // // We wrap equipotential TECu areas in // // we wrap altitude levels in separate "Folders" @@ -102,12 +131,15 @@ fn main() -> Result<(), CliError> { // 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