use std::rc::Rc; use crate::{ Apply, Categories, CommandError, FileId, SegmentLocation, Selection, State, Statistics, Track, TrackSegment, Trackpoint, distance, replace_trackpoints, }; /// The values of an OSM attribute (surface, highway...) of the new trackpoints of a [`Route`], as /// the intervals of consecutive trackpoints that share one: `starts[i]` is the index of the first /// trackpoint of the interval `i`, and `values[i]` its value, 0 when unknown, else 1 + the index /// of its name in `names`. No intervals means that the attribute is unknown everywhere. #[derive(Debug, Default, Clone, Copy)] pub struct RouteCategory<'a> { pub starts: &'a [u32], pub values: &'a [u8], pub names: &'a [String], } impl RouteCategory<'_> { /// The code in `categories` of the value of each of the `len` new trackpoints. A name is /// added to the table of the categories if it is new. fn codes( &self, categories: &mut Categories, len: usize, ) -> Result>, CommandError> { if self.starts.len() != self.values.len() { return Err(invalid("intervals of different lengths")); } let mut codes = vec![None; len]; for (i, (start, value)) in self.starts.iter().zip(self.values).enumerate() { let end = self.starts.get(i + 1).map_or(len, |end| *end as usize); let start = *start as usize; if start > end || end > len { return Err(invalid("intervals out of the trackpoints")); } if *value == 0 { continue; } let name = self .names .get(usize::from(*value) - 1) .ok_or_else(|| invalid("unknown name of category"))?; codes[start..end].fill(categories.code(name)); } Ok(codes) } } /// Replaces the trackpoints `start..end` of a segment of the selection by the given ones (a pure /// insertion when `start == end`, a pure removal when there are no new points). It is how the /// routing tool reroutes a part of a segment, extends it, or removes the end of it. /// /// `start` and `end` are indices in the trackpoints of the selection (see `RoutingBuffer`): the /// range has to be in a single segment, and `start == end == the number of trackpoints` appends /// to the last one. If the selection has no trackpoints at all, they go to its last segment, which /// is created if needed (in the last track, or in a new one, of the last selected file). /// /// The timestamps follow: the new points get some if the segment has, at the speed it had on the /// replaced part (see [`replace_trackpoints`]). /// /// The new points listed in `anchors` become anchors of the routing tool, shown at every zoom /// level. The anchors of the segment out of the range are kept, and the ends of the segment are /// anchors anyway. The ones right before and after the range, between which the route goes, become /// shown at every zoom level as well. #[derive(Debug)] pub struct Route<'a> { pub start: u32, pub end: u32, pub lng: &'a [f64], pub lat: &'a [f64], pub ele: &'a [f64], pub surface: RouteCategory<'a>, pub highway: RouteCategory<'a>, pub sac_scale: RouteCategory<'a>, pub mtb_scale: RouteCategory<'a>, /// Indices among the new trackpoints. pub anchors: &'a [u32], } fn invalid(message: &str) -> CommandError { CommandError::InvalidData(message.into()) } /// A segment of the selection, and the range of its trackpoints that is concerned. pub(super) struct Target { pub location: SegmentLocation, pub start: usize, pub end: usize, } impl Apply for Route<'_> { fn apply(self, state: &mut State) -> Result<(), CommandError> { let (start, end) = (self.start as usize, self.end as usize); let len = self.lng.len(); if start > end || self.lat.len() != len || self.ele.len() != len { return Err(invalid("invalid trackpoints range")); } if self.anchors.iter().any(|anchor| *anchor as usize >= len) { return Err(invalid("anchor out of the new trackpoints")); } if start == end && len == 0 { return Err(CommandError::NothingToDo); } let categories = &mut *state.categories; let surface = self.surface.codes(&mut categories.surface, len)?; let highway = self.highway.codes(&mut categories.highway, len)?; let sac_scale = self.sac_scale.codes(&mut categories.sac_scale, len)?; let mtb_scale = self.mtb_scale.codes(&mut categories.mtb_scale, len)?; let points: Vec = (0..len) .map(|i| Trackpoint { coordinates: crate::LngLat { lng: self.lng[i], lat: self.lat[i], }, ele: self.ele[i], surface: surface[i], highway: highway[i], sac_scale: sac_scale[i], mtb_scale: mtb_scale[i], ..Default::default() }) .collect(); let mut points = points; for anchor in self.anchors { points[*anchor as usize].anchor = Some(0); } let mut file; let target = match find_target(state, start, end)? { Some(target) => { file = (*state.files[&target.location.file_id]).clone(); target } None => { let (file_id, trk) = container(state).ok_or(CommandError::NothingToDo)?; if start != 0 || end != 0 { return Err(invalid("trackpoints range out of bounds")); } file = (*state.files[&file_id]).clone(); // the segment of the last track, which may be new let trk = match trk.or_else(|| file.trk.len().checked_sub(1)) { Some(trk) => trk, None => { file.trk.push(Track::default()); file.trk.len() - 1 } }; if file.trk[trk].trkseg.is_empty() { file.trk[trk].trkseg.push(TrackSegment::default()); } let seg = file.trk[trk].trkseg.len() - 1; Target { location: SegmentLocation { file_id, trk, seg }, start: 0, end: 0, } } }; let segment = &mut file.trk[target.location.trk].trkseg[target.location.seg]; let new_len = points.len(); let (speed, start_time) = speed_and_start_time(segment, &target, &points); let has_times = !segment.is_empty() && segment[0].time.is_some(); if speed.is_some() || has_times { // the timestamps of the whole segment can change let mut all: Vec = segment.iter().cloned().collect(); let old_len = all.len(); replace_trackpoints( &mut all, target.start, target.end, points, speed, start_time, false, ); segment.splice(0, old_len, all); } else { segment.splice(target.start, target.end, points); } // the anchors next to the new points are the ones that were moved or removed around: // they become permanent for index in [target.start.checked_sub(1), Some(target.start + new_len)] .into_iter() .flatten() { if index < segment.len() && segment[index].anchor.is_some_and(|zoom| zoom != 0) { segment.set_anchor(index, 0); } } segment.rev_id = Default::default(); state.files.insert(target.location.file_id, Rc::new(file)); Ok(()) } } /// The segment of the trackpoints `start..end` of the selection. pub(super) fn find_target( state: &State, start: usize, end: usize, ) -> Result, CommandError> { let locations = state .selection .segment_locations(state.files, &state.order.0); let segments: Vec<(SegmentLocation, usize)> = locations .into_iter() .map(|location| { let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg]; (location, segment.len()) }) .collect(); let total: usize = segments.iter().map(|(_, len)| len).sum(); if total == 0 { // an empty segment of the selection, if there is one return match segments.last() { Some((location, _)) if start == 0 && end == 0 => Ok(Some(Target { location: *location, start: 0, end: 0, })), Some(_) => Err(invalid("trackpoints range out of bounds")), None => Ok(None), }; } let mut offset = 0; for (location, len) in segments.iter().filter(|(_, len)| *len > 0) { // appending to the selection is appending to its last segment let appends = offset + len == total && start == total; if start >= offset && (start < offset + len || appends) { if end > offset + len { return Err(invalid("trackpoints range over several segments")); } return Ok(Some(Target { location: *location, start: start - offset, end: end - offset, })); } offset += len; } Err(invalid("trackpoints range out of bounds")) } /// The file, and the track (if the selection is one), where to create a segment in a selection /// that has none. fn container(state: &State) -> Option<(FileId, Option)> { match &*state.selection { Selection::File { file_ids } => { let id = state .order .0 .iter() .rev() .find(|id| file_ids.contains(id)) .or_else(|| file_ids.iter().next())?; state.files.contains_key(id).then_some((*id, None)) } Selection::Track { file_id, trk_ids } => { let trk = state .files .get(file_id)? .trk .iter() .rposition(|trk| trk_ids.contains(&trk.id))?; Some((*file_id, Some(trk))) } _ => None, } } /// The speed at which the new points are reached and the time of the first one, when the segment /// has times to keep consistent: the speed is the one that the segment had on the part that is /// replaced, adjusted so that the moving speed of the whole segment stays what it was. /// /// The replaced part goes from the trackpoint before the range (the anchor that stays) to the one /// after it, or to the ends of the segment: the new points are the route between them. /// /// This is what the routing tool computed from the statistics of the segment before. fn speed_and_start_time( segment: &TrackSegment, target: &Target, points: &[Trackpoint], ) -> (Option, Option) { let time_at = |index: usize| { (index < segment.len()) .then(|| segment[index].time) .flatten() }; let start_time = time_at(target.start); if points.is_empty() || segment.is_empty() { return (None, start_time); } let stats = Statistics::compute(segment); let Some(moving_speed) = stats.global.moving_speed().filter(|speed| *speed > 0.0) else { return (None, start_time); }; // the statistics where the replaced part starts and ends let first_index = target.start.saturating_sub(1); let last_index = target.end.min(segment.len() - 1); let (first, last) = (&stats.local[first_index], &stats.local[last_index]); let moving_distance = |stats: &crate::TrackpointStatistics| stats.moving_distance.unwrap_or(0.0); let moving_time = |stats: &crate::TrackpointStatistics| stats.moving_time.unwrap_or(0); let total_time = |stats: &crate::TrackpointStatistics| stats.total_time.unwrap_or(0); let replacing_distance: f64 = points .windows(2) .map(|pair| distance(pair[0].coordinates, pair[1].coordinates)) .sum(); let replaced_distance = moving_distance(last) - moving_distance(first); let global_moving_distance = stats.global.moving_distance.unwrap_or(0.0); let new_distance = global_moving_distance + replacing_distance - replaced_distance; // in milliseconds let new_time = new_distance / moving_speed * 3_600_000.0; let remaining_time = stats.global.moving_time.unwrap_or(0) as f64 - (moving_time(last) - moving_time(first)) as f64; let mut replacing_time = new_time - remaining_time; if replacing_time <= 0.0 { // fall back to the time that the replaced part took replacing_time = (total_time(last) - total_time(first)) as f64; } let speed = replacing_distance / (replacing_time / 3_600_000.0); let start_time = start_time.or_else(|| { // The first trackpoint has no time: the new points end when the last replaced one did, // after the time they take and the time that the segment was not moving until then. // (Suspicious: without a time for the last one either, it is "0", the year 1970.) let end_time = time_at(last_index); let stopped = (total_time(last) - moving_time(last)) as f64; Some(end_time.unwrap_or(0) - (replacing_time + stopped) as i64) }); (Some(speed), start_time) } #[cfg(test)] mod tests { use std::collections::HashSet; use crate::{File, Load, TrackSegmentId, engine::command::fixture::Fixture}; use super::*; fn load(fx: &mut Fixture, path: &str) -> FileId { let data = std::fs::read(path).unwrap(); Load { data: &data, name: "file", } .apply(&mut fx.state()) .unwrap(); *fx.order.0.last().unwrap() } fn select_file(fx: &mut Fixture, id: FileId) { fx.selection = Selection::File { file_ids: HashSet::from([id]), }; } /// The first segment of the file with segments, selected. fn loaded() -> (Fixture, FileId) { let mut fx = Fixture::default(); let id = load(&mut fx, "data/with_tracks_and_segments.gpx"); let trk = &fx.files[&id].trk[0]; fx.selection = Selection::TrackSegment { file_id: id, trk_id: trk.id, trkseg_ids: HashSet::from([trk.trkseg[0].id]), }; (fx, id) } fn route<'a>( start: u32, end: u32, lng: &'a [f64], lat: &'a [f64], ele: &'a [f64], ) -> Route<'a> { Route { start, end, lng, lat, ele, surface: Default::default(), highway: Default::default(), sac_scale: Default::default(), mtb_scale: Default::default(), anchors: &[], } } fn segment(fx: &Fixture, id: FileId, trk: usize, seg: usize) -> &TrackSegment { &fx.files[&id].trk[trk].trkseg[seg] } #[test] fn test_route_replaces_points_of_the_selected_segment_only() { let (mut fx, id) = loaded(); let before = fx.files[&id].clone(); let len = before.trk[0].trkseg[0].len(); assert!(len >= 3); route(1, 3, &[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0], &[7.0, 8.0, 9.0]) .apply(&mut fx.state()) .unwrap(); let after = &fx.files[&id]; let (old, new) = (&before.trk[0].trkseg[0], &after.trk[0].trkseg[0]); assert_eq!(new.len(), len + 1); assert_eq!(new[0].coordinates.lng, old[0].coordinates.lng); assert_eq!(new[1].coordinates.lng, 1.0); assert_eq!(new[3].coordinates.lat, 6.0); assert_eq!(new[3].ele, 9.0); assert_eq!(new[4].coordinates.lng, old[3].coordinates.lng); assert_ne!(new.rev_id, old.rev_id); assert_eq!(new.id, old.id); // other segments and tracks keep their revision for (b, a) in before.trk.iter().zip(&after.trk) { for (b, a) in b.trkseg.iter().zip(&a.trkseg) { if a.id != new.id { assert_eq!(a.rev_id, b.rev_id); } } } } #[test] fn test_route_append_and_remove() { let (mut fx, id) = loaded(); let len = fx.files[&id].trk[0].trkseg[0].len() as u32; route(len, len, &[1.0], &[2.0], &[3.0]) .apply(&mut fx.state()) .unwrap(); let seg = segment(&fx, id, 0, 0); assert_eq!(seg.len(), len as usize + 1); assert_eq!(seg[len as usize].ele, 3.0); route(0, len + 1, &[], &[], &[]) .apply(&mut fx.state()) .unwrap(); assert_eq!(segment(&fx, id, 0, 0).len(), 0); } #[test] fn test_route_invalid_arguments_change_nothing() { let (mut fx, id) = loaded(); let before = fx.files[&id].clone(); let len = before.trk[0].trkseg[0].len() as u32; let results = [ route(2, 1, &[], &[], &[]).apply(&mut fx.state()), route(0, 0, &[1.0], &[], &[]).apply(&mut fx.state()), route(0, len + 1, &[], &[], &[]).apply(&mut fx.state()), route(len + 1, len + 1, &[1.0], &[1.0], &[1.0]).apply(&mut fx.state()), Route { anchors: &[1], ..route(0, 0, &[1.0], &[1.0], &[1.0]) } .apply(&mut fx.state()), ]; assert!( results .iter() .all(|r| matches!(r, Err(CommandError::InvalidData(_)))), "{results:?}" ); assert_eq!( route(0, 0, &[], &[], &[]).apply(&mut fx.state()), Err(CommandError::NothingToDo) ); assert!(Rc::ptr_eq(&fx.files[&id], &before)); } #[test] fn test_route_needs_a_selection_with_a_file() { let (mut fx, _) = loaded(); fx.selection = Selection::Empty; assert_eq!( route(0, 0, &[1.0], &[1.0], &[1.0]).apply(&mut fx.state()), Err(CommandError::NothingToDo) ); } #[test] fn test_route_indices_are_in_the_selection() { let (mut fx, id) = loaded(); let file = fx.files[&id].clone(); select_file(&mut fx, id); let lens: Vec<_> = file .trk .iter() .flat_map(|trk| trk.trkseg.iter().map(|segment| segment.len())) .collect(); assert!(lens.len() >= 2); // inside the second segment let second = lens[0] as u32; route(second + 1, second + 2, &[1.0], &[2.0], &[3.0]) .apply(&mut fx.state()) .unwrap(); let (trk, seg) = (0, 1); let segments_before: Vec<_> = file.trk.iter().flat_map(|t| &t.trkseg).collect(); let files = fx.files[&id].clone(); let segments_after: Vec<_> = files.trk.iter().flat_map(|t| &t.trkseg).collect(); for (i, (b, a)) in segments_before.iter().zip(&segments_after).enumerate() { assert_eq!(a.rev_id != b.rev_id, i == 1, "segment {i}"); } assert_eq!(segment(&fx, id, trk, seg)[1].coordinates.lng, 1.0); // a range over two segments let total: usize = lens.iter().sum(); assert!(matches!( route(second - 1, second + 1, &[], &[], &[]).apply(&mut fx.state()), Err(CommandError::InvalidData(_)) )); // after the last point: the last segment let before = segments_after.last().unwrap().len(); let total = total as u32; route(total, total, &[1.0], &[2.0], &[3.0]) .apply(&mut fx.state()) .unwrap(); let last = fx.files[&id].trk.last().unwrap().trkseg.last().unwrap(); assert_eq!(last.len(), before + 1); } #[test] fn test_route_creates_the_segment_of_a_selection_that_has_none() { let mut fx = Fixture::default(); let file = File::default(); let id = file.id; fx.files.insert(id, Rc::new(file)); fx.order.0.push(id); select_file(&mut fx, id); route(0, 0, &[1.0, 2.0], &[1.0, 2.0], &[3.0, 4.0]) .apply(&mut fx.state()) .unwrap(); assert_eq!(fx.files[&id].trk.len(), 1); assert_eq!(segment(&fx, id, 0, 0).len(), 2); // the ends are anchors assert_eq!(segment(&fx, id, 0, 0)[0].anchor, Some(0)); assert_eq!(segment(&fx, id, 0, 0)[1].anchor, Some(0)); // a track with no segments let mut file = (*fx.files[&id]).clone(); file.trk[0].trkseg.clear(); let trk_id = file.trk[0].id; fx.files.insert(id, Rc::new(file)); fx.selection = Selection::Track { file_id: id, trk_ids: HashSet::from([trk_id]), }; route(0, 0, &[5.0], &[5.0], &[5.0]) .apply(&mut fx.state()) .unwrap(); assert_eq!(fx.files[&id].trk.len(), 1); assert_eq!(segment(&fx, id, 0, 0).len(), 1); // an empty segment is the target as well let mut file = (*fx.files[&id]).clone(); file.trk[0].trkseg[0] = TrackSegment::default(); let seg_id: TrackSegmentId = file.trk[0].trkseg[0].id; fx.files.insert(id, Rc::new(file)); fx.selection = Selection::TrackSegment { file_id: id, trk_id, trkseg_ids: HashSet::from([seg_id]), }; route(0, 0, &[6.0], &[6.0], &[6.0]) .apply(&mut fx.state()) .unwrap(); assert_eq!(segment(&fx, id, 0, 0).len(), 1); assert_eq!(fx.files[&id].trk[0].trkseg.len(), 1); } #[test] fn test_route_anchors() { let (mut fx, id) = loaded(); let len = segment(&fx, id, 0, 0).len(); assert!(len > 4); let anchors_before: Vec<_> = segment(&fx, id, 0, 0).iter().map(|p| p.anchor).collect(); // 5 new points replacing the 2 in the middle, two of them are anchors let (lng, lat, ele) = ([1.0; 5], [2.0; 5], [3.0; 5]); Route { anchors: &[1, 3], ..route(1, 3, &lng, &lat, &ele) } .apply(&mut fx.state()) .unwrap(); let anchors: Vec<_> = segment(&fx, id, 0, 0).iter().map(|p| p.anchor).collect(); assert_eq!(anchors.len(), len + 3); assert_eq!(anchors[2], Some(0)); assert_eq!(anchors[4], Some(0)); assert_eq!(anchors[1], None); assert_eq!(anchors[3], None); assert_eq!(anchors[5], None); // the others stay as they were, the ends are always anchors assert_eq!(anchors[0], Some(0)); assert_eq!(anchors[len + 2], Some(0)); assert_eq!(anchors[len + 2 - 1], anchors_before[len - 2]); } #[test] fn test_route_makes_the_anchors_around_the_range_permanent() { let (mut fx, id) = loaded(); let mut file = (*fx.files[&id]).clone(); let len = file.trk[0].trkseg[0].len(); assert!(len > 8); // anchors shown from different zoom levels, around and away from the range 4..6 for (index, zoom) in [(1, 7), (3, 9), (6, 12), (8, 14)] { file.trk[0].trkseg[0].set_anchor(index, zoom); } fx.files.insert(id, Rc::new(file)); route(4, 6, &[1.0, 2.0], &[3.0, 4.0], &[5.0, 6.0]) .apply(&mut fx.state()) .unwrap(); let anchors: Vec<_> = segment(&fx, id, 0, 0).iter().map(|p| p.anchor).collect(); assert_eq!(anchors.len(), len); // right before and after the new points: permanent assert_eq!(anchors[3], Some(0)); assert_eq!(anchors[6], Some(0)); // the others are as they were assert_eq!(anchors[1], Some(7)); assert_eq!(anchors[8], Some(14)); } #[test] fn test_route_categories() { let (mut fx, id) = loaded(); let names = ["asphalt".to_string(), "gravel".to_string()]; let (lng, lat, ele) = ([1.0; 4], [2.0; 4], [3.0; 4]); // asphalt, asphalt, unknown, gravel Route { surface: RouteCategory { starts: &[0, 2, 3], values: &[1, 0, 2], names: &names, }, highway: RouteCategory { starts: &[0], values: &[1], names: &["track".to_string()], }, ..route(0, 0, &lng, &lat, &ele) } .apply(&mut fx.state()) .unwrap(); let file = fx.files[&id].clone(); let seg = &file.trk[0].trkseg[0]; let asphalt = fx.categories.surface.code("asphalt"); let gravel = fx.categories.surface.code("gravel"); let surfaces: Vec<_> = (0..4).map(|i| seg[i].surface).collect(); assert_eq!(surfaces, [asphalt, asphalt, None, gravel]); let track = fx.categories.highway.code("track"); assert!((0..4).all(|i| seg[i].highway == track)); assert!((0..4).all(|i| seg[i].sac_scale.is_none() && seg[i].mtb_scale.is_none())); // broken intervals or names let broken = [ RouteCategory { starts: &[0, 1], values: &[1], names: &names, }, RouteCategory { starts: &[5], values: &[1], names: &names, }, RouteCategory { starts: &[0], values: &[3], names: &names, }, ]; for surface in broken { let result = Route { surface, ..route(0, 0, &lng, &lat, &ele) } .apply(&mut fx.state()); assert!(matches!(result, Err(CommandError::InvalidData(_)))); } } /// A segment of `data/with_time.gpx`, selected: 80 trackpoints at 20 km/h. fn timed() -> (Fixture, FileId) { let mut fx = Fixture::default(); let id = load(&mut fx, "data/with_time.gpx"); select_file(&mut fx, id); (fx, id) } fn times(seg: &TrackSegment) -> Vec { seg.iter().map(|p| p.time.unwrap()).collect() } #[test] fn test_route_gives_times_to_the_new_points() { let (mut fx, id) = timed(); let seg = segment(&fx, id, 0, 0); let (n, before) = (seg.len(), times(seg)); let duration = before[n - 1] - before[0]; // replace the points 20..40 by the straight line between them, with 5 points let (a, b) = (seg[20].coordinates, seg[39].coordinates); let at = |t: f64| (a.lng + t * (b.lng - a.lng), a.lat + t * (b.lat - a.lat)); let positions: Vec<_> = (0..5).map(|i| at(i as f64 / 4.0)).collect(); let lng: Vec<_> = positions.iter().map(|p| p.0).collect(); let lat: Vec<_> = positions.iter().map(|p| p.1).collect(); route(20, 40, &lng, &lat, &[0.0; 5]) .apply(&mut fx.state()) .unwrap(); let seg = segment(&fx, id, 0, 0); assert_eq!(seg.len(), n - 15); let after = times(seg); assert!(after.windows(2).all(|w| w[0] < w[1]), "{after:?}"); // the points before are untouched assert_eq!(after[..20], before[..20]); // the speed of the replaced part is the one of the segment: the duration of the segment // is about the same as on the same distance let stats = Statistics::compute(seg); let speed = stats.global.moving_speed().unwrap(); assert!((speed - 20.0).abs() < 1.0, "{speed}"); assert!(after[after.len() - 1] - after[0] <= duration); } #[test] fn test_route_between_anchors_that_stay_keeps_them_untouched() { let (mut fx, id) = timed(); let seg = segment(&fx, id, 0, 0); let (n, before) = (seg.len(), times(seg)); let (a, b) = (seg[20].clone(), seg[39].clone()); // 5 points on the line between the anchors 20 and 39, which are not replaced let at = |t: f64| { ( a.coordinates.lng + t * (b.coordinates.lng - a.coordinates.lng), a.coordinates.lat + t * (b.coordinates.lat - a.coordinates.lat), ) }; let positions: Vec<_> = (1..=5).map(|i| at(i as f64 / 6.0)).collect(); let lng: Vec<_> = positions.iter().map(|p| p.0).collect(); let lat: Vec<_> = positions.iter().map(|p| p.1).collect(); route(21, 39, &lng, &lat, &[0.0; 5]) .apply(&mut fx.state()) .unwrap(); let seg = segment(&fx, id, 0, 0); assert_eq!(seg.len(), n - 18 + 5); let after = times(seg); assert!(after.windows(2).all(|w| w[0] < w[1]), "{after:?}"); // the anchors keep their coordinates and times, and what is before them assert_eq!(after[..=20], before[..=20]); assert_eq!(seg[26].coordinates.lng, b.coordinates.lng); assert_eq!(after[26], before[39]); // and what comes after is unchanged as well assert_eq!(after[27..], before[40..]); let speed = Statistics::compute(seg).global.moving_speed().unwrap(); assert!((speed - 20.0).abs() < 1.0, "{speed}"); } #[test] fn test_route_appending_to_a_segment_with_times() { let (mut fx, id) = timed(); let seg = segment(&fx, id, 0, 0); let n = seg.len(); let last = seg[n - 1].clone(); // replace the last point and go 1 km further east let lng = [last.coordinates.lng, last.coordinates.lng + 0.009]; let lat = [last.coordinates.lat; 2]; route(n as u32 - 1, n as u32, &lng, &lat, &[0.0; 2]) .apply(&mut fx.state()) .unwrap(); let seg = segment(&fx, id, 0, 0); assert_eq!(seg.len(), n + 1); // the replaced point is reached at the speed of the segment: about at its time assert!((seg[n - 1].time.unwrap() - last.time.unwrap()).abs() < 1000); let (a, b) = (seg[n - 1].time.unwrap(), seg[n].time.unwrap()); // the new point is reached at about 20 km/h let km = distance(seg[n - 1].coordinates, seg[n].coordinates); let seconds = (b - a) as f64 / 1000.0; let expected = 3600.0 * km / 20.0; assert!( (seconds - expected).abs() < 0.1 * expected, "{seconds} {expected}" ); } #[test] fn test_route_does_not_invent_times() { let mut fx = Fixture::default(); let id = load(&mut fx, "data/simple.gpx"); select_file(&mut fx, id); assert!(segment(&fx, id, 0, 0).iter().all(|p| p.time.is_none())); route(0, 2, &[1.0, 2.0], &[3.0, 4.0], &[5.0, 6.0]) .apply(&mut fx.state()) .unwrap(); assert!(segment(&fx, id, 0, 0).iter().all(|p| p.time.is_none())); } }