mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-16 08:19:59 +00:00
progress
This commit is contained in:
1 parent
606a0e974a
commit
67a4c090bd
107 files changed
+3474
-3948
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user