navigo 0.2.0

Simply manipulate GPS/geospatial data in Rust — distances, elevation, simplification, climb detection.
Documentation

navigo

simply manipulate GPS/geospatial data — in rust

api / usage

location

A location is a GPS coordinate defined by a longitude, a latitude, and an altitude.

let location = Location {
    longitude: 2.350987,
    latitude: 48.856667,
    altitude: 890.0,
};
  • distance to another location (km):
let distance: f64 = paris.calculate_distance_to(&moscow);
  • bearing to another location (degrees):
let bearing: f64 = paris.calculate_bearing_to(&moscow);
  • elevation change to another location:
let elevation: Elevation = paris.calculate_elevation_to(&moscow);
// elevation.positive — gain in meters
// elevation.negative — loss in meters
  • check if inside a bounding box:
let area = Area { min_latitude: 54.7, max_latitude: 56.7,
                  min_longitude: 36.6, max_longitude: 38.6 };
let is_in: bool = location.is_in_area(&area);
  • check if inside a radius (km):
let is_in: bool = location.is_in_radius(&center, &70.0);

trace

Trace::new ingests raw GPS locations and precomputes everything in one shot:

  • Douglas-Peucker simplification (for traces > 1 000 points, ε = 15 m)
  • Cumulative distances
  • Denoised cumulative elevation gain / loss (median smoothing + hysteresis)
  • Smoothed slope at each point
  • Peaks and valleys (AMPD algorithm with prominence filter)
  • Qualifying climb segments (Garmin-style thresholds)
let trace = Trace::new(&locations);
// or via the convenience wrapper:
let trace = build_trace(&locations);

precomputed fields

trace.locations                   // Vec<Location>  — simplified working set
trace.cumulative_distances        // Vec<f64>        — km from start, [0] == 0.0
trace.cumulative_elevation_gains  // Vec<f64>        — denoised gain in meters
trace.cumulative_elevation_losses // Vec<f64>        — denoised loss in meters
trace.slopes                      // Vec<f64>        — % grade at each point
trace.peaks                       // Vec<usize>      — indices of detected peaks
trace.valleys                     // Vec<usize>      — indices of detected valleys
trace.climbs                      // Vec<ClimbStats> — qualifying climb segments
trace.total_distance              // f64             — total distance in km
trace.total_elevation_gain        // f64             — total denoised gain in meters
trace.total_elevation_loss        // f64             — total denoised loss in meters

methods

  • total length (km):
let length: f64 = trace.length(); // alias for total_distance
  • location at a cumulative distance (km):
let loc: Option<&Location> = trace.point_at_distance(42.0);
  • index at a cumulative distance (binary search):
let idx: usize = trace.index_at_distance(42.0);
  • slice between two distance marks (km, both ends inclusive):
let section: Option<&[Location]> = trace.slice_between_distances(10.0, 50.0);
  • closest location to a point (early-stop heuristic for loop courses):
let (loc, idx, dist_km) = trace.find_closest_point(&target).unwrap();
// start search from a known index (e.g. to handle loop courses):
let result = trace.find_closest_point_from(&target, start_from);
  • bounding box:
let area: Result<Area, &str> = trace.area();
  • sub-section by index range (inclusive):
let section: Result<Vec<Location>, &str> = trace.get_section(start_index, end_index);

climb stats

pub struct ClimbStats {
    pub start_index:    usize, // valley index in trace.locations
    pub end_index:      usize, // summit index in trace.locations
    pub start_dist_km:  f64,
    pub climb_dist_km:  f64,
    pub elevation_gain: f64,   // meters
    pub summit_elev:    f64,   // meters
    pub avg_gradient:   f64,   // %
}

Climbs are qualified using Garmin Climb Pro thresholds: distance ≥ 500 m, average gradient ≥ 3 %, distance × gradient > 3 500 m·%


WebAssembly

The library can be compiled to WASM for use in web applications via the wasm feature.

how it works

All data lives in WASM linear memory. The JS side holds a thin pointer (WasmTrace). Only the boundaries cross the WASM↔JS membrane — scalars are free (registers), bulk arrays are copied once on demand.

buildTrace(Float64Array)          ← one O(n) copy JS→WASM
       │
       ▼
WasmTrace stays in WASM memory
       │
       ├── trace.total_distance        → free (register)
       ├── trace.find_closest_point()  → free (scalars in/out)
       ├── trace.locations_flat        → one O(n) copy, cache it
       └── trace.free()               ← you must call this (no GC bridge)

build

cargo install wasm-pack
wasm-pack build --target web       # ES modules — Vite, plain browser
wasm-pack build --target bundler   # webpack / Rollup

usage

import init, { buildTrace } from './navigo.js';
await init();

// build — one copy in, all computation in WASM
const pts = new Float64Array([
  2.350987, 48.856667, 0,     // lon, lat, alt
  37.617634, 55.755787, 200,
]);
const trace = buildTrace(pts);

// scalar getters — free
trace.total_distance;           // number (km)
trace.total_elevation_gain;     // number (m)
trace.total_elevation_loss;     // number (m)
trace.location_count;           // number

// array getters — copy once, then cache on the JS side
const locs    = trace.locations_flat;              // Float64Array [lon,lat,alt,…]
const dists   = trace.cumulative_distances;        // Float64Array (km)
const gains   = trace.cumulative_elevation_gains;  // Float64Array (m)
const losses  = trace.cumulative_elevation_losses; // Float64Array (m)
const slopes  = trace.slopes;                      // Float64Array (%)
const peaks   = trace.peaks;                       // Uint32Array  (indices)
const valleys = trace.valleys;                     // Uint32Array  (indices)

// query methods — scalars in, one small object out
trace.point_at_distance(42.0);
// → { longitude, latitude, altitude } | null

trace.index_at_distance(42.0);
// → number

trace.find_closest_point(lon, lat, alt);
// → { location: { longitude, latitude, altitude }, index, distance } | null

trace.find_closest_point_from(lon, lat, alt, lastIndex);
// → same shape | null  (use on live-tracking loops)

trace.slice_between_distances(10.0, 50.0);
// → Float64Array [lon,lat,alt,…] | null

trace.get_section(startIndex, endIndex);
// → Float64Array [lon,lat,alt,…] | null

trace.area();
// → { min_longitude, max_longitude, min_latitude, max_latitude } | null

trace.elevation();
// → { positive, negative }  (raw, non-denoised)

trace.climbs();
// → [{ start_index, end_index, start_dist_km, climb_dist_km,
//      elevation_gain, summit_elev, avg_gradient }, …]

// always release when done — Rust allocator has no GC bridge
trace.free();

memory management

WasmTrace lives in WASM linear memory. The JS object is just a pointer — Rust cannot reclaim it when the JS variable is GC'd. Always call .free(), or register a FinalizationRegistry:

const registry = new FinalizationRegistry(t => t.free());
const trace = buildTrace(pts);
registry.register(trace, trace);

license

MIT