Files
gpx.studio/gpx-rs/src/engine/derived/routing_buffer.rs
T

162 lines
6.2 KiB
Rust
Raw Normal View History

2026-10-14 08:17:51 +02:00
use crate::{FileId, Files, Selection, TrackSegmentId, TrackSegmentRevisionId};
2026-10-07 17:58:17 +02:00
/// What the routing tool needs about the selected segments (see
/// [`Selection::segment_locations`]): where their anchors are among their trackpoints.
///
/// Trackpoints are numbered like in the [`crate::StatisticsBuffer`], over all the selected
/// segments one after the other, except that the segments that are not covered by the routing
/// tool (waypoints) are left out.
#[derive(Debug, Default)]
pub struct RoutingBuffer {
/// The index of each anchor in the selection, in order. The first and last trackpoints of a
/// segment always are anchors.
pub anchor_indices: Vec<u32>,
/// The lowest map zoom level at which each anchor is shown.
pub anchor_zooms: Vec<u8>,
/// Index in the selection of the first trackpoint of each selected segment that has some,
/// which tells which segment an anchor or a trackpoint belongs to.
pub segment_starts: Vec<u32>,
/// Id of each selected segment that has trackpoints, as `segment_starts`.
pub segment_ids: Vec<TrackSegmentId>,
/// Changes when the selected segments, or their trackpoints, change. Commands that refer to
/// trackpoints by their index in the selection carry it, to be refused if the selection is
/// not the one the indices were taken from anymore.
pub revision: u32,
/// What the revision stands for.
signature: Vec<(TrackSegmentId, TrackSegmentRevisionId)>,
}
impl RoutingBuffer {
2026-10-14 08:17:51 +02:00
pub fn update(&mut self, files: Option<&Files>, selection: &Selection, order: &[FileId]) {
2026-10-07 17:58:17 +02:00
self.anchor_indices.clear();
self.anchor_zooms.clear();
self.segment_starts.clear();
self.segment_ids.clear();
let mut signature = vec![];
let mut index = 0;
for location in files
.map(|files| selection.segment_locations(files, order))
.unwrap_or_default()
{
let segment = &files.unwrap()[&location.file_id].trk[location.trk].trkseg[location.seg];
signature.push((segment.id, segment.rev_id));
if segment.is_empty() {
continue;
}
self.segment_starts.push(index as u32);
self.segment_ids.push(segment.id);
for trkpt in segment.iter() {
if let Some(zoom) = trkpt.anchor {
self.anchor_indices.push(index as u32);
self.anchor_zooms.push(zoom);
}
index += 1;
}
}
if signature != self.signature {
self.revision = self.revision.wrapping_add(1);
self.signature = signature;
}
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashSet, rc::Rc};
2026-10-07 18:22:30 +02:00
use crate::parse;
2026-10-07 17:58:17 +02:00
use super::*;
2026-10-14 08:17:51 +02:00
fn loaded() -> (Files, FileId) {
2026-10-07 17:58:17 +02:00
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
let file = parse(&data, &mut Default::default()).unwrap();
let id = file.id;
2026-10-14 08:17:51 +02:00
let mut files = Files::default();
2026-10-07 17:58:17 +02:00
files.insert(id, Rc::new(file));
(files, id)
}
fn file_selection(id: FileId) -> Selection {
Selection::File {
file_ids: HashSet::from([id]),
}
}
#[test]
fn test_anchors_and_starts_follow_the_selected_segments() {
let (files, id) = loaded();
let lengths: Vec<usize> = files[&id]
.trk
.iter()
2026-10-07 18:22:30 +02:00
.flat_map(|trk| trk.trkseg.iter().map(|segment| segment.len()))
2026-10-07 17:58:17 +02:00
.collect();
let mut buffer = RoutingBuffer::default();
buffer.update(Some(&files), &file_selection(id), &[id]);
// an empty segment has no start
assert_eq!(buffer.segment_ids.len(), buffer.segment_starts.len());
let mut start = 0;
let expected: Vec<u32> = lengths
.iter()
.filter(|len| **len > 0)
.map(|len| {
start += len;
(start - len) as u32
})
.collect();
assert_eq!(buffer.segment_starts, expected);
// the ends of the segments are always anchors, shown at every zoom level
assert_eq!(buffer.anchor_indices.len(), buffer.anchor_zooms.len());
assert!(buffer.anchor_indices.windows(2).all(|w| w[0] < w[1]));
let total: usize = lengths.iter().sum();
let ends = expected
.iter()
.copied()
.chain(expected.iter().skip(1).map(|start| start - 1))
.chain([total as u32 - 1]);
for end in ends {
let position = buffer.anchor_indices.iter().position(|i| *i == end);
assert_eq!(position.map(|p| buffer.anchor_zooms[p]), Some(0), "{end}");
}
}
#[test]
fn test_nothing_without_segments() {
let (files, id) = loaded();
let mut buffer = RoutingBuffer::default();
buffer.update(Some(&files), &Selection::Empty, &[id]);
assert!(buffer.anchor_indices.is_empty() && buffer.segment_starts.is_empty());
// waypoints are not covered by the routing tool
buffer.update(Some(&files), &Selection::Waypoints { file_id: id }, &[id]);
assert!(buffer.anchor_indices.is_empty() && buffer.segment_starts.is_empty());
buffer.update(None, &file_selection(id), &[id]);
assert!(buffer.anchor_indices.is_empty() && buffer.segment_starts.is_empty());
}
#[test]
fn test_revision_changes_with_the_selected_segments() {
let (mut files, id) = loaded();
let mut buffer = RoutingBuffer::default();
buffer.update(Some(&files), &file_selection(id), &[id]);
let first = buffer.revision;
// the same segments: the indices still mean the same trackpoints
buffer.update(Some(&files), &file_selection(id), &[id]);
assert_eq!(buffer.revision, first);
// other trackpoints in the same segment
let mut file = (*files[&id]).clone();
file.trk[0].trkseg[0].rev_id = Default::default();
files.insert(id, Rc::new(file));
buffer.update(Some(&files), &file_selection(id), &[id]);
assert_ne!(buffer.revision, first);
let second = buffer.revision;
// another selection
buffer.update(Some(&files), &Selection::Empty, &[id]);
assert_ne!(buffer.revision, second);
}
}