Skip to content

Commit

Permalink
many things at once
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeromos Kovacs committed Sep 25, 2024
1 parent 0bb1efa commit f2fa00a
Show file tree
Hide file tree
Showing 8 changed files with 427 additions and 275 deletions.
52 changes: 26 additions & 26 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ fit_file = "0.6.0"
geo-types = "0.7.13"
gpx = "0.10.0"
rayon = "1.10.0"
srtm = "0.2.1"
srtm_reader = { version = "0.3.0", optional = true }
time = { version = "0.3.36", default-features = false }

[patch.crates-io]
srtm = { git = "https://github.com/jeromeschmied/srtm" }
srtm_reader = { git = "https://github.com/jeromeschmied/srtm_reader" }

[features]
default = ["elevation"]
elevation = ["dep:srtm_reader"]
43 changes: 32 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

## Installation

- have [Rust](https://rustup.rs)

- with `cargo` from [github](https://github.com/jeromeschmied): `cargo install --git "https://github.com/jeromeschmied/fit2gpx-rs"`
<!-- - with `cargo` from [crates.io](https://crates.io): `cargo install fit2gpx` -->
> [!NOTE]
> soon there might also be binary releases
- have [Rust](https://rustup.rs)
- with `cargo` from [github](https://github.com/jeromeschmied): `cargo install --locked --git "https://github.com/jeromeschmied/fit2gpx-rs"`
- with `cargo` from [crates.io](https://crates.io): `cargo install fit2gpx`
- with `cargo` and `git` from [github](https://github.com/jeromeschmied):

```sh
Expand All @@ -17,20 +18,32 @@ cargo install --locked --path .

## Usage

### binary

see `fit2gpx --help`

let's say you want to convert `a_lovely_evening_walk.fit` to `a_lovely_evening_walk.gpx`
in that case, you'd do the following
`fit2gpx a_lovely_evening_walk.fit`
if you also want to add elevation data, as the `.fit` file didn't contain any, follow [these steps](#how-to-add-elevation-data)

### library

short:

```rust
fit2gpx::convert_file("walk.fit").unwrap();
```

see [docs](https://docs.rs/crates/fit2gpx) or [examples](https://github.com/jeromeschmied/fit2gpx-rs/tree/main/examples) for more detailed usage

## Purpose

This is a simple Rust `todo!("library")` and binary for converting .FIT files to .GPX files.
This is a simple Rust library and binary for converting .FIT files to .GPX files.
A **_faster_** alternative to the great [**_fit2gpx_**](https://github.com/dodo-saba/fit2gpx)

- [FIT](https://developer.garmin.com/fit/overview/) is a GIS data file format used by Garmin GPS sport devices and Garmin software
- [GPX](https://docs.fileformat.com/gis/gpx/) is an XML based format for GPS tracks.
- [GPX](https://docs.fileformat.com/gis/gpx/) is an XML based format for GNSS tracks

## Is it any good?

Expand All @@ -41,27 +54,35 @@ Yes.
- it's about 80 times as fast (single file, no elevation added)
- it's way faster with multi-file execution too
- it can add elevation data
- Rust library
- it's fun

## How to add elevation data

- first of all, have srtm data: `.hgt` files downloaded
one reliable source is [Sonny's collection](https://sonny.4lima.de/), it's only for Europe though
one great source is [Sonny's collection](https://sonny.4lima.de/), it's only for Europe though
- then unzip everything, place all of the `.hgt` files to a single directory
- set `$ELEV_DATA_DIR` to that very directory or pass `--elev_data_dir ~/my_elevation_data_dir`
- pass the `--add_altitude | -a` flag to `fit2gpx`
- make sure that `elevation` feature is enabled, _it's the default_
- pass the `--add_elevation | -a` flag to `fit2gpx`

## Why might this one not be the right choice

- it doesn't support strava bulk-export stuff: unzipping `.gz` files,
which you can do in the shell with 1 command
### it doesn't support strava bulk-export stuff

- unzipping `.gz` files. solution: in your activities directory run `gzip -d *.gz`
- adding metadata to gpx files from the `activities.csv` file

## Direct dependencies

<!-- - [coordinate-altitude](https://github.com/jeromeschmied/coordinate-altitude) -->

- [srtm](https://github.com/jeromeschmied/srtm)
- [srtm](https://github.com/jeromeschmied/srtm_reader)
- [fit_file](https://crates.io/crates/fit_file)
- [gpx](https://crates.io/crates/gpx)
- [clap](https://crates.io/crates/clap)
- [rayon](https://crates.io/crates/rayon)

```
```
4 changes: 4 additions & 0 deletions examples/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fn main() {
fit2gpx::convert_file("afternoon walk with dog.fit").unwrap();
println!();
}
107 changes: 107 additions & 0 deletions src/elevation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use super::*;
use rayon::prelude::*;
use std::collections::HashMap;

// TODO: docs
pub fn needed_tile_coords(wps: &[Waypoint]) -> Vec<(i32, i32)> {
// kinda Waypoint to (i32, i32)
let trunc = |wp: &Waypoint| -> (i32, i32) {
let (x, y) = wp.point().x_y();
(y.trunc() as i32, x.trunc() as i32)
};
// tiles we need
let mut needs = Vec::new();
for wp in wps.iter().filter(|wp| !is_00(wp)).map(trunc) {
if !needs.contains(&wp) {
needs.push(wp);
}
}
needs
}

// TODO: docs
pub fn read_needed_tiles(
needs: &[(i32, i32)],
elev_data_dir: Option<impl AsRef<Path>>,
) -> Vec<srtm_reader::Tile> {
if needs.is_empty() {
return vec![];
}

let elev_data_dir = if let Some(arg_data_dir) = &elev_data_dir {
arg_data_dir.as_ref()
} else if let Some(env_data_dir) = option_env!("ELEV_DATA_DIR") {
Path::new(env_data_dir)
} else if let Some(env_data_dir) = option_env!("elev_data_dir") {
Path::new(env_data_dir)
} else {
panic!("no elevation data dir is passed as an arg nor set as an environment variable: ELEV_DATA_DIR");
};
needs
.par_iter()
.map(|c| srtm_reader::get_filename(*c))
.map(|t| elev_data_dir.join(t))
.map(|p| srtm_reader::Tile::from_file(p).inspect_err(|e| eprintln!("error: {e:#?}")))
.flatten() // ignore the ones with an error
.collect::<Vec<_>>()
}
// TODO: don't panic
// TODO: docs
/// index the tiles with their coordinates
pub fn get_all_elev_data<'a>(
needs: &'a [(i32, i32)],
tiles: &'a [srtm_reader::Tile],
) -> HashMap<&'a (i32, i32), &'a srtm_reader::Tile> {
assert_eq!(needs.len(), tiles.len());
needs
.par_iter()
.enumerate()
.map(|(i, coord)| (coord, tiles.get(i).unwrap()))
.collect::<HashMap<_, _>>()
// eprintln!("loaded elevation data: {:?}", all_elev_data.keys());
}

/// add elevation to all `wps` using `elev_data`, in parallel
///
/// # Panics
///
/// elevation data needed, but not loaded
///
/// # Safety
///
/// it's the caller's responsibility to have the necessary data loaded
///
/// # Usage
///
/// using the following order, it should be safe
///
/// ```
/// use fit2gpx::elevation::*;
///
/// let mut fit = fit2gpx::FitContext::from_file("evening walk.gpx").unwrap();
/// let elev_data_dir = Some("/home/me/Downloads/srtm_data");
/// let needed_tile_coords = needed_tile_coords(&fit.track_segment.points);
/// let needed_tiles = needed_tiles(&needed_tile_coords, elev_data_dir);
/// let all_elev_data = get_all_elev_data(&needed_tile_coords, &needed_tiles);
///
/// add_elev_unchecked(&mut fit.track_segment.points, &all_elev_data);
/// ```
pub fn add_elev_unchecked(
wps: &mut [Waypoint],
elev_data: &HashMap<&(i32, i32), &srtm_reader::Tile>,
) {
// coord is x,y but we need y,x
let xy_yx = |wp: &Waypoint| -> srtm_reader::Coord {
let (x, y) = wp.point().x_y();
(y, x).into()
};
wps.into_par_iter()
.filter(|wp| wp.elevation.is_none() && !is_00(wp))
.for_each(|wp| {
let coord = xy_yx(wp);
let elev_data = elev_data
.get(&coord.trunc())
.expect("elevation data must be loaded");
wp.elevation = Some(elev_data.get(coord) as f64);
});
}
Loading

0 comments on commit f2fa00a

Please sign in to comment.