From 7a7749a9d3d4d99a7b87727d2ec40ef671a854a1 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Thu, 5 Nov 2026 20:29:23 +0100 Subject: [PATCH] closes #206 --- gpx-rs/src/core/io/parse.rs | 99 +++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/gpx-rs/src/core/io/parse.rs b/gpx-rs/src/core/io/parse.rs index 0e9e2ff7a..878e3b970 100644 --- a/gpx-rs/src/core/io/parse.rs +++ b/gpx-rs/src/core/io/parse.rs @@ -37,6 +37,8 @@ enum GPXElement { Symbol, Type, Color, + /// The color of a track or a route in the Garmin extensions (`gpxx:DisplayColor`). + DisplayColor, Opacity, Width, } @@ -58,6 +60,30 @@ fn is_route_point_extension(name: &str) -> bool { name == "rpt" || name.ends_with(":rpt") } +/// The hexadecimal color of a value of `DisplayColor_t` in the Garmin extensions. None for +/// `Transparent` and for what is not part of the schema. +fn garmin_display_color(name: &str) -> Option<&'static str> { + match name.trim() { + "Black" => Some("000000"), + "DarkRed" => Some("800000"), + "DarkGreen" => Some("008000"), + "DarkYellow" => Some("808000"), + "DarkBlue" => Some("000080"), + "DarkMagenta" => Some("800080"), + "DarkCyan" => Some("008080"), + "LightGray" => Some("c0c0c0"), + "DarkGray" => Some("808080"), + "Red" => Some("ff0000"), + "Green" => Some("00ff00"), + "Yellow" => Some("ffff00"), + "Blue" => Some("0000ff"), + "Magenta" => Some("ff00ff"), + "Cyan" => Some("00ffff"), + "White" => Some("ffffff"), + _ => None, + } +} + /// Whether the element holds a value, which is its text. fn holds_text(element: Option<&GPXElement>) -> bool { matches!( @@ -81,6 +107,7 @@ fn holds_text(element: Option<&GPXElement>) -> bool { | GPXElement::Symbol | GPXElement::Type | GPXElement::Color + | GPXElement::DisplayColor | GPXElement::Opacity | GPXElement::Width ) @@ -244,6 +271,15 @@ fn apply_text( trk.info.color = Some(e.to_owned()); } } + // The color of the style schema has priority, wherever it is in the extensions + Some(GPXElement::DisplayColor) => { + stack.pop(); + if let Some(GPXElement::Track(trk)) = stack.last_mut() + && trk.info.color.is_none() + { + trk.info.color = garmin_display_color(e).map(str::to_owned); + } + } Some(GPXElement::Opacity) => { stack.pop(); if let Some(GPXElement::Track(trk)) = stack.last_mut() { @@ -455,6 +491,7 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result stack.push(GPXElement::Symbol), "type" => stack.push(GPXElement::Type), e if e.ends_with("color") => stack.push(GPXElement::Color), + e if e.ends_with("DisplayColor") => stack.push(GPXElement::DisplayColor), e if e.ends_with("opacity") => stack.push(GPXElement::Opacity), e if e.ends_with("width") => stack.push(GPXElement::Width), _ => (), @@ -1274,4 +1311,66 @@ mod tests { assert!(trk.info.opacity.is_some_and(|o| o == 0.5)); assert!(trk.info.width.is_some_and(|w| w == 6.0)); } + + fn parse_text(text: &str) -> File { + parse(text.as_bytes(), &mut Default::default()).unwrap() + } + + #[test] + fn test_parse_garmin_display_color_of_tracks_and_routes() { + let gpx = parse_text( + r#" + + Blue + + + DarkRed + + + Green + + "#, + ); + + assert_eq!(gpx.trk[0].info.color.as_deref(), Some("0000ff")); + assert_eq!(gpx.trk[1].info.color.as_deref(), Some("800000")); + assert_eq!(gpx.rte[0].info.color.as_deref(), Some("00ff00")); + } + + #[test] + fn test_garmin_display_color_without_a_color_leaves_the_track_uncolored() { + let gpx = parse_text( + r#" + + Transparent + + + Mauve + + "#, + ); + + assert_eq!(gpx.trk[0].info.color, None); + assert_eq!(gpx.trk[1].info.color, None); + } + + #[test] + fn test_style_color_has_priority_over_garmin_display_color() { + let gpx = parse_text( + r#" + + Red + 2d3ee9 + + + 2d3ee9 + Red + + "#, + ); + + assert_eq!(gpx.trk[0].info.color.as_deref(), Some("2d3ee9")); + assert_eq!(gpx.trk[1].info.color.as_deref(), Some("2d3ee9")); + } }