diff --git a/gpx-rs/Cargo.lock b/gpx-rs/Cargo.lock index 30a36cebc..05eaed167 100644 --- a/gpx-rs/Cargo.lock +++ b/gpx-rs/Cargo.lock @@ -2,38 +2,175 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "cc" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "gpx-rs" version = "0.1.0" dependencies = [ + "chrono", "quick-xml", "wasm-bindgen", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + [[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -67,6 +204,18 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "syn" version = "2.0.118" @@ -128,3 +277,62 @@ checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] diff --git a/gpx-rs/Cargo.toml b/gpx-rs/Cargo.toml index bf76571c5..2e7a55fe1 100644 --- a/gpx-rs/Cargo.toml +++ b/gpx-rs/Cargo.toml @@ -7,5 +7,6 @@ edition = "2024" crate-type = ["cdylib"] [dependencies] +chrono = "0.4.45" quick-xml = "0.42.0" wasm-bindgen = "0.2" diff --git a/gpx-rs/data/with_power_2.gpx b/gpx-rs/data/with_power_2.gpx index b0a530733..717fe4cf7 100644 --- a/gpx-rs/data/with_power_2.gpx +++ b/gpx-rs/data/with_power_2.gpx @@ -1,9 +1,14 @@ + xmlns:gpxpx="http://www.garmin.com/xmlschemas/GpxExtensions/v3" + xmlns:gpx_style="http://www.topografix.com/GPX/gpx_style/0/2" version="1.1" + creator="https://gpx.studio"> with_power diff --git a/gpx-rs/src/actions/parse.rs b/gpx-rs/src/actions/parse.rs index d8a9a614b..1b9d4084d 100644 --- a/gpx-rs/src/actions/parse.rs +++ b/gpx-rs/src/actions/parse.rs @@ -4,6 +4,7 @@ use crate::gpx::{ Author, GPXFile, Link, LngLat, Track, TrackPoint, TrackPointChunk, TrackSegment, Waypoint, WaypointChunk, }; +use chrono::DateTime; use quick_xml::Error; use quick_xml::events::Event; use quick_xml::events::attributes::Attributes; @@ -22,10 +23,19 @@ enum GPXElement { SEGMENT(TrackSegment), TRACKPOINT(TrackPoint), WAYPOINT(Waypoint), + ELEVATION, + TIME, + TEMPERATURE, + HEARTRATE, + CADENCE, + POWER, SYMBOL, TYPE, - ELEVATION, + COLOR, + OPACITY, + WIDTH, } + fn parse_coordinates(attributes: Attributes<'_>) -> LngLat { let mut coordinates = LngLat::default(); for attr in attributes { @@ -83,8 +93,17 @@ pub fn parse(data: &[u8]) -> Result { stack.push(GPXElement::WAYPOINT(wpt)); } "ele" => stack.push(GPXElement::ELEVATION), + "time" => stack.push(GPXElement::TIME), + e if e.ends_with("atemp") => stack.push(GPXElement::TEMPERATURE), + e if e.ends_with("hr") => stack.push(GPXElement::HEARTRATE), + e if e.ends_with("cad") => stack.push(GPXElement::CADENCE), + "power" => stack.push(GPXElement::POWER), + e if e.ends_with("PowerInWatts") => stack.push(GPXElement::POWER), "sym" => stack.push(GPXElement::SYMBOL), "type" => stack.push(GPXElement::TYPE), + e if e.ends_with("color") => stack.push(GPXElement::COLOR), + e if e.ends_with("opacity") => stack.push(GPXElement::OPACITY), + e if e.ends_with("width") => stack.push(GPXElement::WIDTH), _ => println!("{:?}", e), }, Ok(Event::End(e)) => match e.name().as_ref() { @@ -229,6 +248,47 @@ pub fn parse(data: &[u8]) -> Result { _ => (), } } + Some(GPXElement::TIME) => { + stack.pop(); + if let Some(GPXElement::TRACKPOINT(trkpt)) = stack.last_mut() { + if let Ok(time) = DateTime::parse_from_rfc3339(e.as_ref()) { + trkpt.time = Some(time.timestamp_millis()); + } + } + } + Some(GPXElement::TEMPERATURE) => { + stack.pop(); + if let Some(GPXElement::TRACKPOINT(trkpt)) = stack.last_mut() { + trkpt.atemp = e.parse().ok(); + } + } + Some(GPXElement::HEARTRATE) => { + stack.pop(); + if let Some(GPXElement::TRACKPOINT(trkpt)) = stack.last_mut() { + trkpt.hr = e.parse().ok(); + } + } + Some(GPXElement::CADENCE) => { + stack.pop(); + if let Some(GPXElement::TRACKPOINT(trkpt)) = stack.last_mut() { + trkpt.cad = e.parse().ok(); + } + } + Some(GPXElement::POWER) => { + stack.pop(); + if let Some(GPXElement::TRACKPOINT(trkpt)) = stack.last_mut() { + trkpt.power = e.parse().ok(); + } + } + Some(GPXElement::SYMBOL) => { + stack.pop(); + match stack.last_mut() { + Some(GPXElement::WAYPOINT(wpt)) => { + wpt.sym = Some(e.to_string()); + } + _ => (), + } + } Some(GPXElement::TYPE) => { stack.pop(); match stack.last_mut() { @@ -241,11 +301,31 @@ pub fn parse(data: &[u8]) -> Result { _ => (), } } - Some(GPXElement::SYMBOL) => { + Some(GPXElement::COLOR) => { stack.pop(); match stack.last_mut() { - Some(GPXElement::WAYPOINT(wpt)) => { - wpt.sym = Some(e.to_string()); + Some(GPXElement::TRACK(trk)) => { + let mut color = "#".to_string(); + color.push_str(&e); + trk.info.color = Some(color); + } + _ => (), + } + } + Some(GPXElement::OPACITY) => { + stack.pop(); + match stack.last_mut() { + Some(GPXElement::TRACK(trk)) => { + trk.info.opacity = e.parse().ok(); + } + _ => (), + } + } + Some(GPXElement::WIDTH) => { + stack.pop(); + match stack.last_mut() { + Some(GPXElement::TRACK(trk)) => { + trk.info.width = e.parse().ok(); } _ => (), } @@ -301,8 +381,8 @@ mod tests { assert!(link.text.as_ref().is_some_and(|t| t == "track link text")); assert!(trk.info.type_.as_ref().is_some_and(|c| c == "Cycling")); - assert_eq!(gpx.trk[0].trkseg.len(), 1); - let trkseg = &gpx.trk[0].trkseg[0]; + assert_eq!(trk.trkseg.len(), 1); + let trkseg = &trk.trkseg[0]; assert_eq!(trkseg.chunks.len(), 1); let chunk = &trkseg.chunks[0]; assert_eq!(chunk.trkpt.len(), 80); @@ -312,13 +392,130 @@ mod tests { assert_eq!(trkpt.ele, 109.0); } + #[test] + fn test_parse_tracks() { + let mut f = File::open("data/with_tracks.gpx").unwrap(); + let mut data = String::new(); + let _ = f.read_to_string(&mut data); + let gpx = parse(data.as_bytes()).unwrap(); + + assert_eq!(gpx.trk.len(), 2); + let trk = &gpx.trk[0]; + assert!(trk.info.name.as_ref().is_some_and(|n| n == "track 1")); + assert!(trk.info.type_.as_ref().is_some_and(|c| c == "Cycling")); + + assert_eq!(trk.trkseg.len(), 1); + let trkseg = &trk.trkseg[0]; + assert_eq!(trkseg.chunks.len(), 1); + let chunk = &trkseg.chunks[0]; + assert_eq!(chunk.trkpt.len(), 49); + let trkpt = &chunk.trkpt[0]; + assert_eq!(trkpt.coordinates.lat, 50.790867); + assert_eq!(trkpt.coordinates.lng, 4.404968); + assert_eq!(trkpt.ele, 109.0); + + let trk = &gpx.trk[1]; + assert!(trk.info.name.as_ref().is_some_and(|n| n == "track 2")); + assert!(trk.info.type_.as_ref().is_some_and(|c| c == "Cycling")); + + assert_eq!(trk.trkseg.len(), 1); + let trkseg = &trk.trkseg[0]; + assert_eq!(trkseg.chunks.len(), 1); + let chunk = &trkseg.chunks[0]; + assert_eq!(chunk.trkpt.len(), 28); + let trkpt = &chunk.trkpt[0]; + assert_eq!(trkpt.coordinates.lat, 50.782212); + assert_eq!(trkpt.coordinates.lng, 4.406377); + assert_eq!(trkpt.ele, 115.5); + } + + #[test] + fn test_parse_segments() { + let mut f = File::open("data/with_segments.gpx").unwrap(); + let mut data = String::new(); + let _ = f.read_to_string(&mut data); + let gpx = parse(data.as_bytes()).unwrap(); + + assert_eq!(gpx.trk.len(), 1); + let trk = &gpx.trk[0]; + + assert_eq!(trk.trkseg.len(), 2); + let trkseg = &trk.trkseg[0]; + assert_eq!(trkseg.chunks.len(), 1); + let chunk = &trkseg.chunks[0]; + assert_eq!(chunk.trkpt.len(), 49); + let trkpt = &chunk.trkpt[0]; + assert_eq!(trkpt.coordinates.lat, 50.790867); + assert_eq!(trkpt.coordinates.lng, 4.404968); + assert_eq!(trkpt.ele, 109.0); + + let trkseg = &trk.trkseg[1]; + assert_eq!(trkseg.chunks.len(), 1); + let chunk = &trkseg.chunks[0]; + assert_eq!(chunk.trkpt.len(), 28); + let trkpt = &chunk.trkpt[0]; + assert_eq!(trkpt.coordinates.lat, 50.782212); + assert_eq!(trkpt.coordinates.lng, 4.406377); + assert_eq!(trkpt.ele, 115.5); + } + + #[test] + fn test_parse_tracks_and_segments() { + let mut f = File::open("data/with_tracks_and_segments.gpx").unwrap(); + let mut data = String::new(); + let _ = f.read_to_string(&mut data); + let gpx = parse(data.as_bytes()).unwrap(); + + assert_eq!(gpx.trk.len(), 2); + let trk = &gpx.trk[0]; + + assert_eq!(trk.trkseg.len(), 2); + let trkseg = &trk.trkseg[0]; + assert_eq!(trkseg.chunks.len(), 1); + let chunk = &trkseg.chunks[0]; + assert_eq!(chunk.trkpt.len(), 16); + let trkpt = &chunk.trkpt[0]; + assert_eq!(trkpt.coordinates.lat, 50.790867); + assert_eq!(trkpt.coordinates.lng, 4.404968); + assert_eq!(trkpt.ele, 109.0); + + let trkseg = &trk.trkseg[1]; + assert_eq!(trkseg.chunks.len(), 1); + let chunk = &trkseg.chunks[0]; + assert_eq!(chunk.trkpt.len(), 34); + let trkpt = &chunk.trkpt[0]; + assert_eq!(trkpt.coordinates.lat, 50.78727108169855); + assert_eq!(trkpt.coordinates.lng, 4.406133681127736); + assert_eq!(trkpt.ele, 115.0); + + let trk = &gpx.trk[1]; + + assert_eq!(trk.trkseg.len(), 2); + let trkseg = &trk.trkseg[0]; + assert_eq!(trkseg.chunks.len(), 1); + let chunk = &trkseg.chunks[0]; + assert_eq!(chunk.trkpt.len(), 19); + let trkpt = &chunk.trkpt[0]; + assert_eq!(trkpt.coordinates.lat, 50.782212); + assert_eq!(trkpt.coordinates.lng, 4.406377); + assert_eq!(trkpt.ele, 115.5); + + let trkseg = &trk.trkseg[1]; + assert_eq!(trkseg.chunks.len(), 1); + let chunk = &trkseg.chunks[0]; + assert_eq!(chunk.trkpt.len(), 10); + let trkpt = &chunk.trkpt[0]; + assert_eq!(trkpt.coordinates.lat, 50.77906316558724); + assert_eq!(trkpt.coordinates.lng, 4.412547477922485); + assert_eq!(trkpt.ele, 133.3); + } + #[test] fn test_parse_waypoint() { let mut f = File::open("data/with_waypoint.gpx").unwrap(); let mut data = String::new(); let _ = f.read_to_string(&mut data); let gpx = parse(data.as_bytes()).unwrap(); - println!("{:?}", gpx); assert_eq!(gpx.wpt.len(), 1); let chunk = &gpx.wpt[0]; @@ -344,4 +541,127 @@ mod tests { assert!(wpt.sym.as_ref().is_some_and(|s| s == "Bike Trail")); assert!(wpt.type_.as_ref().is_some_and(|t| t == "Bike Trail")); } + + #[test] + fn test_parse_trackpoint_time() { + let mut f = File::open("data/with_time.gpx").unwrap(); + let mut data = String::new(); + let _ = f.read_to_string(&mut data); + let gpx = parse(data.as_bytes()).unwrap(); + + assert_eq!(gpx.trk.len(), 1); + let trk = &gpx.trk[0]; + assert_eq!(trk.trkseg.len(), 1); + let trkseg = &trk.trkseg[0]; + assert_eq!(trkseg.chunks.len(), 1); + let chunk = &trkseg.chunks[0]; + assert!(!chunk.trkpt.is_empty()); + let trkpt = &chunk.trkpt[0]; + assert!(trkpt.time.is_some_and(|t| t == 1704063600000)); + } + + #[test] + fn test_parse_trackpoint_hr() { + let mut f = File::open("data/with_hr.gpx").unwrap(); + let mut data = String::new(); + let _ = f.read_to_string(&mut data); + let gpx = parse(data.as_bytes()).unwrap(); + + assert_eq!(gpx.trk.len(), 1); + let trk = &gpx.trk[0]; + assert_eq!(trk.trkseg.len(), 1); + let trkseg = &trk.trkseg[0]; + assert_eq!(trkseg.chunks.len(), 1); + let chunk = &trkseg.chunks[0]; + assert!(!chunk.trkpt.is_empty()); + let trkpt = &chunk.trkpt[0]; + assert!(trkpt.hr.is_some_and(|h| h == 150)); + } + + #[test] + fn test_parse_trackpoint_cad() { + let mut f = File::open("data/with_cad.gpx").unwrap(); + let mut data = String::new(); + let _ = f.read_to_string(&mut data); + let gpx = parse(data.as_bytes()).unwrap(); + + assert_eq!(gpx.trk.len(), 1); + let trk = &gpx.trk[0]; + assert_eq!(trk.trkseg.len(), 1); + let trkseg = &trk.trkseg[0]; + assert_eq!(trkseg.chunks.len(), 1); + let chunk = &trkseg.chunks[0]; + assert!(!chunk.trkpt.is_empty()); + let trkpt = &chunk.trkpt[0]; + assert!(trkpt.cad.is_some_and(|c| c == 80)); + } + + #[test] + fn test_parse_trackpoint_power_1() { + let mut f = File::open("data/with_power_1.gpx").unwrap(); + let mut data = String::new(); + let _ = f.read_to_string(&mut data); + let gpx = parse(data.as_bytes()).unwrap(); + + assert_eq!(gpx.trk.len(), 1); + let trk = &gpx.trk[0]; + assert_eq!(trk.trkseg.len(), 1); + let trkseg = &trk.trkseg[0]; + assert_eq!(trkseg.chunks.len(), 1); + let chunk = &trkseg.chunks[0]; + assert!(!chunk.trkpt.is_empty()); + let trkpt = &chunk.trkpt[0]; + assert!(trkpt.power.is_some_and(|p| p == 200)); + } + + #[test] + fn test_parse_trackpoint_power_2() { + let mut f = File::open("data/with_power_2.gpx").unwrap(); + let mut data = String::new(); + let _ = f.read_to_string(&mut data); + let gpx = parse(data.as_bytes()).unwrap(); + + assert_eq!(gpx.trk.len(), 1); + let trk = &gpx.trk[0]; + assert_eq!(trk.trkseg.len(), 1); + let trkseg = &trk.trkseg[0]; + assert_eq!(trkseg.chunks.len(), 1); + let chunk = &trkseg.chunks[0]; + assert!(!chunk.trkpt.is_empty()); + let trkpt = &chunk.trkpt[0]; + assert!(trkpt.power.is_some_and(|p| p == 200)); + } + + #[test] + fn test_parse_trackpoint_atemp() { + let mut f = File::open("data/with_temp.gpx").unwrap(); + let mut data = String::new(); + let _ = f.read_to_string(&mut data); + let gpx = parse(data.as_bytes()).unwrap(); + + assert_eq!(gpx.trk.len(), 1); + let trk = &gpx.trk[0]; + assert_eq!(trk.trkseg.len(), 1); + let trkseg = &trk.trkseg[0]; + assert_eq!(trkseg.chunks.len(), 1); + let chunk = &trkseg.chunks[0]; + assert!(!chunk.trkpt.is_empty()); + let trkpt = &chunk.trkpt[0]; + assert!(trkpt.atemp.is_some_and(|t| t == 21)); + } + + #[test] + fn test_parse_track_style() { + let mut f = File::open("data/with_style.gpx").unwrap(); + let mut data = String::new(); + let _ = f.read_to_string(&mut data); + let gpx = parse(data.as_bytes()).unwrap(); + + assert_eq!(gpx.trk.len(), 1); + let trk = &gpx.trk[0]; + assert_eq!(trk.trkseg.len(), 1); + assert!(trk.info.color.as_ref().is_some_and(|c| c == "#2d3ee9")); + assert!(trk.info.opacity.is_some_and(|o| o == 0.5)); + assert!(trk.info.width.is_some_and(|w| w == 6.0)); + } } diff --git a/gpx-rs/src/gpx/common.rs b/gpx-rs/src/gpx/common.rs index 6e1619c05..f025fe792 100644 --- a/gpx-rs/src/gpx/common.rs +++ b/gpx-rs/src/gpx/common.rs @@ -9,3 +9,9 @@ pub struct LngLat { pub lng: f64, pub lat: f64, } + +#[derive(Debug, Default)] +pub struct LngLatBounds { + pub sw: LngLat, + pub ne: LngLat, +} diff --git a/gpx-rs/src/gpx/trackpoint.rs b/gpx-rs/src/gpx/trackpoint.rs index 7659d14c9..cae899d1c 100644 --- a/gpx-rs/src/gpx/trackpoint.rs +++ b/gpx-rs/src/gpx/trackpoint.rs @@ -5,9 +5,9 @@ pub struct TrackPoint { pub coordinates: LngLat, pub ele: f64, pub time: Option, + pub atemp: Option, pub hr: Option, pub cad: Option, pub power: Option, - pub atemp: Option, // TODO OSM data? or store intervals at a higher level? } diff --git a/gpx-rs/src/lib.rs b/gpx-rs/src/lib.rs index f3d3e7265..d8c1f9054 100644 --- a/gpx-rs/src/lib.rs +++ b/gpx-rs/src/lib.rs @@ -2,3 +2,5 @@ mod actions; mod controller; mod gpx; mod stack; +mod statistics; +mod utils; diff --git a/gpx-rs/src/statistics.rs b/gpx-rs/src/statistics.rs new file mode 100644 index 000000000..4545c2598 --- /dev/null +++ b/gpx-rs/src/statistics.rs @@ -0,0 +1,113 @@ +use crate::{ + gpx::{LngLat, LngLatBounds, TrackPoint, TrackPointChunk}, + utils::{distance, speed}, +}; + +#[derive(Default)] +pub struct GPXStatistics { + pub total_distance: f64, + pub moving_distance: f64, + pub moving_time: i64, + pub elevation_gain: f64, + pub elevation_loss: f64, + pub start_time: Option, + pub end_time: Option, + pub bounds: LngLatBounds, + pub local: Vec, +} + +impl GPXStatistics { + fn accumulate(&mut self, prev: &TrackPoint, cur: &TrackPoint) { + self.accumulate_distance_and_time(prev, cur); + self.update_time_bounds(cur.time); + self.update_bounds(&cur.coordinates); + self.local + .push(TrackpointStatistics::from_partial_stats(&self)); + } + + fn accumulate_distance_and_time(&mut self, prev: &TrackPoint, cur: &TrackPoint) { + let dist = distance(&prev.coordinates, &cur.coordinates); + let time = prev.time.zip(cur.time).map(|(t1, t2)| t2 - t1); + + self.total_distance += dist; + + if let Some(time) = time { + let speed = speed(dist, time); + if speed >= 0.5 && speed <= 1500.0 { + self.moving_distance += dist; + self.moving_time += time; + } + } + } + + fn update_time_bounds(&mut self, time: Option) { + if let Some(time) = time { + if self.start_time.is_none() { + self.start_time = Some(time); + } + self.end_time = Some(time); + } + } + + fn update_bounds(&mut self, coordinates: &LngLat) { + self.bounds.sw.lng = self.bounds.sw.lng.min(coordinates.lng); + self.bounds.sw.lat = self.bounds.sw.lat.min(coordinates.lat); + self.bounds.ne.lng = self.bounds.ne.lng.min(coordinates.lng); + self.bounds.ne.lat = self.bounds.ne.lat.min(coordinates.lat); + } +} + +#[derive(Default)] +pub struct SlopeSegment { + pub slope: f64, + pub distance: f64, +} + +#[derive(Default)] +pub struct TrackpointStatistics { + pub total_distance: f64, + pub moving_distance: f64, + pub total_time: i64, + pub moving_time: i64, + pub speed: f64, + pub elevation_gain: f64, + pub elevation_loss: f64, + pub slope: f64, + pub slope_segment: SlopeSegment, +} + +impl TrackpointStatistics { + fn from_partial_stats(stats: &GPXStatistics) -> Self { + Self { + total_distance: stats.total_distance, + moving_distance: stats.moving_distance, + total_time: stats + .start_time + .zip(stats.end_time) + .map_or(0, |(t1, t2)| t2 - t1), + moving_time: stats.moving_time, + speed: todo!(), + elevation_gain: stats.elevation_gain, + elevation_loss: stats.elevation_loss, + slope: Default::default(), + slope_segment: Default::default(), + } + } +} + +impl GPXStatistics { + pub fn compute(chunk: &TrackPointChunk) -> Self { + let mut stats = Self::default(); + if chunk.trkpt.is_empty() { + return stats; + } + + let mut prev = &chunk.trkpt[0]; + for i in 0..chunk.trkpt.len() { + let cur = &chunk.trkpt[i]; + stats.accumulate(prev, cur); + prev = cur; + } + stats + } +} diff --git a/gpx-rs/src/utils.rs b/gpx-rs/src/utils.rs new file mode 100644 index 000000000..d453fe1c5 --- /dev/null +++ b/gpx-rs/src/utils.rs @@ -0,0 +1,24 @@ +use std::f64::consts::PI; + +use crate::gpx::LngLat; + +static TO_RADIANS: f64 = PI / 180.0; +static EARTH_RADIUS: f64 = 6371.0088; + +/// Computes the distance in kilometers between two coordinates using the Haversine formula +pub fn distance(coord1: &LngLat, coord2: &LngLat) -> f64 { + let lat1 = coord1.lat * TO_RADIANS; + let lat2 = coord2.lat * TO_RADIANS; + let delta_lat = lat2 - lat1; + let delta_lng = (coord2.lng - coord1.lng) * TO_RADIANS; + + let a = + (delta_lat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (delta_lng / 2.0).sin().powi(2); + let c = 2.0 * a.min(1.0).sqrt().asin(); + EARTH_RADIUS * c +} + +/// Computes the speed for a given distance in kilometers and a time in milliseconds +pub fn speed(distance: f64, time: i64) -> f64 { + distance / (time as f64 / 3600_000.0) +}