From f1904bc12f440253e13d3264a39146bcec1af35e Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sat, 3 Oct 2026 20:54:05 +0200 Subject: [PATCH] cleaning --- gpx-rs/engine/src/core/action/file.rs | 1 - gpx-rs/engine/src/core/action/mod.rs | 3 - gpx-rs/engine/src/core/algorithm/mod.rs | 1 - gpx-rs/engine/src/core/algorithm/simplify.rs | 10 +- gpx-rs/engine/src/core/algorithm/smooth.rs | 4 +- gpx-rs/engine/src/core/gpx/chunk.rs | 11 +- gpx-rs/engine/src/core/gpx/segment.rs | 18 +- gpx-rs/engine/src/core/io/parse.rs | 188 ++++++------------ gpx-rs/engine/src/core/mod.rs | 2 - .../engine/src/core/statistics/statistics.rs | 56 +++--- gpx-rs/engine/src/core/utils.rs | 139 ++++++------- .../src/engine/command/file/duplicate.rs | 5 +- .../src/engine/derived/coordinates_cache.rs | 18 +- .../engine/derived/file_structure_cache.rs | 8 +- gpx-rs/engine/src/engine/engine.rs | 20 +- gpx-rs/engine/src/engine/state/stack.rs | 12 +- gpx-rs/wasm/src/lib.rs | 36 ++-- 17 files changed, 222 insertions(+), 310 deletions(-) delete mode 100644 gpx-rs/engine/src/core/action/file.rs delete mode 100644 gpx-rs/engine/src/core/action/mod.rs diff --git a/gpx-rs/engine/src/core/action/file.rs b/gpx-rs/engine/src/core/action/file.rs deleted file mode 100644 index 8b1378917..000000000 --- a/gpx-rs/engine/src/core/action/file.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/gpx-rs/engine/src/core/action/mod.rs b/gpx-rs/engine/src/core/action/mod.rs deleted file mode 100644 index 529c201fa..000000000 --- a/gpx-rs/engine/src/core/action/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod file; - -pub use file::*; diff --git a/gpx-rs/engine/src/core/algorithm/mod.rs b/gpx-rs/engine/src/core/algorithm/mod.rs index ea1c6c971..95fdaf8de 100644 --- a/gpx-rs/engine/src/core/algorithm/mod.rs +++ b/gpx-rs/engine/src/core/algorithm/mod.rs @@ -2,4 +2,3 @@ mod simplify; mod smooth; pub use simplify::*; -pub use smooth::*; diff --git a/gpx-rs/engine/src/core/algorithm/simplify.rs b/gpx-rs/engine/src/core/algorithm/simplify.rs index 9a2fe0723..5f67a1091 100644 --- a/gpx-rs/engine/src/core/algorithm/simplify.rs +++ b/gpx-rs/engine/src/core/algorithm/simplify.rs @@ -59,12 +59,10 @@ fn ramer_douglas_peucker_helper( cur = trkseg.next_index(cur); } - if let Some(idx) = max_idx { - if max_dist > epsilon { - ramer_douglas_peucker_helper(trkseg, start, idx, mapping, epsilon, indices); - indices.push(idx); - ramer_douglas_peucker_helper(trkseg, idx, end, mapping, epsilon, indices); - } + if let Some(idx) = max_idx.filter(|_| max_dist > epsilon) { + ramer_douglas_peucker_helper(trkseg, start, idx, mapping, epsilon, indices); + indices.push(idx); + ramer_douglas_peucker_helper(trkseg, idx, end, mapping, epsilon, indices); } } diff --git a/gpx-rs/engine/src/core/algorithm/smooth.rs b/gpx-rs/engine/src/core/algorithm/smooth.rs index 9d1ffbccf..046b7f67e 100644 --- a/gpx-rs/engine/src/core/algorithm/smooth.rs +++ b/gpx-rs/engine/src/core/algorithm/smooth.rs @@ -55,7 +55,7 @@ macro_rules! for_each_window { #[cfg(test)] mod tests { - use crate::{TrackSegment, Trackpoint, TrackpointChunk, for_each_window}; + use crate::{TrackSegment, Trackpoint, TrackpointChunk}; fn segment(eles: &[f64]) -> TrackSegment { let mut trkseg = TrackSegment::default(); @@ -116,7 +116,7 @@ mod tests { #[test] fn test_window_sub_range() { - let trkseg = segment(&vec![0.0; 10]); + let trkseg = segment(&[0.0; 10]); let mut centers = vec![]; let mut first = trkseg.first_index(); for _ in 0..3 { diff --git a/gpx-rs/engine/src/core/gpx/chunk.rs b/gpx-rs/engine/src/core/gpx/chunk.rs index 3fd661910..f0878bf7d 100644 --- a/gpx-rs/engine/src/core/gpx/chunk.rs +++ b/gpx-rs/engine/src/core/gpx/chunk.rs @@ -2,7 +2,7 @@ use uuid::Uuid; use crate::{Trackpoint, Waypoint}; -static MAX_TRKPT_CHUNK_SIZE: usize = 4096; +const MAX_TRKPT_CHUNK_SIZE: usize = 4096; #[derive(Debug, PartialEq, Eq)] pub struct TrackpointChunkId(Uuid); @@ -40,7 +40,7 @@ impl Default for WaypointChunkId { } } -static MAX_WPT_CHUNK_SIZE: usize = 128; +const MAX_WPT_CHUNK_SIZE: usize = 128; #[derive(Debug, Default)] pub struct WaypointChunk { @@ -91,9 +91,10 @@ mod tests { let a = TrackpointChunk::default(); let b = TrackpointChunk::default(); assert_ne!(a, b); - let mut c = TrackpointChunk::default(); - c.id = TrackpointChunkId(a.id.0); - c.trkpt.push(Trackpoint::default()); + let c = TrackpointChunk { + id: TrackpointChunkId(a.id.0), + trkpt: vec![Trackpoint::default()], + }; assert_eq!(a, c); } } diff --git a/gpx-rs/engine/src/core/gpx/segment.rs b/gpx-rs/engine/src/core/gpx/segment.rs index a3c33c6df..639d20cd1 100644 --- a/gpx-rs/engine/src/core/gpx/segment.rs +++ b/gpx-rs/engine/src/core/gpx/segment.rs @@ -109,6 +109,10 @@ impl TrackSegment { self.cumul_length.last().copied().unwrap_or_default() } + pub fn is_empty(&self) -> bool { + self.chunks.is_empty() + } + pub fn iter(&self) -> TrackSegmentIterator<'_> { TrackSegmentIterator::new(self) } @@ -248,9 +252,10 @@ mod tests { for n in 1..=nb_chunks { let mut chunk = TrackpointChunk::default(); for _ in 0..n { - let mut trkpt = Trackpoint::default(); - trkpt.ele = count as f64; - chunk.trkpt.push(trkpt); + chunk.trkpt.push(Trackpoint { + ele: count as f64, + ..Default::default() + }); count += 1; } trkseg.push(chunk); @@ -367,7 +372,7 @@ mod tests { fn test_index_out_of_bounds_1() { let nb_chunks = 10; let trkseg = create_track_segment(nb_chunks); - trkseg[trkseg.len()].ele; + let _ = trkseg[trkseg.len()]; } #[test] @@ -375,12 +380,11 @@ mod tests { fn test_index_out_of_bounds_2() { let nb_chunks = 10; let trkseg = create_track_segment(nb_chunks); - trkseg[TrackSegmentIndex { + let _ = trkseg[TrackSegmentIndex { chunk: trkseg.chunks.len(), pos: 0, flat: 0, - }] - .ele; + }]; } #[test] diff --git a/gpx-rs/engine/src/core/io/parse.rs b/gpx-rs/engine/src/core/io/parse.rs index f3850f96d..61613b812 100644 --- a/gpx-rs/engine/src/core/io/parse.rs +++ b/gpx-rs/engine/src/core/io/parse.rs @@ -38,13 +38,11 @@ enum GPXElement { fn parse_coordinates(attributes: Attributes<'_>) -> LngLat { let mut coordinates = LngLat::default(); - for attr in attributes { - if let Ok(attr) = attr { - match attr.key.as_ref() { - "lat" => coordinates.lat = attr.value.parse().unwrap_or_default(), - "lon" => coordinates.lng = attr.value.parse().unwrap_or_default(), - _ => (), - } + for attr in attributes.flatten() { + match attr.key.as_ref() { + "lat" => coordinates.lat = attr.value.parse().unwrap_or_default(), + "lon" => coordinates.lng = attr.value.parse().unwrap_or_default(), + _ => (), } } coordinates @@ -68,11 +66,9 @@ pub fn parse(data: &[u8]) -> Result { "author" => stack.push(GPXElement::Author(Author::default())), "link" => { let mut link = Link::default(); - for attr in e.attributes() { - if let Ok(attr) = attr { - if attr.key.as_ref() == "href" { - link.href = attr.value.to_string(); - } + for attr in e.attributes().flatten() { + if attr.key.as_ref() == "href" { + link.href = attr.value.to_string(); } } stack.push(GPXElement::Link(link)); @@ -83,14 +79,16 @@ pub fn parse(data: &[u8]) -> Result { stack.push(GPXElement::Segment(TrackSegment::default())); } "trkpt" => { - let mut trkpt = Trackpoint::default(); - trkpt.coordinates = parse_coordinates(e.attributes()); - stack.push(GPXElement::Trackpoint(trkpt)); + stack.push(GPXElement::Trackpoint(Trackpoint { + coordinates: parse_coordinates(e.attributes()), + ..Default::default() + })); } "wpt" => { - let mut wpt = Waypoint::default(); - wpt.coordinates = parse_coordinates(e.attributes()); - stack.push(GPXElement::Waypoint(wpt)); + stack.push(GPXElement::Waypoint(Waypoint { + coordinates: parse_coordinates(e.attributes()), + ..Default::default() + })); } "ele" => stack.push(GPXElement::Elevation), "time" => stack.push(GPXElement::Time), @@ -109,8 +107,7 @@ pub fn parse(data: &[u8]) -> Result { Ok(Event::End(e)) => match e.name().as_ref() { "gpx" => { if !wpt_chunk.wpt.is_empty() { - gpx.wpt.push(Rc::new(wpt_chunk)); - wpt_chunk = WaypointChunk::default(); + gpx.wpt.push(Rc::new(std::mem::take(&mut wpt_chunk))); } } "metadata" => { @@ -143,24 +140,21 @@ pub fn parse(data: &[u8]) -> Result { } } "trkseg" => { - if let Some(GPXElement::Segment(mut trkseg)) = stack.pop() { - if let Some(GPXElement::Track(trk)) = stack.last_mut() { - if !trkpt_chunk.trkpt.is_empty() { - trkseg.push(trkpt_chunk); - trkpt_chunk = TrackpointChunk::default(); - } - trk.trkseg.push(trkseg); - } + if let Some(GPXElement::Segment(mut trkseg)) = stack.pop() + && let Some(GPXElement::Track(trk)) = stack.last_mut() + { + // `push` ignores the chunk if it is empty + trkseg.push(std::mem::take(&mut trkpt_chunk)); + trk.trkseg.push(trkseg); } } "trkpt" => { - if let Some(GPXElement::Trackpoint(trkpt)) = stack.pop() { - if let Some(GPXElement::Segment(trkseg)) = stack.last_mut() { - trkpt_chunk.trkpt.push(trkpt); - if trkpt_chunk.is_full() { - trkseg.push(trkpt_chunk); - trkpt_chunk = TrackpointChunk::default(); - } + if let Some(GPXElement::Trackpoint(trkpt)) = stack.pop() + && let Some(GPXElement::Segment(trkseg)) = stack.last_mut() + { + trkpt_chunk.trkpt.push(trkpt); + if trkpt_chunk.is_full() { + trkseg.push(std::mem::take(&mut trkpt_chunk)); } } } @@ -168,8 +162,7 @@ pub fn parse(data: &[u8]) -> Result { if let Some(GPXElement::Waypoint(wpt)) = stack.pop() { wpt_chunk.wpt.push(wpt); if wpt_chunk.is_full() { - gpx.wpt.push(Rc::new(wpt_chunk)); - wpt_chunk = WaypointChunk::default(); + gpx.wpt.push(Rc::new(std::mem::take(&mut wpt_chunk))); } } } @@ -223,11 +216,8 @@ pub fn parse(data: &[u8]) -> Result { } Some(GPXElement::Source) => { stack.pop(); - match stack.last_mut() { - Some(GPXElement::Track(trk)) => { - trk.info.src = Some(e.to_string()); - } - _ => (), + if let Some(GPXElement::Track(trk)) = stack.last_mut() { + trk.info.src = Some(e.to_string()); } } Some(GPXElement::Text) => { @@ -251,9 +241,9 @@ 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()); - } + trkpt.time = DateTime::parse_from_rfc3339(e.as_ref()) + .ok() + .map(|time| time.timestamp_millis()); } } Some(GPXElement::Temperature) => { @@ -282,11 +272,8 @@ pub fn parse(data: &[u8]) -> Result { } Some(GPXElement::Symbol) => { stack.pop(); - match stack.last_mut() { - Some(GPXElement::Waypoint(wpt)) => { - wpt.sym = Some(e.to_string()); - } - _ => (), + if let Some(GPXElement::Waypoint(wpt)) = stack.last_mut() { + wpt.sym = Some(e.to_string()); } } Some(GPXElement::Type) => { @@ -303,31 +290,20 @@ pub fn parse(data: &[u8]) -> Result { } Some(GPXElement::Color) => { stack.pop(); - match stack.last_mut() { - Some(GPXElement::Track(trk)) => { - let mut color = "#".to_string(); - color.push_str(&e); - trk.info.color = Some(color); - } - _ => (), + if let Some(GPXElement::Track(trk)) = stack.last_mut() { + trk.info.color = Some(format!("#{}", &*e)); } } Some(GPXElement::Opacity) => { stack.pop(); - match stack.last_mut() { - Some(GPXElement::Track(trk)) => { - trk.info.opacity = e.parse().ok(); - } - _ => (), + if let Some(GPXElement::Track(trk)) = stack.last_mut() { + 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(); - } - _ => (), + if let Some(GPXElement::Track(trk)) = stack.last_mut() { + trk.info.width = e.parse().ok(); } } _ => (), @@ -343,16 +319,15 @@ pub fn parse(data: &[u8]) -> Result { #[cfg(test)] mod tests { - use std::{fs::File, io::Read}; - use super::*; + fn parse_data(name: &str) -> File { + parse(&std::fs::read(format!("data/{name}.gpx")).unwrap()).unwrap() + } + #[test] fn test_parse_simple() { - let mut f = File::open("data/simple.gpx").unwrap(); - let mut data = String::new(); - let _ = f.read_to_string(&mut data); - let gpx = parse(data.as_bytes()).unwrap(); + let gpx = parse_data("simple"); assert_eq!(gpx.info.name, "simple"); assert!(gpx.info.desc.is_some_and(|d| d == "description")); @@ -392,10 +367,7 @@ mod tests { #[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(); + let gpx = parse_data("with_tracks"); assert_eq!(gpx.trk.len(), 2); let trk = &gpx.trk[0]; @@ -425,10 +397,7 @@ mod tests { #[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(); + let gpx = parse_data("with_segments"); assert_eq!(gpx.trk.len(), 1); let trk = &gpx.trk[0]; @@ -451,10 +420,7 @@ mod tests { #[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(); + let gpx = parse_data("with_tracks_and_segments"); assert_eq!(gpx.trk.len(), 2); let trk = &gpx.trk[0]; @@ -494,10 +460,7 @@ mod tests { #[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(); + let gpx = parse_data("with_waypoint"); assert_eq!(gpx.wpt.len(), 1); let chunk = &gpx.wpt[0]; @@ -526,106 +489,85 @@ mod tests { #[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(); + let gpx = parse_data("with_time"); assert_eq!(gpx.trk.len(), 1); let trk = &gpx.trk[0]; assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert!(trkseg.len() > 0); + assert!(!trkseg.is_empty()); let trkpt = &trkseg[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(); + let gpx = parse_data("with_hr"); assert_eq!(gpx.trk.len(), 1); let trk = &gpx.trk[0]; assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert!(trkseg.len() > 0); + assert!(!trkseg.is_empty()); let trkpt = &trkseg[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(); + let gpx = parse_data("with_cad"); assert_eq!(gpx.trk.len(), 1); let trk = &gpx.trk[0]; assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert!(trkseg.len() > 0); + assert!(!trkseg.is_empty()); let trkpt = &trkseg[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(); + let gpx = parse_data("with_power_1"); assert_eq!(gpx.trk.len(), 1); let trk = &gpx.trk[0]; assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert!(trkseg.len() > 0); + assert!(!trkseg.is_empty()); let trkpt = &trkseg[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(); + let gpx = parse_data("with_power_2"); assert_eq!(gpx.trk.len(), 1); let trk = &gpx.trk[0]; assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert!(trkseg.len() > 0); + assert!(!trkseg.is_empty()); let trkpt = &trkseg[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(); + let gpx = parse_data("with_temp"); assert_eq!(gpx.trk.len(), 1); let trk = &gpx.trk[0]; assert_eq!(trk.trkseg.len(), 1); let trkseg = &trk.trkseg[0]; - assert!(trkseg.len() > 0); + assert!(!trkseg.is_empty()); let trkpt = &trkseg[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(); + let gpx = parse_data("with_style"); assert_eq!(gpx.trk.len(), 1); let trk = &gpx.trk[0]; diff --git a/gpx-rs/engine/src/core/mod.rs b/gpx-rs/engine/src/core/mod.rs index b03150b56..1e058e91f 100644 --- a/gpx-rs/engine/src/core/mod.rs +++ b/gpx-rs/engine/src/core/mod.rs @@ -1,11 +1,9 @@ -mod action; mod algorithm; pub(crate) mod gpx; mod io; pub(crate) mod statistics; mod utils; -pub use action::*; pub use algorithm::*; pub use gpx::*; pub use io::*; diff --git a/gpx-rs/engine/src/core/statistics/statistics.rs b/gpx-rs/engine/src/core/statistics/statistics.rs index 4a9c470f4..efd96582e 100644 --- a/gpx-rs/engine/src/core/statistics/statistics.rs +++ b/gpx-rs/engine/src/core/statistics/statistics.rs @@ -20,7 +20,7 @@ impl Statistics { pub fn compute(trkseg: &TrackSegment) -> Self { let mut stats = Self::default(); - if trkseg.len() == 0 { + if trkseg.is_empty() { return stats; } @@ -42,18 +42,18 @@ impl Statistics { self.update_time_bounds(cur.time); self.update_bounds(cur.coordinates); self.local - .push(TrackpointStatistics::from_partial_stats(&self)); + .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 = time_diff(&cur.time, &prev.time); + let time = time_diff(cur.time, prev.time); self.global.total_distance += dist; if let Some(time) = time { let speed = speed(dist, time); - if speed >= 0.5 && speed <= 1500.0 { + if (0.5..=1500.0).contains(&speed) { self.global.moving_distance = self .global .moving_distance @@ -72,7 +72,7 @@ impl Statistics { self.global.start_time = Some(time); } self.global.end_time = Some(time); - self.global.total_time = time_diff(&self.global.end_time, &self.global.start_time); + self.global.total_time = time_diff(self.global.end_time, self.global.start_time); } } @@ -86,10 +86,10 @@ impl Statistics { trkseg.first_index(), trkseg.last_index(), Some(10000), - |i, j| time_diff(&trkseg[i].time, &trkseg[j].time), + |i, j| time_diff(trkseg[i].time, trkseg[j].time), |i, left, right| { self.local[i.flat].speed = - time_diff(&trkseg[right].time, &trkseg[left].time).map(|t| { + time_diff(trkseg[right].time, trkseg[left].time).map(|t| { speed( self.local[right.flat].total_distance - self.local[left.flat].total_distance, @@ -370,16 +370,20 @@ mod tests { #[test] fn test_global_merge() { - let mut a = GlobalStatistics::default(); - a.total_distance = 1.0; - a.elevation_gain = 10.0; - a.moving_time = Some(5); + let mut a = GlobalStatistics { + total_distance: 1.0, + elevation_gain: 10.0, + moving_time: Some(5), + ..Default::default() + }; a.bounds.extend(crate::LngLat { lng: 0.0, lat: 0.0 }); - let mut b = GlobalStatistics::default(); - b.total_distance = 2.0; - b.elevation_loss = 4.0; - b.moving_distance = Some(1.5); - b.moving_time = Some(7); + let mut b = GlobalStatistics { + total_distance: 2.0, + elevation_loss: 4.0, + moving_distance: Some(1.5), + moving_time: Some(7), + ..Default::default() + }; b.bounds.extend(crate::LngLat { lng: 2.0, lat: 3.0 }); a.merge(&b); @@ -390,14 +394,18 @@ mod tests { assert_eq!(a.moving_time, Some(12)); assert_eq!(a.total_time, None); - let mut c = GlobalStatistics::default(); - c.start_time = Some(1_000); - c.end_time = Some(4_000); - c.total_time = Some(3_000); - let mut d = GlobalStatistics::default(); - d.start_time = Some(10_000); - d.end_time = Some(12_000); - d.total_time = Some(2_000); + let mut c = GlobalStatistics { + start_time: Some(1_000), + end_time: Some(4_000), + total_time: Some(3_000), + ..Default::default() + }; + let d = GlobalStatistics { + start_time: Some(10_000), + end_time: Some(12_000), + total_time: Some(2_000), + ..Default::default() + }; c.merge(&d); // 3 s + 2 s, the gap between the two is ignored assert_eq!(c.total_time, Some(5_000)); diff --git a/gpx-rs/engine/src/core/utils.rs b/gpx-rs/engine/src/core/utils.rs index 4850d59ad..5c3a5307c 100644 --- a/gpx-rs/engine/src/core/utils.rs +++ b/gpx-rs/engine/src/core/utils.rs @@ -2,8 +2,8 @@ use std::f64::consts::PI; use crate::LngLat; -static TO_RADIANS: f64 = PI / 180.0; -static EARTH_RADIUS: f64 = 6371.0088; +const TO_RADIANS: f64 = PI / 180.0; +const EARTH_RADIUS: f64 = 6371.0088; /// Computes the distance in kilometers between two coordinates using the Haversine formula pub fn distance(p1: LngLat, p2: LngLat) -> f64 { @@ -18,16 +18,13 @@ pub fn distance(p1: LngLat, p2: LngLat) -> f64 { EARTH_RADIUS * c } -pub fn time_diff(a: &Option, b: &Option) -> Option { - match (a, b) { - (Some(t1), Some(t2)) => Some((t1 - t2) as i32), - _ => None, - } +pub fn time_diff(a: Option, b: Option) -> Option { + Some((a? - b?) as i32) } /// Computes the speed for a given distance in kilometers and a time in milliseconds pub fn speed(distance: f64, time: i32) -> f64 { - distance / (time as f64 / 3600_000.0) + distance / (time as f64 / 3_600_000.0) } pub fn slope(ele: f64, distance: f64) -> f64 { @@ -38,88 +35,78 @@ pub fn slope(ele: f64, distance: f64) -> f64 { } } -static METERS_PER_LATITUDE_DEGREE: f64 = 111320.0; +const METERS_PER_LATITUDE_DEGREE: f64 = 111320.0; -fn get_meters_per_longitude_degree(latitude: f64) -> f64 { - ((latitude * PI) / 180.0).cos() * METERS_PER_LATITUDE_DEGREE +/// Approximate planar projection, in meters, around the latitude of `origin` (ignores the +/// curvature of the earth). +struct Planar { + meters_per_longitude_degree: f64, } -// Calculates the point on the line segment defined by p1 and p2 -// that is closest to the third point, p3. -// Uses simple planar geometry (ignores earth curvature). -fn projected(p1: LngLat, p2: LngLat, p3: LngLat) -> LngLat { - // Convert to meters using approximate scaling - let meters_per_longitude_degree = get_meters_per_longitude_degree(p1.lat); +impl Planar { + fn around(origin: LngLat) -> Self { + Self { + meters_per_longitude_degree: (origin.lat * TO_RADIANS).cos() + * METERS_PER_LATITUDE_DEGREE, + } + } - let x1 = p1.lng * meters_per_longitude_degree; - let y1 = p1.lat * METERS_PER_LATITUDE_DEGREE; - let x2 = p2.lng * meters_per_longitude_degree; - let y2 = p2.lat * METERS_PER_LATITUDE_DEGREE; - let x3 = p3.lng * meters_per_longitude_degree; - let y3 = p3.lat * METERS_PER_LATITUDE_DEGREE; + fn to_meters(&self, p: LngLat) -> (f64, f64) { + ( + p.lng * self.meters_per_longitude_degree, + p.lat * METERS_PER_LATITUDE_DEGREE, + ) + } - let dx = x2 - x1; - let dy = y2 - y1; - let segment_length_squared = dx * dx + dy * dy; - - if segment_length_squared == 0.0 { - // p1 and p2 are the same point - p1 - } else { - // Project p3 onto the line defined by p1-p2 - let t = - 0.0_f64.max(1.0_f64.min(((x3 - x1) * dx + (y3 - y1) * dy) / segment_length_squared)); - - // Find the closest point on the segment - let proj_x = x1 + t * dx; - let proj_y = y1 + t * dy; - - // Convert back to degrees + fn to_degrees(&self, (x, y): (f64, f64)) -> LngLat { LngLat { - lng: proj_x / meters_per_longitude_degree, - lat: proj_y / METERS_PER_LATITUDE_DEGREE, + lng: x / self.meters_per_longitude_degree, + lat: y / METERS_PER_LATITUDE_DEGREE, } } } -/// Calculates the perpendicular distance in meters -/// between a line segment (defined by p1 and p2) and a third point, p3. -/// Uses simple planar geometry (ignores earth curvature). -fn crossarc_lnglat(p1: LngLat, p2: LngLat, p3: LngLat) -> f64 { - // Convert to meters using approximate scaling - let meters_per_longitude_degree = get_meters_per_longitude_degree(p1.lat); - crossarc( - p1.lng * meters_per_longitude_degree, - p1.lat * METERS_PER_LATITUDE_DEGREE, - p2.lng * meters_per_longitude_degree, - p2.lat * METERS_PER_LATITUDE_DEGREE, - p3.lng * meters_per_longitude_degree, - p3.lat * METERS_PER_LATITUDE_DEGREE, - ) -} - -pub fn crossarc(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> f64 { +/// Position, between 0 (at `(x1, y1)`) and 1 (at `(x2, y2)`), of the point of the segment that is +/// the closest to `(x3, y3)`. A degenerate segment (same ends) is a point. +fn closest_on_segment(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> f64 { let dx = x2 - x1; let dy = y2 - y1; let segment_length_squared = dx * dx + dy * dy; - if segment_length_squared == 0.0 { - // p1 and p2 are the same point - ((x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1)).sqrt() + 0.0 } else { - // Project p3 onto the line defined by p1 - p2 - let t = - 0.0_f64.max(1.0_f64.min(((x3 - x1) * dx + (y3 - y1) * dy) / segment_length_squared)); - - // Find the closest point on the segment - let proj_x = x1 + t * dx; - let proj_y = y1 + t * dy; - - // Return distance from p3 to the projected point - ((x3 - proj_x) * (x3 - proj_x) + (y3 - proj_y) * (y3 - proj_y)).sqrt() + (((x3 - x1) * dx + (y3 - y1) * dy) / segment_length_squared).clamp(0.0, 1.0) } } +/// Calculates the point on the line segment defined by p1 and p2 that is closest to the third +/// point, p3. Uses simple planar geometry (ignores earth curvature). +pub fn projected(p1: LngLat, p2: LngLat, p3: LngLat) -> LngLat { + let planar = Planar::around(p1); + let (x1, y1) = planar.to_meters(p1); + let (x2, y2) = planar.to_meters(p2); + let (x3, y3) = planar.to_meters(p3); + let t = closest_on_segment(x1, y1, x2, y2, x3, y3); + planar.to_degrees((x1 + t * (x2 - x1), y1 + t * (y2 - y1))) +} + +/// Calculates the perpendicular distance in meters between a line segment (defined by p1 and p2) +/// and a third point, p3. Uses simple planar geometry (ignores earth curvature). +pub fn crossarc_lnglat(p1: LngLat, p2: LngLat, p3: LngLat) -> f64 { + let planar = Planar::around(p1); + let (x1, y1) = planar.to_meters(p1); + let (x2, y2) = planar.to_meters(p2); + let (x3, y3) = planar.to_meters(p3); + crossarc(x1, y1, x2, y2, x3, y3) +} + +/// Distance from the point `(x3, y3)` to the segment `(x1, y1)`-`(x2, y2)`, in the units of the +/// coordinates (planar geometry). +pub fn crossarc(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> f64 { + let t = closest_on_segment(x1, y1, x2, y2, x3, y3); + (x3 - (x1 + t * (x2 - x1))).hypot(y3 - (y1 + t * (y2 - y1))) +} + #[cfg(test)] mod tests { use super::*; @@ -145,10 +132,10 @@ mod tests { #[test] fn test_time_diff() { - assert_eq!(time_diff(&Some(5000), &Some(2000)), Some(3000)); - assert_eq!(time_diff(&Some(2000), &Some(5000)), Some(-3000)); - assert_eq!(time_diff(&None, &Some(1)), None); - assert_eq!(time_diff(&Some(1), &None), None); + assert_eq!(time_diff(Some(5000), Some(2000)), Some(3000)); + assert_eq!(time_diff(Some(2000), Some(5000)), Some(-3000)); + assert_eq!(time_diff(None, Some(1)), None); + assert_eq!(time_diff(Some(1), None), None); } #[test] diff --git a/gpx-rs/engine/src/engine/command/file/duplicate.rs b/gpx-rs/engine/src/engine/command/file/duplicate.rs index 66446bee1..709f93ccf 100644 --- a/gpx-rs/engine/src/engine/command/file/duplicate.rs +++ b/gpx-rs/engine/src/engine/command/file/duplicate.rs @@ -194,12 +194,11 @@ fn copy_track(track: &Track) -> Track { } fn copy_file(file: &File) -> File { - let mut copy = File { + File { id: Default::default(), trk: file.trk.iter().map(copy_track).collect(), ..file.clone() - }; - copy + } } #[cfg(test)] diff --git a/gpx-rs/engine/src/engine/derived/coordinates_cache.rs b/gpx-rs/engine/src/engine/derived/coordinates_cache.rs index e74d22846..461c6166f 100644 --- a/gpx-rs/engine/src/engine/derived/coordinates_cache.rs +++ b/gpx-rs/engine/src/engine/derived/coordinates_cache.rs @@ -25,12 +25,10 @@ impl CoordinatesCache { .get(&file.id) .is_none_or(|(r, _)| *r != file.wpt_rev_id) { - let coordinates = file - .wpt - .iter() - .flat_map(|chunk| &chunk.wpt) - .flat_map(|wpt| [wpt.coordinates.lng, wpt.coordinates.lat]) - .collect(); + let mut coordinates = Vec::new(); + for wpt in file.wpt.iter().flat_map(|chunk| &chunk.wpt) { + coordinates.extend([wpt.coordinates.lng, wpt.coordinates.lat]); + } self.waypoints .insert(file.id, (file.wpt_rev_id, coordinates)); } @@ -41,10 +39,10 @@ impl CoordinatesCache { .get(&seg.id) .is_none_or(|(r, _)| *r != seg.rev_id) { - let coordinates = seg - .iter() - .flat_map(|trkpt| [trkpt.coordinates.lng, trkpt.coordinates.lat]) - .collect(); + let mut coordinates = Vec::with_capacity(seg.len() * 2); + for trkpt in seg.iter() { + coordinates.extend([trkpt.coordinates.lng, trkpt.coordinates.lat]); + } self.segments.insert(seg.id, (seg.rev_id, coordinates)); } } diff --git a/gpx-rs/engine/src/engine/derived/file_structure_cache.rs b/gpx-rs/engine/src/engine/derived/file_structure_cache.rs index 20a3be0f4..0ab33fbb9 100644 --- a/gpx-rs/engine/src/engine/derived/file_structure_cache.rs +++ b/gpx-rs/engine/src/engine/derived/file_structure_cache.rs @@ -11,7 +11,7 @@ pub struct FileStructureCache { impl FileStructureCache { /// Brings the cache in line with `files`, given what changed since the last update. - pub fn update(&mut self, files: Option<&Files>, diff: &Option) { + pub fn update(&mut self, files: Option<&Files>, diff: Option<&Diff>) { if let Some(diff) = diff { for id in diff.removed.iter() { self.map.remove(id); @@ -48,7 +48,7 @@ mod tests { let mut cache = FileStructureCache::default(); cache.update( Some(&fx.files), - &Some(Diff { + Some(&Diff { added: vec![a, b], ..Default::default() }), @@ -60,7 +60,7 @@ mod tests { fx.files.insert(a, Rc::new(file)); cache.update( Some(&fx.files), - &Some(Diff { + Some(&Diff { modified: vec![a], ..Default::default() }), @@ -71,7 +71,7 @@ mod tests { fx.files.remove(&b); cache.update( Some(&fx.files), - &Some(Diff { + Some(&Diff { removed: vec![b], ..Default::default() }), diff --git a/gpx-rs/engine/src/engine/engine.rs b/gpx-rs/engine/src/engine/engine.rs index 929bafb92..b96c7117a 100644 --- a/gpx-rs/engine/src/engine/engine.rs +++ b/gpx-rs/engine/src/engine/engine.rs @@ -1,7 +1,5 @@ // TODO license in every file -use std::collections::HashSet; - use crate::{ Action, Apply, Command, CoordinatesCache, Diff, FileId, FileOrder, FileStructure, FileStructureCache, Selection, Stack, State, StatisticsBuffer, StatisticsCache, TrackSegmentId, @@ -37,8 +35,8 @@ impl Engine { } /// Which files the last action added, removed or modified. - pub fn last_diff(&self) -> &Option { - &self.diff + pub fn last_diff(&self) -> Option<&Diff> { + self.diff.as_ref() } /// Whether the last action changed the order of the files. @@ -143,7 +141,7 @@ impl Engine { } self.statistics_cache.update(current); self.coordinates_cache.update(current); - self.structure_cache.update(current, &self.diff); + self.structure_cache.update(current, self.diff.as_ref()); self.statistics_buffer .update(&self.statistics_cache.get(current, &self.selection)); } @@ -343,18 +341,14 @@ mod tests { assert!(engine.order().is_empty()); load(&mut engine, "data/simple.gpx"); let loaded_id = engine.order()[0]; - let diff = engine.last_diff(); - assert!(diff.is_some()); - let diff = diff.as_ref().unwrap(); + let diff = engine.last_diff().unwrap(); assert_eq!(diff.added, vec![loaded_id]); assert!(engine.order_changed()); new(&mut engine, "empty"); let empty_id = engine.order()[1]; // only the new file is reported, the other one is not recomputed - let diff = engine.last_diff(); - assert!(diff.is_some()); - let diff = diff.as_ref().unwrap(); + let diff = engine.last_diff().unwrap(); assert_eq!(diff.added, vec![empty_id]); assert!(diff.modified.is_empty() && diff.removed.is_empty()); assert_eq!(engine.file_structure(&empty_id).unwrap().name, "empty"); @@ -384,9 +378,7 @@ mod tests { // undo removes the empty file, redoing the load removes the other one too assert!(engine.execute(Action::Undo)); - let diff = engine.last_diff(); - assert!(diff.is_some()); - let diff = diff.as_ref().unwrap(); + let diff = engine.last_diff().unwrap(); assert_eq!(diff.removed, vec![empty_id]); assert!(engine.order_changed()); assert!(engine.file_structure(&empty_id).is_none()); diff --git a/gpx-rs/engine/src/engine/state/stack.rs b/gpx-rs/engine/src/engine/state/stack.rs index 1e2e3a8e4..e14cce69e 100644 --- a/gpx-rs/engine/src/engine/state/stack.rs +++ b/gpx-rs/engine/src/engine/state/stack.rs @@ -67,18 +67,12 @@ impl Stack { } fn get(&self, index: usize) -> Option<&StackEntry> { - if index < self.entries.len() { - Some(&self.entries[index]) - } else { - None - } + self.entries.get(index) } fn push(&mut self, entry: StackEntry) { if let Some(i) = self.index { - if i + 1 < self.entries.len() { - self.entries.truncate(i + 1); - } + self.entries.truncate(i + 1); } self.entries.push(entry); @@ -103,7 +97,7 @@ impl Stack { for id in prev_ids.intersection(&cur_ids) { let before = prev.map_or_default(|e| e.get(id)); let after = cur.map_or_default(|e| e.get(id)); - if before != after { + if !before.zip(after).is_some_and(|(b, a)| Rc::ptr_eq(b, a)) && before != after { modified.push(*id); } } diff --git a/gpx-rs/wasm/src/lib.rs b/gpx-rs/wasm/src/lib.rs index 4dbbb689a..675183bd9 100644 --- a/gpx-rs/wasm/src/lib.rs +++ b/gpx-rs/wasm/src/lib.rs @@ -49,13 +49,15 @@ impl From for engine::CleanType { } thread_local! { - static ENGINE: RefCell> = RefCell::new(None); + static ENGINE: RefCell> = const { RefCell::new(None) }; } fn execute(action: Action) -> bool { - ENGINE.with(|engine| match engine.borrow_mut().as_mut() { - Some(engine) => engine.execute(action), - None => false, + ENGINE.with(|engine| { + engine + .borrow_mut() + .as_mut() + .is_some_and(|engine| engine.execute(action)) }) } @@ -65,13 +67,13 @@ fn edit(command: Command) -> bool { /// Decodes concatenated 16-byte UUIDs. fn file_ids(bytes: &[u8]) -> Option> { - if bytes.len() % 16 != 0 { - return None; - } - bytes - .chunks_exact(16) - .map(|chunk| uuid::Uuid::from_slice(chunk).ok().map(FileId)) - .collect() + let (chunks, rest) = bytes.as_chunks::<16>(); + rest.is_empty().then(|| { + chunks + .iter() + .map(|c| FileId(uuid::Uuid::from_bytes(*c))) + .collect() + }) } fn same_len(a: &[f64], b: &[f64], c: &[f64]) -> bool { @@ -302,19 +304,13 @@ pub fn redo() -> bool { /// `file_ids_bytes`: concatenated 16-byte UUIDs. #[wasm_bindgen] pub fn select(file_ids_bytes: &[u8]) -> bool { - match file_ids(file_ids_bytes) { - Some(file_ids) => execute(Action::Select { file_ids }), - None => false, - } + file_ids(file_ids_bytes).is_some_and(|file_ids| execute(Action::Select { file_ids })) } /// `file_ids_bytes`: concatenated 16-byte UUIDs. #[wasm_bindgen] pub fn add_select(file_ids_bytes: &[u8]) -> bool { - match file_ids(file_ids_bytes) { - Some(file_ids) => execute(Action::AddSelect { file_ids }), - None => false, - } + file_ids(file_ids_bytes).is_some_and(|file_ids| execute(Action::AddSelect { file_ids })) } #[wasm_bindgen] @@ -513,7 +509,7 @@ pub fn selection() -> Selection { pub fn last_update() -> FilesUpdate { let update = Object::new(); with_engine(|e| { - let diff = e.last_diff().clone().unwrap_or_default(); + let diff = e.last_diff().cloned().unwrap_or_default(); set(&update, "orderChanged", e.order_changed()); set(&update, "selectionChanged", e.selection_changed()); set(&update, "added", ids(&diff.added));