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);