Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce ionex2kml tool #119

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[workspace]
members = ["rinex", "crx2rnx", "rnx2crx", "rinex-cli", "ublox-rnx", "sinex"]
members = ["rinex", "crx2rnx", "rnx2crx", "rinex-cli", "ublox-rnx", "sinex", "ionex2kml"]
20 changes: 20 additions & 0 deletions ionex2kml/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "ionex2kml"
version = "0.0.1"
license = "MIT OR Apache-2.0"
authors = ["Guillaume W. Bres <[email protected]>"]
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"] }
51 changes: 51 additions & 0 deletions ionex2kml/README.md
Original file line number Diff line number Diff line change
@@ -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|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

@gwbres gwbres Aug 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if we can use latex like equations in docrs too ? At the time I looked at it, there was a tweak involving storing html pages if I remember correctly, seemd too complicated

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
```
119 changes: 119 additions & 0 deletions ionex2kml/src/cli.rs
Original file line number Diff line number Diff line change
@@ -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, <[email protected]>")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.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::<String>("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 <String, String> "KML attributes"
pub fn kml_attributes(&self) -> Option<HashMap<String, String>> {
if let Some(attributes) = self.matches.get_one::<String>("attributes") {
let content: Vec<&str> = attributes.split(",").collect();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't matter, but this isn't a great way to write this.

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::<u32>("equipoential") {
// // *n as usize
// //} else {
// 20 // default value
// //}
// }
/// Returns ionex filename
pub fn ionex_filepath(&self) -> &str {
self.matches.get_one::<String>("ionex").unwrap()
}
}
151 changes: 151 additions & 0 deletions ionex2kml/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<f64> = 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<Kml<f64>> = Vec::new();
let epoch_folder_attrs = vec![(String::from("Epoch"), epoch.to_string())]
.into_iter()
.collect::<HashMap<String, String>>();

//test a linestring to describe equipoential TECu area
let polygon = Polygon::<f64> {
inner: vec![],
outer: {
LinearRing::<f64> {
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::<f64> {
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<f64> = 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(())
}
Loading