diff --git a/gpx-rs/engine/data/with_highway.gpx b/gpx-rs/engine/data/with_highway.gpx
index c79581a9e..d79900067 100644
--- a/gpx-rs/engine/data/with_highway.gpx
+++ b/gpx-rs/engine/data/with_highway.gpx
@@ -39,6 +39,8 @@
gravel
track
+ mountain_hiking
+ 1
@@ -49,6 +51,8 @@
gravel
+ hiking
+ 1
diff --git a/gpx-rs/engine/src/core/gpx/categories.rs b/gpx-rs/engine/src/core/gpx/categories.rs
index 338ea4620..ad0f3097d 100644
--- a/gpx-rs/engine/src/core/gpx/categories.rs
+++ b/gpx-rs/engine/src/core/gpx/categories.rs
@@ -40,6 +40,8 @@ impl Categories {
pub struct TrackpointCategories {
pub surface: Categories,
pub highway: Categories,
+ pub sac_scale: Categories,
+ pub mtb_scale: Categories,
}
#[cfg(test)]
@@ -84,7 +86,11 @@ mod tests {
let mut categories = TrackpointCategories::default();
categories.surface.code("asphalt");
assert_eq!(categories.highway.code("path"), Some(0));
+ assert_eq!(categories.sac_scale.code("hiking"), Some(0));
+ assert_eq!(categories.mtb_scale.code("1"), Some(0));
assert_eq!(categories.surface.names(), ["asphalt"]);
assert_eq!(categories.highway.names(), ["path"]);
+ assert_eq!(categories.sac_scale.names(), ["hiking"]);
+ assert_eq!(categories.mtb_scale.names(), ["1"]);
}
}
diff --git a/gpx-rs/engine/src/core/gpx/trackpoint.rs b/gpx-rs/engine/src/core/gpx/trackpoint.rs
index e2e628f62..fd2ca9eb5 100644
--- a/gpx-rs/engine/src/core/gpx/trackpoint.rs
+++ b/gpx-rs/engine/src/core/gpx/trackpoint.rs
@@ -13,4 +13,8 @@ pub struct Trackpoint {
pub surface: Option,
/// Code of the highway, see [`crate::TrackpointCategories`].
pub highway: Option,
+ /// Code of the SAC hiking scale, see [`crate::TrackpointCategories`].
+ pub sac_scale: Option,
+ /// Code of the mountain biking scale, see [`crate::TrackpointCategories`].
+ pub mtb_scale: Option,
}
diff --git a/gpx-rs/engine/src/core/io/parse.rs b/gpx-rs/engine/src/core/io/parse.rs
index 16bf93f50..2e0871ece 100644
--- a/gpx-rs/engine/src/core/io/parse.rs
+++ b/gpx-rs/engine/src/core/io/parse.rs
@@ -31,6 +31,8 @@ enum GPXElement {
Power,
Surface,
Highway,
+ SacScale,
+ MtbScale,
Symbol,
Type,
Color,
@@ -50,7 +52,7 @@ fn parse_coordinates(attributes: Attributes<'_>) -> LngLat {
coordinates
}
-/// Parses a GPX file. The surface and the highway of the trackpoints are stored as codes of
+/// Parses a GPX file. The surface, highway, SAC scale and MTB scale of the trackpoints are stored as codes of
/// `categories`, which learns the values it does not know yet.
pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result {
let mut reader = Reader::from_reader(data);
@@ -103,6 +105,8 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result stack.push(GPXElement::Power),
"surface" => stack.push(GPXElement::Surface),
"highway" => stack.push(GPXElement::Highway),
+ "sac_scale" => stack.push(GPXElement::SacScale),
+ "mtb_scale" => stack.push(GPXElement::MtbScale),
"sym" => stack.push(GPXElement::Symbol),
"type" => stack.push(GPXElement::Type),
e if e.ends_with("color") => stack.push(GPXElement::Color),
@@ -288,6 +292,18 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result {
+ stack.pop();
+ if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
+ trkpt.sac_scale = categories.sac_scale.code(&e);
+ }
+ }
+ Some(GPXElement::MtbScale) => {
+ stack.pop();
+ if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
+ trkpt.mtb_scale = categories.mtb_scale.code(&e);
+ }
+ }
Some(GPXElement::Symbol) => {
stack.pop();
if let Some(GPXElement::Waypoint(wpt)) = stack.last_mut() {
@@ -566,7 +582,7 @@ mod tests {
}
#[test]
- fn test_parse_trackpoint_surface_and_highway() {
+ fn test_parse_trackpoint_surface_highway_and_scales() {
let (gpx, categories) = parse_data_with_categories("with_highway");
let trkseg = &gpx.trk[0].trkseg[0];
@@ -576,23 +592,33 @@ mod tests {
let values: Vec<_> = trkseg
.iter()
.map(|trkpt| {
- (
+ [
names(trkpt.surface, &categories.surface),
names(trkpt.highway, &categories.highway),
- )
+ names(trkpt.sac_scale, &categories.sac_scale),
+ names(trkpt.mtb_scale, &categories.mtb_scale),
+ ]
})
.collect();
let some = |s: &str| Some(s.to_owned());
assert_eq!(
values,
[
- (some("asphalt"), some("residential")),
- (some("asphalt"), some("residential")),
- (None, None),
- (some("gravel"), some("track")),
- (some("gravel"), None),
+ [some("asphalt"), some("residential"), None, None],
+ [some("asphalt"), some("residential"), None, None],
+ [None, None, None, None],
+ [
+ some("gravel"),
+ some("track"),
+ some("mountain_hiking"),
+ some("1")
+ ],
+ [some("gravel"), None, some("hiking"), some("1")],
]
);
+ // the tables are separate
+ assert_eq!(categories.sac_scale.names(), ["mountain_hiking", "hiking"]);
+ assert_eq!(categories.mtb_scale.names(), ["1"]);
}
#[test]
diff --git a/gpx-rs/engine/src/engine/derived/statistics_buffer.rs b/gpx-rs/engine/src/engine/derived/statistics_buffer.rs
index 69b3977b0..41ac77eda 100644
--- a/gpx-rs/engine/src/engine/derived/statistics_buffer.rs
+++ b/gpx-rs/engine/src/engine/derived/statistics_buffer.rs
@@ -38,6 +38,10 @@ pub struct StatisticsBuffer {
pub surface: Vec,
/// Highway of the trackpoints, as the surface.
pub highway: Vec,
+ /// SAC hiking scale of the trackpoints, as the surface.
+ pub sac_scale: Vec,
+ /// Mountain biking scale of the trackpoints, as the surface.
+ pub mtb_scale: Vec,
}
/// 0 for an unknown value, else the code plus one. The categories hold at most 255 values, so it
@@ -78,6 +82,8 @@ impl StatisticsBuffer {
self.power.clear();
self.surface.clear();
self.highway.clear();
+ self.sac_scale.clear();
+ self.mtb_scale.clear();
let optional = |value: Option| value.unwrap_or(f64::NAN);
@@ -117,6 +123,8 @@ impl StatisticsBuffer {
self.power.push(optional(trkpt.power.map(f64::from)));
self.surface.push(unknown_or_next(trkpt.surface));
self.highway.push(unknown_or_next(trkpt.highway));
+ self.sac_scale.push(unknown_or_next(trkpt.sac_scale));
+ self.mtb_scale.push(unknown_or_next(trkpt.mtb_scale));
}
self.global.merge(&stats.global);
}
@@ -196,6 +204,8 @@ mod tests {
buffer.power.len(),
buffer.surface.len(),
buffer.highway.len(),
+ buffer.sac_scale.len(),
+ buffer.mtb_scale.len(),
]
}
@@ -304,6 +314,8 @@ mod tests {
// 0 for the trackpoints that have none, else the code of the engine plus one
assert_eq!(buffer.surface, [1, 1, 0, 2, 2]);
assert_eq!(buffer.highway, [1, 1, 0, 2, 0]);
+ assert_eq!(buffer.sac_scale, [0, 0, 0, 1, 2]);
+ assert_eq!(buffer.mtb_scale, [0, 0, 0, 1, 1]);
assert!(all_lengths(&buffer).iter().all(|len| *len == 5));
}
@@ -313,12 +325,16 @@ mod tests {
let mut point = segment[0].clone();
point.surface = Some(7);
point.highway = Some(254);
+ point.sac_scale = Some(0);
+ point.mtb_scale = Some(3);
segment.splice(0, 1, vec![point]);
let s = Statistics::compute(&segment);
let mut buffer = StatisticsBuffer::default();
buffer.update(&[(&segment, &s)]);
assert_eq!((buffer.surface[0], buffer.highway[0]), (8, 255));
+ assert_eq!((buffer.sac_scale[0], buffer.mtb_scale[0]), (1, 4));
assert_eq!((buffer.surface[1], buffer.highway[1]), (0, 0));
+ assert_eq!((buffer.sac_scale[1], buffer.mtb_scale[1]), (0, 0));
}
#[test]
@@ -329,9 +345,12 @@ mod tests {
assert_eq!(buffer.surface.len(), s.local.len());
assert!(buffer.surface.iter().all(|c| *c == 0));
assert!(buffer.highway.iter().all(|c| *c == 0));
+ assert!(buffer.sac_scale.iter().all(|c| *c == 0));
+ assert!(buffer.mtb_scale.iter().all(|c| *c == 0));
// and they are reset by the next update
buffer.update(&[]);
assert!(buffer.surface.is_empty() && buffer.highway.is_empty());
+ assert!(buffer.sac_scale.is_empty() && buffer.mtb_scale.is_empty());
}
#[test]
diff --git a/gpx-rs/engine/src/engine/engine.rs b/gpx-rs/engine/src/engine/engine.rs
index b2efd6712..8a8a6531b 100644
--- a/gpx-rs/engine/src/engine/engine.rs
+++ b/gpx-rs/engine/src/engine/engine.rs
@@ -1252,6 +1252,12 @@ mod tests {
// the trackpoints of the selection refer to them
assert_eq!(engine.statistics().surface, [1, 1, 0, 2, 2]);
assert_eq!(engine.statistics().highway, [1, 1, 0, 2, 0]);
+ assert_eq!(
+ engine.categories().sac_scale.names(),
+ ["mountain_hiking", "hiking"]
+ );
+ assert_eq!(engine.statistics().sac_scale, [0, 0, 0, 1, 2]);
+ assert_eq!(engine.statistics().mtb_scale, [0, 0, 0, 1, 1]);
// a second file goes on with the same table
load(&mut engine, "data/with_surface.gpx");
diff --git a/gpx-rs/wasm/src/lib.rs b/gpx-rs/wasm/src/lib.rs
index 0182537e0..d8d725d5f 100644
--- a/gpx-rs/wasm/src/lib.rs
+++ b/gpx-rs/wasm/src/lib.rs
@@ -164,10 +164,12 @@ stats_getter!(cad, Float64Array);
stats_getter!(atemp, Float64Array);
stats_getter!(power, Float64Array);
-// Surface and highway of the trackpoints: 0 when unknown, else 1 + the index in `surfaces()` or
-// `highways()`.
+// Surface, highway, SAC scale and MTB scale of the trackpoints: 0 when unknown, else 1 + the index
+// in `surfaces()`, `highways()`, `sac_scales()` or `mtb_scales()`.
stats_getter!(surface, Uint8Array);
stats_getter!(highway, Uint8Array);
+stats_getter!(sac_scale, Uint8Array);
+stats_getter!(mtb_scale, Uint8Array);
/// The names of the surfaces, in the order of their codes (see `surface`). The list only grows:
/// a name keeps its index.
@@ -186,6 +188,30 @@ pub fn highways() -> StringList {
.unchecked_into()
}
+/// The names of the SAC scales, in the order of their codes (see `sac_scale`).
+#[wasm_bindgen]
+pub fn sac_scales() -> StringList {
+ with_engine(|e| {
+ array(e.categories().sac_scale.names(), |name| {
+ name.as_str().into()
+ })
+ })
+ .unwrap_or_default()
+ .unchecked_into()
+}
+
+/// The names of the MTB scales, in the order of their codes (see `mtb_scale`).
+#[wasm_bindgen]
+pub fn mtb_scales() -> StringList {
+ with_engine(|e| {
+ array(e.categories().mtb_scale.names(), |name| {
+ name.as_str().into()
+ })
+ })
+ .unwrap_or_default()
+ .unchecked_into()
+}
+
// File commands
#[wasm_bindgen]
diff --git a/website/src/lib/components/elevation-profile/elevation-profile.ts b/website/src/lib/components/elevation-profile/elevation-profile.ts
index aced894b3..5f56d7aec 100644
--- a/website/src/lib/components/elevation-profile/elevation-profile.ts
+++ b/website/src/lib/components/elevation-profile/elevation-profile.ts
@@ -44,6 +44,8 @@ interface ElevationProfilePoint {
};
surface?: string;
highway?: string;
+ sacScale?: string;
+ mtbScale?: string;
coordinates: Coordinates;
index: number;
}
@@ -189,9 +191,8 @@ export class ElevationProfile {
};
let surface = point.surface ?? 'unknown';
let highway = point.highway ?? 'unknown';
- // TODO the engine does not store the sac and mtb scales yet
- let sacScale: string | undefined = undefined;
- let mtbScale: string | undefined = undefined;
+ let sacScale = point.sacScale;
+ let mtbScale = point.mtbScale;
let labels = [
` ${i18n._('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
@@ -414,6 +415,8 @@ export class ElevationProfile {
},
surface: categoryAt(data.surface, data.surfaces, index),
highway: categoryAt(data.highway, data.highways, index),
+ sacScale: categoryAt(data.sacScale, data.sacScales, index),
+ mtbScale: categoryAt(data.mtbScale, data.mtbScales, index),
coordinates: { lat: data.lat[index], lon: data.lng[index] },
index,
});
@@ -606,8 +609,7 @@ export class ElevationProfile {
highwayFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
const point = context.p0.raw as ElevationProfilePoint;
- // TODO the engine does not store the sac and mtb scales yet
- return getHighwayColor(point.highway ?? '', undefined, undefined);
+ return getHighwayColor(point.highway ?? '', point.sacScale, point.mtbScale);
}
destroy() {
diff --git a/website/src/lib/engine.ts b/website/src/lib/engine.ts
index 87d473ccd..18066ef7a 100644
--- a/website/src/lib/engine.ts
+++ b/website/src/lib/engine.ts
@@ -148,6 +148,14 @@ export type SelectionStatistics = {
highway: Uint8Array;
/** Names of the highways, by code. */
highways: string[];
+ /** SAC hiking scale of the trackpoints: 0 when unknown, else 1 + the index in `sacScales`. */
+ sacScale: Uint8Array;
+ /** Names of the SAC scales, by code. */
+ sacScales: string[];
+ /** Mountain biking scale of the trackpoints: 0 when unknown, else 1 + the index in `mtbScales`. */
+ mtbScale: Uint8Array;
+ /** Names of the mountain biking scales, by code. */
+ mtbScales: string[];
/**
* Global statistics of the trackpoints from `start` to `end` (both included), for example
* the part of the elevation profile that was dragged over. `undefined` if the range is not
@@ -181,6 +189,10 @@ const EMPTY_SELECTION_STATISTICS: SelectionStatistics = {
surfaces: [],
highway: new Uint8Array(),
highways: [],
+ sacScale: new Uint8Array(),
+ sacScales: [],
+ mtbScale: new Uint8Array(),
+ mtbScales: [],
slice: () => undefined,
};
@@ -635,6 +647,10 @@ class Engine {
surfaces: wasm.surfaces(),
highway: wasm.highway().slice(),
highways: wasm.highways(),
+ sacScale: wasm.sac_scale().slice(),
+ sacScales: wasm.sac_scales(),
+ mtbScale: wasm.mtb_scale().slice(),
+ mtbScales: wasm.mtb_scales(),
slice: (start, end) =>
version === this._statisticsVersion ? wasm.slice_statistics(start, end) : undefined,
};
diff --git a/website/src/routes/[[language]]/+page.svelte b/website/src/routes/[[language]]/+page.svelte
index 4e8636653..c54b9089e 100644
--- a/website/src/routes/[[language]]/+page.svelte
+++ b/website/src/routes/[[language]]/+page.svelte
@@ -49,6 +49,10 @@
surfaces: [],
highway: new Uint8Array(),
highways: [],
+ sacScale: new Uint8Array(),
+ sacScales: [],
+ mtbScale: new Uint8Array(),
+ mtbScales: [],
slice: () => undefined,
});
let slicedStatistics = writable(undefined);