mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-04 12:58:23 +00:00
refactoring
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
mod simplify;
|
||||
mod smooth;
|
||||
|
||||
pub use simplify::*;
|
||||
pub use smooth::*;
|
||||
@@ -0,0 +1,69 @@
|
||||
use crate::{
|
||||
core::gpx::{TrackSegment, TrackSegmentIndex},
|
||||
core::utils::crossarc,
|
||||
};
|
||||
|
||||
pub fn ramer_douglas_peucker<F>(
|
||||
trkseg: &TrackSegment,
|
||||
mapping: &F,
|
||||
epsilon: f64,
|
||||
) -> Vec<TrackSegmentIndex>
|
||||
where
|
||||
F: Fn(TrackSegmentIndex) -> (f64, f64),
|
||||
{
|
||||
match trkseg.len() {
|
||||
0 => vec![],
|
||||
1 => vec![trkseg.first_index().unwrap()],
|
||||
2 => vec![trkseg.last_index().unwrap()],
|
||||
_ => {
|
||||
let first = trkseg.first_index().unwrap();
|
||||
let last = trkseg.last_index().unwrap();
|
||||
|
||||
let mut indices = vec![first];
|
||||
ramer_douglas_peucker_helper(trkseg, first, last, mapping, epsilon, &mut indices);
|
||||
indices.push(last);
|
||||
indices
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ramer_douglas_peucker_helper<F>(
|
||||
trkseg: &TrackSegment,
|
||||
start: TrackSegmentIndex,
|
||||
end: TrackSegmentIndex,
|
||||
mapping: &F,
|
||||
epsilon: f64,
|
||||
indices: &mut Vec<TrackSegmentIndex>,
|
||||
) where
|
||||
F: Fn(TrackSegmentIndex) -> (f64, f64),
|
||||
{
|
||||
let mut max_idx = None;
|
||||
let mut max_dist = 0.0;
|
||||
|
||||
let start_pt = mapping(start);
|
||||
let end_pt = mapping(end);
|
||||
|
||||
let mut cur = trkseg.next_index(Some(start));
|
||||
while let Some(idx) = cur {
|
||||
if idx == end {
|
||||
break;
|
||||
}
|
||||
|
||||
let pt = mapping(idx);
|
||||
let dist = crossarc(start_pt.0, start_pt.1, end_pt.0, end_pt.1, pt.0, pt.1);
|
||||
if dist > max_dist {
|
||||
max_idx = Some(idx);
|
||||
max_dist = dist;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#[macro_export]
|
||||
macro_rules! for_each_window {
|
||||
(
|
||||
$trkseg:expr,
|
||||
$left:expr,
|
||||
$right:expr,
|
||||
$window:expr,
|
||||
|$a:ident, $b:ident| $distance:expr,
|
||||
|$i:ident, $l:ident, $r:ident| $body:block,
|
||||
) => {{
|
||||
let mut window_start = $left.unwrap_or_default();
|
||||
let mut window_center = $left;
|
||||
let mut window_end = $left.unwrap_or_default();
|
||||
while let Some(idx) = window_center {
|
||||
// advance left if needed
|
||||
while window_start != idx && {
|
||||
let $a = window_start;
|
||||
let $b = idx;
|
||||
$distance
|
||||
} > $window {
|
||||
if let Some(next) = $trkseg.next_index(Some(window_start)) {
|
||||
if next == idx {
|
||||
break;
|
||||
}
|
||||
window_start = next;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// advance right if needed
|
||||
window_end = window_end.max(idx);
|
||||
while let Some(next) = $trkseg.next_index(Some(window_end)) {
|
||||
if window_end != idx && {
|
||||
let $a = idx;
|
||||
let $b = next;
|
||||
$distance
|
||||
} > $window {
|
||||
break;
|
||||
}
|
||||
window_end = next;
|
||||
}
|
||||
// apply body
|
||||
let $i = idx;
|
||||
let $l = window_start;
|
||||
let $r = window_end;
|
||||
$body
|
||||
// go next
|
||||
if window_center == $right {
|
||||
break;
|
||||
}
|
||||
window_center = $trkseg.next_index(window_center);
|
||||
}
|
||||
}};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{Trackpoint, Waypoint};
|
||||
|
||||
static MAX_TRKPT_CHUNK_SIZE: usize = 4096;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct TrackpointChunkId(Uuid);
|
||||
|
||||
impl Default for TrackpointChunkId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TrackpointChunk {
|
||||
pub id: TrackpointChunkId,
|
||||
pub trkpt: Vec<Trackpoint>,
|
||||
}
|
||||
|
||||
impl PartialEq for TrackpointChunk {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl TrackpointChunk {
|
||||
pub fn is_full(&self) -> bool {
|
||||
self.trkpt.len() == MAX_TRKPT_CHUNK_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct WaypointChunkId(Uuid);
|
||||
|
||||
impl Default for WaypointChunkId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
static MAX_WPT_CHUNK_SIZE: usize = 128;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WaypointChunk {
|
||||
pub id: WaypointChunkId,
|
||||
pub wpt: Vec<Waypoint>,
|
||||
}
|
||||
|
||||
impl PartialEq for WaypointChunk {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl WaypointChunk {
|
||||
pub fn is_full(&self) -> bool {
|
||||
self.wpt.len() == MAX_WPT_CHUNK_SIZE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct Link {
|
||||
pub href: String,
|
||||
pub text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct LngLat {
|
||||
pub lng: f64,
|
||||
pub lat: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LngLatBounds {
|
||||
pub sw: LngLat,
|
||||
pub ne: LngLat,
|
||||
}
|
||||
|
||||
impl Default for LngLatBounds {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sw: LngLat {
|
||||
lng: 180.0,
|
||||
lat: 90.0,
|
||||
},
|
||||
ne: LngLat {
|
||||
lng: -180.0,
|
||||
lat: -90.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LngLatBounds {
|
||||
pub fn extend(&mut self, coordinates: LngLat) {
|
||||
self.sw.lng = self.sw.lng.min(coordinates.lng);
|
||||
self.sw.lat = self.sw.lat.min(coordinates.lat);
|
||||
self.ne.lng = self.ne.lng.max(coordinates.lng);
|
||||
self.ne.lat = self.ne.lat.max(coordinates.lat);
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: &LngLatBounds) {
|
||||
self.sw.lng = self.sw.lng.min(other.sw.lng);
|
||||
self.sw.lat = self.sw.lat.min(other.sw.lat);
|
||||
self.ne.lng = self.ne.lng.max(other.ne.lng);
|
||||
self.ne.lat = self.ne.lat.max(other.ne.lat);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{Link, Track, WaypointChunk};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct FileId(pub Uuid);
|
||||
|
||||
impl Default for FileId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct File {
|
||||
pub id: FileId,
|
||||
pub info: FileInfo,
|
||||
pub trk: Vec<Track>,
|
||||
pub wpt: Vec<Rc<WaypointChunk>>,
|
||||
// TODO routes
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct FileInfo {
|
||||
pub name: String,
|
||||
pub desc: Option<String>,
|
||||
pub author: Option<Author>,
|
||||
pub link: Option<Link>,
|
||||
pub time: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct Author {
|
||||
pub name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub link: Option<Link>,
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
mod chunk;
|
||||
mod common;
|
||||
mod file;
|
||||
mod segment;
|
||||
mod track;
|
||||
mod trackpoint;
|
||||
mod waypoint;
|
||||
|
||||
pub use chunk::*;
|
||||
pub use common::*;
|
||||
pub use file::*;
|
||||
pub use segment::*;
|
||||
pub use track::*;
|
||||
pub use trackpoint::*;
|
||||
pub use waypoint::*;
|
||||
@@ -0,0 +1,271 @@
|
||||
use std::{ops::Index, rc::Rc};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{Trackpoint, TrackpointChunk};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct TrackSegmentId(Uuid);
|
||||
|
||||
impl Default for TrackSegmentId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct TrackSegmentRevisionId(Uuid);
|
||||
|
||||
impl Default for TrackSegmentRevisionId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct TrackSegment {
|
||||
pub id: TrackSegmentId,
|
||||
pub rev_id: TrackSegmentRevisionId,
|
||||
chunks: Vec<Rc<TrackpointChunk>>,
|
||||
cumul_length: Vec<usize>,
|
||||
}
|
||||
|
||||
impl TrackSegment {
|
||||
pub fn push(&mut self, chunk: TrackpointChunk) {
|
||||
if chunk.trkpt.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.cumul_length
|
||||
.push(self.cumul_length.last().copied().unwrap_or_default() + chunk.trkpt.len());
|
||||
self.chunks.push(Rc::new(chunk));
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.cumul_length.last().copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> TrackSegmentIterator<'_> {
|
||||
TrackSegmentIterator::new(self)
|
||||
}
|
||||
|
||||
pub fn first_index(&self) -> Option<TrackSegmentIndex> {
|
||||
self.next_index(None)
|
||||
}
|
||||
|
||||
pub fn last_index(&self) -> Option<TrackSegmentIndex> {
|
||||
self.prev_index(None)
|
||||
}
|
||||
|
||||
pub fn next_index(&self, cur: Option<TrackSegmentIndex>) -> Option<TrackSegmentIndex> {
|
||||
let mut next = cur.map_or_default(|idx| TrackSegmentIndex {
|
||||
chunk: idx.chunk,
|
||||
pos: idx.pos + 1,
|
||||
flat: idx.flat + 1,
|
||||
});
|
||||
loop {
|
||||
if next.chunk >= self.chunks.len() {
|
||||
return None;
|
||||
}
|
||||
if next.pos == self.chunks[next.chunk].trkpt.len() {
|
||||
next.chunk += 1;
|
||||
next.pos = 0;
|
||||
} else {
|
||||
return Some(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev_index(&self, cur: Option<TrackSegmentIndex>) -> Option<TrackSegmentIndex> {
|
||||
let mut prev = cur.unwrap_or(TrackSegmentIndex {
|
||||
chunk: self.chunks.len(),
|
||||
pos: 0,
|
||||
flat: self.cumul_length.last().copied().unwrap_or_default(),
|
||||
});
|
||||
loop {
|
||||
if prev.pos == 0 {
|
||||
if prev.chunk == 0 {
|
||||
return None;
|
||||
}
|
||||
prev.chunk -= 1;
|
||||
prev.pos = self.chunks[prev.chunk].trkpt.len();
|
||||
prev.flat -= 1;
|
||||
} else {
|
||||
prev.pos -= 1;
|
||||
prev.flat -= 1;
|
||||
return Some(prev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn locate(&self, idx: usize) -> Option<TrackSegmentIndex> {
|
||||
let chunk = self.cumul_length.partition_point(|l| idx >= *l);
|
||||
if chunk >= self.chunks.len() {
|
||||
return None;
|
||||
}
|
||||
let pos = if chunk > 0 {
|
||||
idx - self.cumul_length[chunk - 1]
|
||||
} else {
|
||||
idx
|
||||
};
|
||||
if pos >= self.chunks[chunk].trkpt.len() {
|
||||
None
|
||||
} else {
|
||||
Some(TrackSegmentIndex {
|
||||
chunk,
|
||||
pos,
|
||||
flat: idx,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<TrackSegmentIndex> for TrackSegment {
|
||||
type Output = Trackpoint;
|
||||
|
||||
fn index(&self, idx: TrackSegmentIndex) -> &Self::Output {
|
||||
&self.chunks[idx.chunk].trkpt[idx.pos]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<usize> for TrackSegment {
|
||||
type Output = Trackpoint;
|
||||
|
||||
fn index(&self, idx: usize) -> &Self::Output {
|
||||
&self[self.locate(idx).unwrap()]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, PartialOrd, Ord)]
|
||||
pub struct TrackSegmentIndex {
|
||||
pub chunk: usize,
|
||||
pub pos: usize,
|
||||
pub flat: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TrackSegmentIterator<'a> {
|
||||
trkseg: &'a TrackSegment,
|
||||
idx: Option<TrackSegmentIndex>,
|
||||
}
|
||||
|
||||
impl<'a> TrackSegmentIterator<'a> {
|
||||
pub fn new(trkseg: &'a TrackSegment) -> Self {
|
||||
Self {
|
||||
trkseg,
|
||||
idx: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TrackSegmentIterator<'a> {
|
||||
type Item = &'a Trackpoint;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.idx = self.trkseg.next_index(self.idx);
|
||||
self.idx.map(|idx| &self.trkseg[idx])
|
||||
}
|
||||
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
let idx = self.idx.map_or_default(|idx| idx.flat) + n;
|
||||
self.idx = self.trkseg.locate(idx);
|
||||
self.idx.map(|idx| &self.trkseg[idx])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_track_segment(nb_chunks: usize) -> TrackSegment {
|
||||
let mut trkseg = TrackSegment::default();
|
||||
let mut count = 0;
|
||||
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);
|
||||
count += 1;
|
||||
}
|
||||
trkseg.push(chunk);
|
||||
}
|
||||
trkseg
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_len() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
assert_eq!(trkseg.len(), nb_chunks * (nb_chunks + 1) / 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_locate() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
for i in 0..trkseg.len() {
|
||||
let idx = trkseg.locate(i);
|
||||
assert!(idx.is_some());
|
||||
assert_eq!(trkseg[idx.unwrap()].ele, i as f64);
|
||||
}
|
||||
|
||||
assert!(trkseg.locate(trkseg.len()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_index() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
for i in 0..trkseg.len() {
|
||||
assert_eq!(trkseg[i].ele, i as f64);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_index_out_of_bounds_1() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
trkseg[trkseg.len()].ele;
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_index_out_of_bounds_2() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
trkseg[TrackSegmentIndex {
|
||||
chunk: trkseg.chunks.len(),
|
||||
pos: 0,
|
||||
flat: 0,
|
||||
}]
|
||||
.ele;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
for (i, trkpt) in trkseg.iter().enumerate() {
|
||||
assert!(std::ptr::eq(&trkseg[i], trkpt));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_nth() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
let tenth = trkseg.iter().nth(10);
|
||||
assert!(tenth.is_some());
|
||||
assert!(std::ptr::eq(&trkseg[10], tenth.unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_skip() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
for (i, trkpt) in trkseg.iter().enumerate().skip(5) {
|
||||
assert!(std::ptr::eq(&trkseg[i], trkpt));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{Link, TrackSegment};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct TrackId(Uuid);
|
||||
|
||||
impl Default for TrackId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct Track {
|
||||
pub id: TrackId,
|
||||
pub info: TrackInfo,
|
||||
pub trkseg: Vec<TrackSegment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct TrackInfo {
|
||||
pub name: Option<String>,
|
||||
pub cmt: Option<String>,
|
||||
pub desc: Option<String>,
|
||||
pub src: Option<String>,
|
||||
pub link: Option<Link>,
|
||||
pub type_: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub opacity: Option<f64>,
|
||||
pub width: Option<f64>,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use crate::LngLat;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Trackpoint {
|
||||
pub coordinates: LngLat,
|
||||
pub ele: f64,
|
||||
pub time: Option<i64>,
|
||||
pub atemp: Option<i16>,
|
||||
pub hr: Option<u16>,
|
||||
pub cad: Option<u16>,
|
||||
pub power: Option<u16>,
|
||||
// TODO OSM data? or store intervals at a higher level?
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{Link, LngLat};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct WaypointId(Uuid);
|
||||
|
||||
impl Default for WaypointId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Waypoint {
|
||||
pub id: WaypointId,
|
||||
pub coordinates: LngLat,
|
||||
pub ele: f64,
|
||||
pub time: Option<i64>,
|
||||
pub name: Option<String>,
|
||||
pub cmt: Option<String>,
|
||||
pub desc: Option<String>,
|
||||
pub link: Option<Link>,
|
||||
pub sym: Option<String>,
|
||||
pub type_: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod parse;
|
||||
|
||||
pub use parse::*;
|
||||
@@ -0,0 +1,637 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Author, File, Link, LngLat, Track, TrackSegment, Trackpoint, TrackpointChunk, Waypoint,
|
||||
WaypointChunk,
|
||||
};
|
||||
use chrono::DateTime;
|
||||
use quick_xml::Error;
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::events::attributes::Attributes;
|
||||
use quick_xml::reader::Reader;
|
||||
|
||||
enum GPXElement {
|
||||
Metadata,
|
||||
Name,
|
||||
Comment,
|
||||
Description,
|
||||
Source,
|
||||
Author(Author),
|
||||
Link(Link),
|
||||
Text,
|
||||
Track(Track),
|
||||
Segment(TrackSegment),
|
||||
Trackpoint(Trackpoint),
|
||||
Waypoint(Waypoint),
|
||||
Elevation,
|
||||
Time,
|
||||
Temperature,
|
||||
Heartrate,
|
||||
Cadence,
|
||||
Power,
|
||||
Symbol,
|
||||
Type,
|
||||
Color,
|
||||
Opacity,
|
||||
Width,
|
||||
}
|
||||
|
||||
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(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
coordinates
|
||||
}
|
||||
|
||||
pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||
let mut reader = Reader::from_reader(data);
|
||||
let mut buf = vec![];
|
||||
let mut gpx = File::default();
|
||||
let mut stack: Vec<GPXElement> = vec![];
|
||||
let mut trkpt_chunk = TrackpointChunk::default();
|
||||
let mut wpt_chunk = WaypointChunk::default();
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(Event::Start(e)) => match e.name().as_ref() {
|
||||
"metadata" => stack.push(GPXElement::Metadata),
|
||||
"name" => stack.push(GPXElement::Name),
|
||||
"cmt" => stack.push(GPXElement::Comment),
|
||||
"desc" => stack.push(GPXElement::Description),
|
||||
"src" => stack.push(GPXElement::Source),
|
||||
"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();
|
||||
}
|
||||
}
|
||||
}
|
||||
stack.push(GPXElement::Link(link));
|
||||
}
|
||||
"text" => stack.push(GPXElement::Text),
|
||||
"trk" => stack.push(GPXElement::Track(Track::default())),
|
||||
"trkseg" => {
|
||||
stack.push(GPXElement::Segment(TrackSegment::default()));
|
||||
}
|
||||
"trkpt" => {
|
||||
let mut trkpt = Trackpoint::default();
|
||||
trkpt.coordinates = parse_coordinates(e.attributes());
|
||||
stack.push(GPXElement::Trackpoint(trkpt));
|
||||
}
|
||||
"wpt" => {
|
||||
let mut wpt = Waypoint::default();
|
||||
wpt.coordinates = parse_coordinates(e.attributes());
|
||||
stack.push(GPXElement::Waypoint(wpt));
|
||||
}
|
||||
"ele" => stack.push(GPXElement::Elevation),
|
||||
"time" => stack.push(GPXElement::Time),
|
||||
e if e.ends_with("atemp") => stack.push(GPXElement::Temperature),
|
||||
e if e.ends_with("hr") => stack.push(GPXElement::Heartrate),
|
||||
e if e.ends_with("cad") => stack.push(GPXElement::Cadence),
|
||||
"power" => stack.push(GPXElement::Power),
|
||||
e if e.ends_with("PowerInWatts") => stack.push(GPXElement::Power),
|
||||
"sym" => stack.push(GPXElement::Symbol),
|
||||
"type" => stack.push(GPXElement::Type),
|
||||
e if e.ends_with("color") => stack.push(GPXElement::Color),
|
||||
e if e.ends_with("opacity") => stack.push(GPXElement::Opacity),
|
||||
e if e.ends_with("width") => stack.push(GPXElement::Width),
|
||||
_ => (),
|
||||
},
|
||||
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();
|
||||
}
|
||||
}
|
||||
"metadata" => {
|
||||
stack.pop();
|
||||
}
|
||||
"author" => {
|
||||
if let Some(GPXElement::Author(author)) = stack.pop() {
|
||||
gpx.info.author = Some(author);
|
||||
}
|
||||
}
|
||||
"link" => {
|
||||
if let Some(GPXElement::Link(link)) = stack.pop() {
|
||||
match stack.last_mut() {
|
||||
Some(GPXElement::Author(author)) => {
|
||||
author.link = Some(link);
|
||||
}
|
||||
Some(GPXElement::Track(trk)) => {
|
||||
trk.info.link = Some(link);
|
||||
}
|
||||
Some(GPXElement::Waypoint(wpt)) => {
|
||||
wpt.link = Some(link);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
"trk" => {
|
||||
if let Some(GPXElement::Track(trk)) = stack.pop() {
|
||||
gpx.trk.push(trk);
|
||||
}
|
||||
}
|
||||
"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);
|
||||
}
|
||||
}
|
||||
}
|
||||
"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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"wpt" => {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Ok(Event::Text(e)) => match stack.last_mut() {
|
||||
Some(GPXElement::Name) => {
|
||||
stack.pop();
|
||||
match stack.last_mut() {
|
||||
Some(GPXElement::Metadata) => {
|
||||
gpx.info.name = e.to_string();
|
||||
}
|
||||
Some(GPXElement::Author(author)) => {
|
||||
author.name = Some(e.to_string());
|
||||
}
|
||||
Some(GPXElement::Track(trk)) => {
|
||||
trk.info.name = Some(e.to_string());
|
||||
}
|
||||
Some(GPXElement::Waypoint(wpt)) => {
|
||||
wpt.name = Some(e.to_string());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Comment) => {
|
||||
stack.pop();
|
||||
match stack.last_mut() {
|
||||
Some(GPXElement::Track(trk)) => {
|
||||
trk.info.cmt = Some(e.to_string());
|
||||
}
|
||||
Some(GPXElement::Waypoint(wpt)) => {
|
||||
wpt.cmt = Some(e.to_string());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Description) => {
|
||||
stack.pop();
|
||||
match stack.last_mut() {
|
||||
Some(GPXElement::Metadata) => {
|
||||
gpx.info.desc = Some(e.to_string());
|
||||
}
|
||||
Some(GPXElement::Track(trk)) => {
|
||||
trk.info.desc = Some(e.to_string());
|
||||
}
|
||||
Some(GPXElement::Waypoint(wpt)) => {
|
||||
wpt.desc = Some(e.to_string());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Source) => {
|
||||
stack.pop();
|
||||
match stack.last_mut() {
|
||||
Some(GPXElement::Track(trk)) => {
|
||||
trk.info.src = Some(e.to_string());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Text) => {
|
||||
stack.pop();
|
||||
if let Some(GPXElement::Link(link)) = stack.last_mut() {
|
||||
link.text = Some(e.to_string());
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Elevation) => {
|
||||
stack.pop();
|
||||
match stack.last_mut() {
|
||||
Some(GPXElement::Trackpoint(trkpt)) => {
|
||||
trkpt.ele = e.parse().unwrap_or_default();
|
||||
}
|
||||
Some(GPXElement::Waypoint(wpt)) => {
|
||||
wpt.ele = e.parse().unwrap_or_default();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Temperature) => {
|
||||
stack.pop();
|
||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||
trkpt.atemp = e.parse().ok();
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Heartrate) => {
|
||||
stack.pop();
|
||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||
trkpt.hr = e.parse().ok();
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Cadence) => {
|
||||
stack.pop();
|
||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||
trkpt.cad = e.parse().ok();
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Power) => {
|
||||
stack.pop();
|
||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||
trkpt.power = e.parse().ok();
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Symbol) => {
|
||||
stack.pop();
|
||||
match stack.last_mut() {
|
||||
Some(GPXElement::Waypoint(wpt)) => {
|
||||
wpt.sym = Some(e.to_string());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Type) => {
|
||||
stack.pop();
|
||||
match stack.last_mut() {
|
||||
Some(GPXElement::Track(trk)) => {
|
||||
trk.info.type_ = Some(e.to_string());
|
||||
}
|
||||
Some(GPXElement::Waypoint(wpt)) => {
|
||||
wpt.type_ = Some(e.to_string());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Opacity) => {
|
||||
stack.pop();
|
||||
match stack.last_mut() {
|
||||
Some(GPXElement::Track(trk)) => {
|
||||
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();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(e),
|
||||
_ => (),
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
Ok(gpx)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[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();
|
||||
|
||||
assert_eq!(gpx.info.name, "simple");
|
||||
assert!(gpx.info.desc.is_some_and(|d| d == "description"));
|
||||
assert!(gpx.info.author.is_some());
|
||||
let author = gpx.info.author.as_ref().unwrap();
|
||||
assert!(author.name.as_ref().is_some_and(|n| n == "gpx.studio"));
|
||||
assert!(author.link.is_some());
|
||||
let link = author.link.as_ref().unwrap();
|
||||
assert_eq!(link.href, "https://gpx.studio");
|
||||
assert!(link.text.as_ref().is_some_and(|t| t == "link text"));
|
||||
|
||||
assert_eq!(gpx.trk.len(), 1);
|
||||
let trk = &gpx.trk[0];
|
||||
assert!(trk.info.name.as_ref().is_some_and(|n| n == "track name"));
|
||||
assert!(trk.info.cmt.as_ref().is_some_and(|c| c == "track comment"));
|
||||
assert!(
|
||||
trk.info
|
||||
.desc
|
||||
.as_ref()
|
||||
.is_some_and(|d| d == "track description")
|
||||
);
|
||||
assert!(trk.info.src.as_ref().is_some_and(|s| s == "track source"));
|
||||
assert!(trk.info.link.is_some());
|
||||
let link = trk.info.link.as_ref().unwrap();
|
||||
assert_eq!(link.href, "https://gpx.studio");
|
||||
assert!(link.text.as_ref().is_some_and(|t| t == "track link text"));
|
||||
assert!(trk.info.type_.as_ref().is_some_and(|c| c == "Cycling"));
|
||||
|
||||
assert_eq!(trk.trkseg.len(), 1);
|
||||
let trkseg = &trk.trkseg[0];
|
||||
assert_eq!(trkseg.len(), 80);
|
||||
let trkpt = &trkseg[0];
|
||||
assert_eq!(trkpt.coordinates.lat, 50.790867);
|
||||
assert_eq!(trkpt.coordinates.lng, 4.404968);
|
||||
assert_eq!(trkpt.ele, 109.0);
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
assert_eq!(gpx.trk.len(), 2);
|
||||
let trk = &gpx.trk[0];
|
||||
assert!(trk.info.name.as_ref().is_some_and(|n| n == "track 1"));
|
||||
assert!(trk.info.type_.as_ref().is_some_and(|c| c == "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!(trk.info.name.as_ref().is_some_and(|n| n == "track 2"));
|
||||
assert!(trk.info.type_.as_ref().is_some_and(|c| c == "Cycling"));
|
||||
|
||||
assert_eq!(trk.trkseg.len(), 1);
|
||||
let trkseg = &trk.trkseg[0];
|
||||
assert_eq!(trkseg.len(), 28);
|
||||
let trkpt = &trkseg[0];
|
||||
assert_eq!(trkpt.coordinates.lat, 50.782212);
|
||||
assert_eq!(trkpt.coordinates.lng, 4.406377);
|
||||
assert_eq!(trkpt.ele, 115.5);
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
assert_eq!(gpx.trk.len(), 1);
|
||||
let trk = &gpx.trk[0];
|
||||
|
||||
assert_eq!(trk.trkseg.len(), 2);
|
||||
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 trkseg = &trk.trkseg[1];
|
||||
assert_eq!(trkseg.len(), 28);
|
||||
let trkpt = &trkseg[0];
|
||||
assert_eq!(trkpt.coordinates.lat, 50.782212);
|
||||
assert_eq!(trkpt.coordinates.lng, 4.406377);
|
||||
assert_eq!(trkpt.ele, 115.5);
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
assert_eq!(gpx.trk.len(), 2);
|
||||
let trk = &gpx.trk[0];
|
||||
|
||||
assert_eq!(trk.trkseg.len(), 2);
|
||||
let trkseg = &trk.trkseg[0];
|
||||
assert_eq!(trkseg.len(), 16);
|
||||
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 trkseg = &trk.trkseg[1];
|
||||
assert_eq!(trkseg.len(), 34);
|
||||
let trkpt = &trkseg[0];
|
||||
assert_eq!(trkpt.coordinates.lat, 50.78727108169855);
|
||||
assert_eq!(trkpt.coordinates.lng, 4.406133681127736);
|
||||
assert_eq!(trkpt.ele, 115.0);
|
||||
|
||||
let trk = &gpx.trk[1];
|
||||
|
||||
assert_eq!(trk.trkseg.len(), 2);
|
||||
let trkseg = &trk.trkseg[0];
|
||||
assert_eq!(trkseg.len(), 19);
|
||||
let trkpt = &trkseg[0];
|
||||
assert_eq!(trkpt.coordinates.lat, 50.782212);
|
||||
assert_eq!(trkpt.coordinates.lng, 4.406377);
|
||||
assert_eq!(trkpt.ele, 115.5);
|
||||
|
||||
let trkseg = &trk.trkseg[1];
|
||||
assert_eq!(trkseg.len(), 10);
|
||||
let trkpt = &trkseg[0];
|
||||
assert_eq!(trkpt.coordinates.lat, 50.77906316558724);
|
||||
assert_eq!(trkpt.coordinates.lng, 4.412547477922485);
|
||||
assert_eq!(trkpt.ele, 133.3);
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
assert_eq!(gpx.wpt.len(), 1);
|
||||
let chunk = &gpx.wpt[0];
|
||||
assert_eq!(chunk.wpt.len(), 1);
|
||||
let wpt = &chunk.wpt[0];
|
||||
assert_eq!(wpt.coordinates.lat, 50.7836710064975);
|
||||
assert_eq!(wpt.coordinates.lng, 4.410764082658738);
|
||||
assert!(wpt.name.as_ref().is_some_and(|n| n == "waypoint name"));
|
||||
assert!(wpt.cmt.as_ref().is_some_and(|c| c == "waypoint comment"));
|
||||
assert!(
|
||||
wpt.desc
|
||||
.as_ref()
|
||||
.is_some_and(|d| d == "waypoint description")
|
||||
);
|
||||
assert!(wpt.link.is_some());
|
||||
let link = wpt.link.as_ref().unwrap();
|
||||
assert_eq!(link.href, "https://gpx.studio");
|
||||
assert!(
|
||||
link.text
|
||||
.as_ref()
|
||||
.is_some_and(|t| t == "waypoint link text")
|
||||
);
|
||||
assert!(wpt.sym.as_ref().is_some_and(|s| s == "Bike Trail"));
|
||||
assert!(wpt.type_.as_ref().is_some_and(|t| t == "Bike Trail"));
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
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);
|
||||
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();
|
||||
|
||||
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);
|
||||
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();
|
||||
|
||||
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);
|
||||
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();
|
||||
|
||||
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);
|
||||
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();
|
||||
|
||||
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);
|
||||
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();
|
||||
|
||||
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);
|
||||
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();
|
||||
|
||||
assert_eq!(gpx.trk.len(), 1);
|
||||
let trk = &gpx.trk[0];
|
||||
assert_eq!(trk.trkseg.len(), 1);
|
||||
assert!(trk.info.color.as_ref().is_some_and(|c| c == "#2d3ee9"));
|
||||
assert!(trk.info.opacity.is_some_and(|o| o == 0.5));
|
||||
assert!(trk.info.width.is_some_and(|w| w == 6.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mod algorithm;
|
||||
pub(crate) mod gpx;
|
||||
mod io;
|
||||
pub(crate) mod statistics;
|
||||
mod utils;
|
||||
|
||||
pub use algorithm::*;
|
||||
pub use gpx::*;
|
||||
pub use io::*;
|
||||
pub use statistics::*;
|
||||
pub use utils::*;
|
||||
@@ -0,0 +1,5 @@
|
||||
mod statistics;
|
||||
mod utils;
|
||||
|
||||
pub use statistics::*;
|
||||
pub use utils::*;
|
||||
@@ -0,0 +1,313 @@
|
||||
use crate::{
|
||||
LngLat, LngLatBounds, TrackSegment, TrackSegmentIndex, Trackpoint, distance, for_each_window,
|
||||
ramer_douglas_peucker, slope, speed, sum_options, time_diff,
|
||||
};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Statistics {
|
||||
pub global: GlobalStatistics,
|
||||
pub local: Vec<TrackpointStatistics>,
|
||||
}
|
||||
|
||||
impl Statistics {
|
||||
pub fn total_time(&self) -> Option<i32> {
|
||||
self.global.total_time()
|
||||
}
|
||||
|
||||
pub fn total_speed(&self) -> Option<f64> {
|
||||
self.global.total_speed()
|
||||
}
|
||||
|
||||
pub fn moving_speed(&self) -> Option<f64> {
|
||||
self.global.moving_speed()
|
||||
}
|
||||
|
||||
pub fn compute(trkseg: &TrackSegment) -> Self {
|
||||
let mut stats = Self::default();
|
||||
if trkseg.len() == 0 {
|
||||
return stats;
|
||||
}
|
||||
|
||||
let mut prev = &trkseg[0];
|
||||
for cur in trkseg.iter() {
|
||||
stats.accumulate(prev, cur);
|
||||
prev = cur;
|
||||
}
|
||||
|
||||
stats.compute_smoothed_speed(trkseg);
|
||||
stats.compute_smoothed_elevation_segments(trkseg);
|
||||
stats.compute_smoothed_slope(trkseg);
|
||||
|
||||
stats
|
||||
}
|
||||
|
||||
fn accumulate(&mut self, prev: &Trackpoint, cur: &Trackpoint) {
|
||||
self.accumulate_distance_and_time(prev, cur);
|
||||
self.update_time_bounds(cur.time);
|
||||
self.update_bounds(cur.coordinates);
|
||||
self.local
|
||||
.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);
|
||||
|
||||
self.global.total_distance += dist;
|
||||
|
||||
if let Some(time) = time {
|
||||
let speed = speed(dist, time);
|
||||
if speed >= 0.5 && speed <= 1500.0 {
|
||||
self.global.moving_distance = self
|
||||
.global
|
||||
.moving_distance
|
||||
.map_or(Some(dist), |d| Some(d + dist));
|
||||
self.global.moving_time = self
|
||||
.global
|
||||
.moving_time
|
||||
.map_or(Some(time), |t| Some(t + time));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_time_bounds(&mut self, time: Option<i64>) {
|
||||
if let Some(time) = time {
|
||||
if self.global.start_time.is_none() {
|
||||
self.global.start_time = Some(time);
|
||||
}
|
||||
self.global.end_time = Some(time);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_bounds(&mut self, coordinates: LngLat) {
|
||||
self.global.bounds.extend(coordinates);
|
||||
}
|
||||
|
||||
fn compute_smoothed_speed(&mut self, trkseg: &TrackSegment) {
|
||||
for_each_window!(
|
||||
trkseg,
|
||||
trkseg.first_index(),
|
||||
trkseg.last_index(),
|
||||
Some(10000),
|
||||
|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| {
|
||||
speed(
|
||||
self.local[right.flat].total_distance
|
||||
- self.local[left.flat].total_distance,
|
||||
t,
|
||||
)
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn compute_smoothed_elevation_segments(&mut self, trkseg: &TrackSegment) {
|
||||
let simplified = self.get_elevation_extremas(trkseg);
|
||||
|
||||
for i in 0..(simplified.len() - 1) {
|
||||
let start = simplified[i];
|
||||
let end = simplified[i + 1];
|
||||
let last = i + 1 == simplified.len() - 1;
|
||||
|
||||
self.compute_smoothed_elevation_gain(trkseg, start, end, last);
|
||||
|
||||
let segment_dist =
|
||||
self.local[end.flat].total_distance - self.local[start.flat].total_distance;
|
||||
let segment_ele = trkseg[end].ele - trkseg[start].ele;
|
||||
let segment_slope = slope(segment_ele, segment_dist);
|
||||
for k in start.flat..(end.flat + last as usize) {
|
||||
self.local[k].slope_segment = SlopeSegment {
|
||||
slope: segment_slope,
|
||||
distance: segment_dist,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_smoothed_elevation_gain(
|
||||
&mut self,
|
||||
trkseg: &TrackSegment,
|
||||
start: TrackSegmentIndex,
|
||||
end: TrackSegmentIndex,
|
||||
last: bool,
|
||||
) {
|
||||
let mut cumul_ele = 0.0;
|
||||
let mut current_left = start;
|
||||
let mut current_right = Some(start);
|
||||
let mut prev_smoothed_ele = trkseg[start].ele;
|
||||
|
||||
for_each_window!(
|
||||
trkseg,
|
||||
Some(start),
|
||||
Some(end),
|
||||
0.1,
|
||||
|i, j| self.local[j.flat].total_distance - self.local[i.flat].total_distance,
|
||||
|i, left, right| {
|
||||
while current_left != left {
|
||||
cumul_ele -= trkseg[current_left].ele;
|
||||
current_left = trkseg.next_index(Some(current_left)).unwrap();
|
||||
}
|
||||
while let Some(current) = current_right {
|
||||
if current > right {
|
||||
break;
|
||||
}
|
||||
cumul_ele += trkseg[current].ele;
|
||||
current_right = trkseg.next_index(current_right);
|
||||
}
|
||||
|
||||
let smoothed_ele = if i == start || i == end {
|
||||
trkseg[i].ele
|
||||
} else {
|
||||
cumul_ele / (right.flat - left.flat + 1) as f64
|
||||
};
|
||||
|
||||
let delta = smoothed_ele - prev_smoothed_ele;
|
||||
if delta > 0.0 {
|
||||
self.global.elevation_gain += delta;
|
||||
} else if delta < 0.0 {
|
||||
self.global.elevation_loss -= delta;
|
||||
}
|
||||
|
||||
if i < end || last {
|
||||
self.local[i.flat].elevation_gain = self.global.elevation_gain;
|
||||
self.local[i.flat].elevation_loss = self.global.elevation_loss;
|
||||
}
|
||||
|
||||
prev_smoothed_ele = smoothed_ele;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn compute_smoothed_slope(&mut self, trkseg: &TrackSegment) {
|
||||
for_each_window!(
|
||||
trkseg,
|
||||
trkseg.first_index(),
|
||||
trkseg.last_index(),
|
||||
0.05,
|
||||
|i, j| self.local[j.flat].total_distance - self.local[i.flat].total_distance,
|
||||
|i, left, right| {
|
||||
let dist =
|
||||
self.local[right.flat].total_distance - self.local[left.flat].total_distance;
|
||||
let ele = trkseg[right].ele - trkseg[left].ele;
|
||||
self.local[i.flat].slope = slope(ele, dist);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn get_elevation_extremas(&self, trkseg: &TrackSegment) -> Vec<TrackSegmentIndex> {
|
||||
ramer_douglas_peucker(
|
||||
trkseg,
|
||||
&|idx| {
|
||||
(
|
||||
self.local[idx.flat].total_distance * 1000.0,
|
||||
trkseg[idx].ele,
|
||||
)
|
||||
},
|
||||
20.0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct GlobalStatistics {
|
||||
pub total_distance: f64,
|
||||
pub moving_distance: Option<f64>,
|
||||
pub moving_time: Option<i32>,
|
||||
pub elevation_gain: f64,
|
||||
pub elevation_loss: f64,
|
||||
pub start_time: Option<i64>,
|
||||
pub end_time: Option<i64>,
|
||||
pub bounds: LngLatBounds,
|
||||
}
|
||||
|
||||
impl GlobalStatistics {
|
||||
pub fn total_time(&self) -> Option<i32> {
|
||||
time_diff(&self.start_time, &self.end_time)
|
||||
}
|
||||
|
||||
pub fn total_speed(&self) -> Option<f64> {
|
||||
self.total_time().map(|t| speed(self.total_distance, t))
|
||||
}
|
||||
|
||||
pub fn moving_speed(&self) -> Option<f64> {
|
||||
self.moving_distance
|
||||
.zip(self.moving_time)
|
||||
.map(|(d, t)| speed(d, t))
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: &GlobalStatistics) {
|
||||
self.total_distance += other.total_distance;
|
||||
self.moving_distance = sum_options(self.moving_distance, other.moving_distance);
|
||||
self.moving_time = sum_options(self.moving_time, other.moving_time);
|
||||
self.elevation_gain += other.elevation_gain;
|
||||
self.elevation_loss += other.elevation_loss;
|
||||
self.bounds.merge(&other.bounds);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct TrackpointStatistics {
|
||||
pub total_distance: f64,
|
||||
pub moving_distance: Option<f64>,
|
||||
pub total_time: Option<i32>,
|
||||
pub moving_time: Option<i32>,
|
||||
pub speed: Option<f64>,
|
||||
pub elevation_gain: f64,
|
||||
pub elevation_loss: f64,
|
||||
pub slope: f64,
|
||||
pub slope_segment: SlopeSegment,
|
||||
}
|
||||
|
||||
impl TrackpointStatistics {
|
||||
fn from_partial_stats(stats: &Statistics) -> Self {
|
||||
Self {
|
||||
total_distance: stats.global.total_distance,
|
||||
moving_distance: stats.global.moving_distance,
|
||||
total_time: stats.total_time(),
|
||||
moving_time: stats.global.moving_time,
|
||||
// stats below are computed later
|
||||
speed: None,
|
||||
elevation_gain: Default::default(),
|
||||
elevation_loss: Default::default(),
|
||||
slope: Default::default(),
|
||||
slope_segment: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct SlopeSegment {
|
||||
pub slope: f64,
|
||||
pub distance: f64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
use crate::parse;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_compute_smoothed_speed() {
|
||||
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 trkseg = &gpx.trk[0].trkseg[0];
|
||||
let stats = Statistics::compute(trkseg);
|
||||
assert_eq!(stats.local.len(), trkseg.len());
|
||||
for trkpt_stats in stats.local.iter() {
|
||||
assert!(trkpt_stats.speed.is_some());
|
||||
let speed = trkpt_stats.speed.unwrap();
|
||||
assert_ne!(speed, f64::NAN);
|
||||
assert!((speed - 20.0).abs() < 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO more tests
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use std::ops::Add;
|
||||
|
||||
pub fn sum_options<T>(a: Option<T>, b: Option<T>) -> Option<T>
|
||||
where
|
||||
T: Add<Output = T>,
|
||||
{
|
||||
match (a, b) {
|
||||
(Some(a), Some(b)) => Some(a + b),
|
||||
(Some(a), None) => Some(a),
|
||||
(None, Some(b)) => Some(b),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use crate::LngLat;
|
||||
|
||||
static TO_RADIANS: f64 = PI / 180.0;
|
||||
static 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 {
|
||||
let lat1 = p1.lat * TO_RADIANS;
|
||||
let lat2 = p2.lat * TO_RADIANS;
|
||||
let delta_lat = lat2 - lat1;
|
||||
let delta_lng = (p2.lng - p1.lng) * TO_RADIANS;
|
||||
|
||||
let a =
|
||||
(delta_lat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (delta_lng / 2.0).sin().powi(2);
|
||||
let c = 2.0 * a.min(1.0).sqrt().asin();
|
||||
EARTH_RADIUS * c
|
||||
}
|
||||
|
||||
pub fn time_diff(a: &Option<i64>, b: &Option<i64>) -> Option<i32> {
|
||||
match (a, b) {
|
||||
(Some(t1), Some(t2)) => Some((t1 - t2) as i32),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
pub fn slope(ele: f64, distance: f64) -> f64 {
|
||||
if distance == 0.0 {
|
||||
100.0
|
||||
} else {
|
||||
0.1 * ele / distance
|
||||
}
|
||||
}
|
||||
|
||||
static 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
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
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;
|
||||
|
||||
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
|
||||
LngLat {
|
||||
lng: proj_x / meters_per_longitude_degree,
|
||||
lat: proj_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 {
|
||||
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()
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user