This commit is contained in:
vcoppe committed 2026-10-08 18:34:19 +02:00
1 parent 606a0e974a
commit 67a4c090bd
107 files changed
+3474 -3948

No files matched your search

+2 -2
View File
@@ -3,7 +3,7 @@
///
/// The codes are the positions of the names in the order of first appearance. The table only
/// grows: a code never changes meaning, so it stays valid in every state of the history.
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Categories(Vec<String>);
impl Categories {
@@ -36,7 +36,7 @@ impl Categories {
/// The categories of the data of the trackpoints. There is one table per engine, shared by all
/// its files.
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct TrackpointCategories {
pub surface: Categories,
pub highway: Categories,
+8 -32
View File
@@ -5,8 +5,8 @@ use crate::{Trackpoint, Waypoint};
/// A run of items, shared between the successive versions of a [`crate::Chunked`] list: a chunk
/// that is not touched by an edit is not copied.
///
/// Chunks are equal when they have the same identity, which is cheap to compare and changes
/// whenever the content does (a modified chunk is a new one).
/// A chunk has an identity, which changes whenever the content does (a modified chunk is a new
/// one): what is derived from a chunk, or kept of it, is up to date as long as the identity is.
pub trait Chunk {
type Item: Clone;
@@ -27,8 +27,8 @@ pub trait Chunk {
const MAX_TRKPT_CHUNK_SIZE: usize = 4096;
#[derive(Debug, PartialEq, Eq)]
pub struct TrackpointChunkId(Uuid);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct TrackpointChunkId(pub Uuid);
impl Default for TrackpointChunkId {
fn default() -> Self {
@@ -42,12 +42,6 @@ pub struct TrackpointChunk {
pub trkpt: Vec<Trackpoint>,
}
impl PartialEq for TrackpointChunk {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Chunk for TrackpointChunk {
type Item = Trackpoint;
const MAX_SIZE: usize = MAX_TRKPT_CHUNK_SIZE;
@@ -68,8 +62,8 @@ impl Chunk for TrackpointChunk {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WaypointChunkId(Uuid);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct WaypointChunkId(pub Uuid);
impl Default for WaypointChunkId {
fn default() -> Self {
@@ -85,12 +79,6 @@ pub struct WaypointChunk {
pub wpt: Vec<Waypoint>,
}
impl PartialEq for WaypointChunk {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Chunk for WaypointChunk {
type Item = Waypoint;
const MAX_SIZE: usize = MAX_WPT_CHUNK_SIZE;
@@ -141,20 +129,8 @@ mod tests {
fn test_new_chunks_have_their_own_identity() {
let a = TrackpointChunk::new(vec![Trackpoint::default()]);
let b = TrackpointChunk::new(vec![Trackpoint::default()]);
assert_ne!(a, b);
assert_ne!(a.id, b.id);
assert_eq!(a.items().len(), 1);
assert_ne!(WaypointChunk::new(vec![]), WaypointChunk::new(vec![]));
}
#[test]
fn test_chunk_equality_is_by_id() {
let a = TrackpointChunk::default();
let b = TrackpointChunk::default();
assert_ne!(a, b);
let c = TrackpointChunk {
id: TrackpointChunkId(a.id.0),
trkpt: vec![Trackpoint::default()],
};
assert_eq!(a, c);
assert_ne!(WaypointChunk::new(vec![]).id, WaypointChunk::new(vec![]).id);
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ use crate::Chunk;
/// copies the chunks around the change, so that a version costs little more than the change.
///
/// There are no empty chunks.
#[derive(Debug, PartialEq)]
#[derive(Debug)]
pub struct Chunked<C: Chunk> {
chunks: Vec<Rc<C>>,
cumul_length: Vec<usize>,
+2 -2
View File
@@ -1,10 +1,10 @@
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Link {
pub href: String,
pub text: Option<String>,
}
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct LngLat {
pub lng: f64,
pub lat: f64,
+4 -5
View File
@@ -2,7 +2,7 @@ use uuid::Uuid;
use crate::{Link, Track, Waypoints};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct FileId(pub Uuid);
impl Default for FileId {
@@ -11,16 +11,15 @@ impl Default for FileId {
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone)]
pub struct File {
pub id: FileId,
pub info: FileInfo,
pub trk: Vec<Track>,
pub wpt: Waypoints,
// TODO routes
}
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FileInfo {
pub name: String,
pub desc: Option<String>,
@@ -29,7 +28,7 @@ pub struct FileInfo {
pub time: Option<i64>,
}
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Author {
pub name: Option<String>,
pub email: Option<String>,
+2 -2
View File
@@ -4,7 +4,7 @@ use uuid::Uuid;
use crate::{ChunkIndex, Chunked, ChunkedIter, Trackpoint, TrackpointChunk, compute_anchors};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct TrackSegmentId(pub Uuid);
impl Default for TrackSegmentId {
@@ -22,7 +22,7 @@ impl Default for TrackSegmentRevisionId {
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone)]
pub struct TrackSegment {
pub id: TrackSegmentId,
pub rev_id: TrackSegmentRevisionId,
+3 -3
View File
@@ -2,7 +2,7 @@ use uuid::Uuid;
use crate::{Link, TrackSegment};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct TrackId(pub Uuid);
impl Default for TrackId {
@@ -11,14 +11,14 @@ impl Default for TrackId {
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone)]
pub struct Track {
pub id: TrackId,
pub info: TrackInfo,
pub trkseg: Vec<TrackSegment>,
}
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct TrackInfo {
pub name: Option<String>,
pub cmt: Option<String>,
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::LngLat;
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct Trackpoint {
pub coordinates: LngLat,
pub ele: f64,
+2 -2
View File
@@ -2,7 +2,7 @@ use uuid::Uuid;
use crate::{Link, LngLat};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct WaypointId(pub Uuid);
impl Default for WaypointId {
@@ -11,7 +11,7 @@ impl Default for WaypointId {
}
}
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct Waypoint {
pub id: WaypointId,
pub coordinates: LngLat,
+7 -1
View File
@@ -19,7 +19,7 @@ impl Default for FileWaypointsRevisionId {
/// Everything that changes the waypoints changes the revision too, which is why there is no
/// mutable access to the list itself: the revision tells what is derived from the waypoints
/// (coordinates for the map...) that it is out of date.
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone)]
pub struct Waypoints {
chunks: Chunked<WaypointChunk>,
/// Changes when the waypoints do.
@@ -50,6 +50,12 @@ impl Waypoints {
self.rev_id = Default::default();
}
/// Adds a chunk that is shared with something else after the last waypoint.
pub fn push_shared(&mut self, chunk: std::rc::Rc<WaypointChunk>) {
self.chunks.push_shared(chunk);
self.rev_id = Default::default();
}
/// Replaces the waypoints in `start..end` by `waypoints`, see [`Chunked::splice`].
pub fn splice(&mut self, start: usize, end: usize, waypoints: Vec<Waypoint>) {
self.chunks.splice(start, end, waypoints);
+116 -2
View File
@@ -21,6 +21,7 @@ enum GPXElement {
Track(Track),
Segment(TrackSegment),
Trackpoint(Trackpoint),
RoutePoint(LngLat),
Waypoint(Waypoint),
Elevation,
Time,
@@ -51,6 +52,11 @@ fn parse_coordinates(attributes: Attributes<'_>) -> LngLat {
coordinates
}
/// Whether the element is a point of the detailed path of a route point (`gpxx:rpt`).
fn is_route_point_extension(name: &str) -> bool {
name == "rpt" || name.ends_with(":rpt")
}
/// Whether the element holds a value, which is its text.
fn holds_text(element: Option<&GPXElement>) -> bool {
matches!(
@@ -262,6 +268,10 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
let mut stack: Vec<GPXElement> = vec![];
let mut trkpt_chunk = TrackpointChunk::default();
let mut wpt_chunk = WaypointChunk::default();
// Routes are read as tracks of a single segment, which is what `rte_segment` collects the points of. A route
// point that comes with the detailed path of the route to the next one (`gpxx:rpt`) is replaced by it.
let mut rte_segment = TrackSegment::default();
let mut rpt_points: Vec<Trackpoint> = vec![];
// the text of the element at the top of the stack, up to now
let mut text = String::new();
loop {
@@ -283,11 +293,11 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
stack.push(GPXElement::Link(link));
}
"text" => stack.push(GPXElement::Text),
"trk" => stack.push(GPXElement::Track(Track::default())),
"trk" | "rte" => stack.push(GPXElement::Track(Track::default())),
"trkseg" => {
stack.push(GPXElement::Segment(TrackSegment::default()));
}
"trkpt" => {
"trkpt" | "rtept" => {
stack.push(GPXElement::Trackpoint(Trackpoint {
coordinates: parse_coordinates(e.attributes()),
..Default::default()
@@ -299,6 +309,9 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
..Default::default()
}));
}
name if is_route_point_extension(name) => {
stack.push(GPXElement::RoutePoint(parse_coordinates(e.attributes())));
}
"ele" => stack.push(GPXElement::Elevation),
"time" => stack.push(GPXElement::Time),
e if e.ends_with("atemp") => stack.push(GPXElement::Temperature),
@@ -330,6 +343,25 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
}
}
}
"rtept" => {
if let Some(GPXElement::Track(_)) = stack.last() {
trkpt_chunk.trkpt.push(Trackpoint {
coordinates: parse_coordinates(e.attributes()),
..Default::default()
});
if trkpt_chunk.is_full() {
rte_segment.push(std::mem::take(&mut trkpt_chunk));
}
}
}
name if is_route_point_extension(name) => {
if let Some(GPXElement::Trackpoint(_)) = stack.last() {
rpt_points.push(Trackpoint {
coordinates: parse_coordinates(e.attributes()),
..Default::default()
});
}
}
"wpt" => {
wpt_chunk.wpt.push(Waypoint {
coordinates: parse_coordinates(e.attributes()),
@@ -385,6 +417,42 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
gpx.trk.push(trk);
}
}
"rte" => {
if let Some(GPXElement::Track(mut trk)) = stack.pop() {
rte_segment.push(std::mem::take(&mut trkpt_chunk));
if !rte_segment.is_empty() {
let mut trkseg = std::mem::take(&mut rte_segment);
trkseg.compute_anchors();
trk.trkseg.push(trkseg);
}
gpx.trk.push(trk);
}
}
"rtept" => {
if let Some(GPXElement::Trackpoint(rtept)) = stack.pop()
&& let Some(GPXElement::Track(_)) = stack.last()
{
let points = if rpt_points.is_empty() {
vec![rtept]
} else {
std::mem::take(&mut rpt_points)
};
for point in points {
trkpt_chunk.trkpt.push(point);
if trkpt_chunk.is_full() {
rte_segment.push(std::mem::take(&mut trkpt_chunk));
}
}
}
}
name if is_route_point_extension(name) => {
if let Some(GPXElement::RoutePoint(coordinates)) = stack.pop() {
rpt_points.push(Trackpoint {
coordinates,
..Default::default()
});
}
}
"trkseg" => {
if let Some(GPXElement::Segment(mut trkseg)) = stack.pop()
&& let Some(GPXElement::Track(trk)) = stack.last_mut()
@@ -571,6 +639,52 @@ mod tests {
assert_eq!(trkpt.ele, 115.5);
}
#[test]
fn test_parse_routes_as_tracks() {
let gpx = parse_data("with_routes");
assert_eq!(gpx.trk.len(), 2);
let trk = &gpx.trk[0];
assert_eq!(trk.info.name.as_deref(), Some("route 1"));
assert_eq!(trk.info.type_.as_deref(), Some("Cycling"));
assert_eq!(trk.trkseg.len(), 1);
let trkseg = &trk.trkseg[0];
assert_eq!(trkseg.len(), 49);
let trkpt = &trkseg[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_eq!(trk.info.name.as_deref(), Some("route 2"));
assert_eq!(trk.trkseg[0].len(), 28);
}
#[test]
fn test_parse_route_point_extensions() {
let gpx = parse_data("with_route_extensions");
// a route without points has no segment
assert_eq!(gpx.trk.len(), 2);
assert!(gpx.trk[1].trkseg.is_empty());
let trk = &gpx.trk[0];
assert_eq!(trk.info.name.as_deref(), Some("with extensions"));
assert_eq!(trk.trkseg.len(), 1);
let points: Vec<_> = trk.trkseg[0].iter().collect();
// the detailed path replaces the route point it comes with
let coordinates: Vec<_> = points
.iter()
.map(|p| (p.coordinates.lat, p.coordinates.lng))
.collect();
assert_eq!(
coordinates,
[(1.1, 1.1), (1.2, 1.2), (2.0, 2.0), (3.0, 3.0)]
);
assert_eq!(points[2].ele, 20.0);
assert!(points[2].time.is_some());
}
#[test]
fn test_parse_segments() {
let gpx = parse_data("with_segments");