mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-04 12:58:23 +00:00
cleaning
This commit is contained in:
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
mod file;
|
||||
|
||||
pub use file::*;
|
||||
@@ -2,4 +2,3 @@ mod simplify;
|
||||
mod smooth;
|
||||
|
||||
pub use simplify::*;
|
||||
pub use smooth::*;
|
||||
|
||||
@@ -59,12 +59,10 @@ fn ramer_douglas_peucker_helper<F>(
|
||||
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);
|
||||
}
|
||||
if let Some(idx) = max_idx.filter(|_| 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ macro_rules! for_each_window {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{TrackSegment, Trackpoint, TrackpointChunk, for_each_window};
|
||||
use crate::{TrackSegment, Trackpoint, TrackpointChunk};
|
||||
|
||||
fn segment(eles: &[f64]) -> TrackSegment {
|
||||
let mut trkseg = TrackSegment::default();
|
||||
@@ -116,7 +116,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_window_sub_range() {
|
||||
let trkseg = segment(&vec![0.0; 10]);
|
||||
let trkseg = segment(&[0.0; 10]);
|
||||
let mut centers = vec![];
|
||||
let mut first = trkseg.first_index();
|
||||
for _ in 0..3 {
|
||||
|
||||
@@ -2,7 +2,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::{Trackpoint, Waypoint};
|
||||
|
||||
static MAX_TRKPT_CHUNK_SIZE: usize = 4096;
|
||||
const MAX_TRKPT_CHUNK_SIZE: usize = 4096;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct TrackpointChunkId(Uuid);
|
||||
@@ -40,7 +40,7 @@ impl Default for WaypointChunkId {
|
||||
}
|
||||
}
|
||||
|
||||
static MAX_WPT_CHUNK_SIZE: usize = 128;
|
||||
const MAX_WPT_CHUNK_SIZE: usize = 128;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WaypointChunk {
|
||||
@@ -91,9 +91,10 @@ mod tests {
|
||||
let a = TrackpointChunk::default();
|
||||
let b = TrackpointChunk::default();
|
||||
assert_ne!(a, b);
|
||||
let mut c = TrackpointChunk::default();
|
||||
c.id = TrackpointChunkId(a.id.0);
|
||||
c.trkpt.push(Trackpoint::default());
|
||||
let c = TrackpointChunk {
|
||||
id: TrackpointChunkId(a.id.0),
|
||||
trkpt: vec![Trackpoint::default()],
|
||||
};
|
||||
assert_eq!(a, c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,10 @@ impl TrackSegment {
|
||||
self.cumul_length.last().copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.chunks.is_empty()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> TrackSegmentIterator<'_> {
|
||||
TrackSegmentIterator::new(self)
|
||||
}
|
||||
@@ -248,9 +252,10 @@ mod tests {
|
||||
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);
|
||||
chunk.trkpt.push(Trackpoint {
|
||||
ele: count as f64,
|
||||
..Default::default()
|
||||
});
|
||||
count += 1;
|
||||
}
|
||||
trkseg.push(chunk);
|
||||
@@ -367,7 +372,7 @@ mod tests {
|
||||
fn test_index_out_of_bounds_1() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
trkseg[trkseg.len()].ele;
|
||||
let _ = trkseg[trkseg.len()];
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -375,12 +380,11 @@ mod tests {
|
||||
fn test_index_out_of_bounds_2() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
trkseg[TrackSegmentIndex {
|
||||
let _ = trkseg[TrackSegmentIndex {
|
||||
chunk: trkseg.chunks.len(),
|
||||
pos: 0,
|
||||
flat: 0,
|
||||
}]
|
||||
.ele;
|
||||
}];
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -38,13 +38,11 @@ enum GPXElement {
|
||||
|
||||
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(),
|
||||
_ => (),
|
||||
}
|
||||
for attr in attributes.flatten() {
|
||||
match attr.key.as_ref() {
|
||||
"lat" => coordinates.lat = attr.value.parse().unwrap_or_default(),
|
||||
"lon" => coordinates.lng = attr.value.parse().unwrap_or_default(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
coordinates
|
||||
@@ -68,11 +66,9 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||
"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();
|
||||
}
|
||||
for attr in e.attributes().flatten() {
|
||||
if attr.key.as_ref() == "href" {
|
||||
link.href = attr.value.to_string();
|
||||
}
|
||||
}
|
||||
stack.push(GPXElement::Link(link));
|
||||
@@ -83,14 +79,16 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||
stack.push(GPXElement::Segment(TrackSegment::default()));
|
||||
}
|
||||
"trkpt" => {
|
||||
let mut trkpt = Trackpoint::default();
|
||||
trkpt.coordinates = parse_coordinates(e.attributes());
|
||||
stack.push(GPXElement::Trackpoint(trkpt));
|
||||
stack.push(GPXElement::Trackpoint(Trackpoint {
|
||||
coordinates: parse_coordinates(e.attributes()),
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
"wpt" => {
|
||||
let mut wpt = Waypoint::default();
|
||||
wpt.coordinates = parse_coordinates(e.attributes());
|
||||
stack.push(GPXElement::Waypoint(wpt));
|
||||
stack.push(GPXElement::Waypoint(Waypoint {
|
||||
coordinates: parse_coordinates(e.attributes()),
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
"ele" => stack.push(GPXElement::Elevation),
|
||||
"time" => stack.push(GPXElement::Time),
|
||||
@@ -109,8 +107,7 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||
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();
|
||||
gpx.wpt.push(Rc::new(std::mem::take(&mut wpt_chunk)));
|
||||
}
|
||||
}
|
||||
"metadata" => {
|
||||
@@ -143,24 +140,21 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||
}
|
||||
}
|
||||
"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);
|
||||
}
|
||||
if let Some(GPXElement::Segment(mut trkseg)) = stack.pop()
|
||||
&& let Some(GPXElement::Track(trk)) = stack.last_mut()
|
||||
{
|
||||
// `push` ignores the chunk if it is empty
|
||||
trkseg.push(std::mem::take(&mut trkpt_chunk));
|
||||
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();
|
||||
}
|
||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.pop()
|
||||
&& let Some(GPXElement::Segment(trkseg)) = stack.last_mut()
|
||||
{
|
||||
trkpt_chunk.trkpt.push(trkpt);
|
||||
if trkpt_chunk.is_full() {
|
||||
trkseg.push(std::mem::take(&mut trkpt_chunk));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,8 +162,7 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||
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();
|
||||
gpx.wpt.push(Rc::new(std::mem::take(&mut wpt_chunk)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,11 +216,8 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||
}
|
||||
Some(GPXElement::Source) => {
|
||||
stack.pop();
|
||||
match stack.last_mut() {
|
||||
Some(GPXElement::Track(trk)) => {
|
||||
trk.info.src = Some(e.to_string());
|
||||
}
|
||||
_ => (),
|
||||
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
|
||||
trk.info.src = Some(e.to_string());
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Text) => {
|
||||
@@ -251,9 +241,9 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||
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());
|
||||
}
|
||||
trkpt.time = DateTime::parse_from_rfc3339(e.as_ref())
|
||||
.ok()
|
||||
.map(|time| time.timestamp_millis());
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Temperature) => {
|
||||
@@ -282,11 +272,8 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||
}
|
||||
Some(GPXElement::Symbol) => {
|
||||
stack.pop();
|
||||
match stack.last_mut() {
|
||||
Some(GPXElement::Waypoint(wpt)) => {
|
||||
wpt.sym = Some(e.to_string());
|
||||
}
|
||||
_ => (),
|
||||
if let Some(GPXElement::Waypoint(wpt)) = stack.last_mut() {
|
||||
wpt.sym = Some(e.to_string());
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Type) => {
|
||||
@@ -303,31 +290,20 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||
}
|
||||
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);
|
||||
}
|
||||
_ => (),
|
||||
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
|
||||
trk.info.color = Some(format!("#{}", &*e));
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Opacity) => {
|
||||
stack.pop();
|
||||
match stack.last_mut() {
|
||||
Some(GPXElement::Track(trk)) => {
|
||||
trk.info.opacity = e.parse().ok();
|
||||
}
|
||||
_ => (),
|
||||
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
|
||||
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();
|
||||
}
|
||||
_ => (),
|
||||
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
|
||||
trk.info.width = e.parse().ok();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
@@ -343,16 +319,15 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn parse_data(name: &str) -> File {
|
||||
parse(&std::fs::read(format!("data/{name}.gpx")).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[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();
|
||||
let gpx = parse_data("simple");
|
||||
|
||||
assert_eq!(gpx.info.name, "simple");
|
||||
assert!(gpx.info.desc.is_some_and(|d| d == "description"));
|
||||
@@ -392,10 +367,7 @@ mod tests {
|
||||
|
||||
#[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();
|
||||
let gpx = parse_data("with_tracks");
|
||||
|
||||
assert_eq!(gpx.trk.len(), 2);
|
||||
let trk = &gpx.trk[0];
|
||||
@@ -425,10 +397,7 @@ mod tests {
|
||||
|
||||
#[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();
|
||||
let gpx = parse_data("with_segments");
|
||||
|
||||
assert_eq!(gpx.trk.len(), 1);
|
||||
let trk = &gpx.trk[0];
|
||||
@@ -451,10 +420,7 @@ mod tests {
|
||||
|
||||
#[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();
|
||||
let gpx = parse_data("with_tracks_and_segments");
|
||||
|
||||
assert_eq!(gpx.trk.len(), 2);
|
||||
let trk = &gpx.trk[0];
|
||||
@@ -494,10 +460,7 @@ mod tests {
|
||||
|
||||
#[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();
|
||||
let gpx = parse_data("with_waypoint");
|
||||
|
||||
assert_eq!(gpx.wpt.len(), 1);
|
||||
let chunk = &gpx.wpt[0];
|
||||
@@ -526,106 +489,85 @@ mod tests {
|
||||
|
||||
#[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();
|
||||
let gpx = parse_data("with_time");
|
||||
|
||||
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);
|
||||
assert!(!trkseg.is_empty());
|
||||
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();
|
||||
let gpx = parse_data("with_hr");
|
||||
|
||||
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);
|
||||
assert!(!trkseg.is_empty());
|
||||
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();
|
||||
let gpx = parse_data("with_cad");
|
||||
|
||||
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);
|
||||
assert!(!trkseg.is_empty());
|
||||
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();
|
||||
let gpx = parse_data("with_power_1");
|
||||
|
||||
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);
|
||||
assert!(!trkseg.is_empty());
|
||||
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();
|
||||
let gpx = parse_data("with_power_2");
|
||||
|
||||
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);
|
||||
assert!(!trkseg.is_empty());
|
||||
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();
|
||||
let gpx = parse_data("with_temp");
|
||||
|
||||
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);
|
||||
assert!(!trkseg.is_empty());
|
||||
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();
|
||||
let gpx = parse_data("with_style");
|
||||
|
||||
assert_eq!(gpx.trk.len(), 1);
|
||||
let trk = &gpx.trk[0];
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
mod action;
|
||||
mod algorithm;
|
||||
pub(crate) mod gpx;
|
||||
mod io;
|
||||
pub(crate) mod statistics;
|
||||
mod utils;
|
||||
|
||||
pub use action::*;
|
||||
pub use algorithm::*;
|
||||
pub use gpx::*;
|
||||
pub use io::*;
|
||||
|
||||
@@ -20,7 +20,7 @@ impl Statistics {
|
||||
|
||||
pub fn compute(trkseg: &TrackSegment) -> Self {
|
||||
let mut stats = Self::default();
|
||||
if trkseg.len() == 0 {
|
||||
if trkseg.is_empty() {
|
||||
return stats;
|
||||
}
|
||||
|
||||
@@ -42,18 +42,18 @@ impl Statistics {
|
||||
self.update_time_bounds(cur.time);
|
||||
self.update_bounds(cur.coordinates);
|
||||
self.local
|
||||
.push(TrackpointStatistics::from_partial_stats(&self));
|
||||
.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);
|
||||
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 {
|
||||
if (0.5..=1500.0).contains(&speed) {
|
||||
self.global.moving_distance = self
|
||||
.global
|
||||
.moving_distance
|
||||
@@ -72,7 +72,7 @@ impl Statistics {
|
||||
self.global.start_time = Some(time);
|
||||
}
|
||||
self.global.end_time = Some(time);
|
||||
self.global.total_time = time_diff(&self.global.end_time, &self.global.start_time);
|
||||
self.global.total_time = time_diff(self.global.end_time, self.global.start_time);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,10 +86,10 @@ impl Statistics {
|
||||
trkseg.first_index(),
|
||||
trkseg.last_index(),
|
||||
Some(10000),
|
||||
|i, j| time_diff(&trkseg[i].time, &trkseg[j].time),
|
||||
|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| {
|
||||
time_diff(trkseg[right].time, trkseg[left].time).map(|t| {
|
||||
speed(
|
||||
self.local[right.flat].total_distance
|
||||
- self.local[left.flat].total_distance,
|
||||
@@ -370,16 +370,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_global_merge() {
|
||||
let mut a = GlobalStatistics::default();
|
||||
a.total_distance = 1.0;
|
||||
a.elevation_gain = 10.0;
|
||||
a.moving_time = Some(5);
|
||||
let mut a = GlobalStatistics {
|
||||
total_distance: 1.0,
|
||||
elevation_gain: 10.0,
|
||||
moving_time: Some(5),
|
||||
..Default::default()
|
||||
};
|
||||
a.bounds.extend(crate::LngLat { lng: 0.0, lat: 0.0 });
|
||||
let mut b = GlobalStatistics::default();
|
||||
b.total_distance = 2.0;
|
||||
b.elevation_loss = 4.0;
|
||||
b.moving_distance = Some(1.5);
|
||||
b.moving_time = Some(7);
|
||||
let mut b = GlobalStatistics {
|
||||
total_distance: 2.0,
|
||||
elevation_loss: 4.0,
|
||||
moving_distance: Some(1.5),
|
||||
moving_time: Some(7),
|
||||
..Default::default()
|
||||
};
|
||||
b.bounds.extend(crate::LngLat { lng: 2.0, lat: 3.0 });
|
||||
|
||||
a.merge(&b);
|
||||
@@ -390,14 +394,18 @@ mod tests {
|
||||
assert_eq!(a.moving_time, Some(12));
|
||||
assert_eq!(a.total_time, None);
|
||||
|
||||
let mut c = GlobalStatistics::default();
|
||||
c.start_time = Some(1_000);
|
||||
c.end_time = Some(4_000);
|
||||
c.total_time = Some(3_000);
|
||||
let mut d = GlobalStatistics::default();
|
||||
d.start_time = Some(10_000);
|
||||
d.end_time = Some(12_000);
|
||||
d.total_time = Some(2_000);
|
||||
let mut c = GlobalStatistics {
|
||||
start_time: Some(1_000),
|
||||
end_time: Some(4_000),
|
||||
total_time: Some(3_000),
|
||||
..Default::default()
|
||||
};
|
||||
let d = GlobalStatistics {
|
||||
start_time: Some(10_000),
|
||||
end_time: Some(12_000),
|
||||
total_time: Some(2_000),
|
||||
..Default::default()
|
||||
};
|
||||
c.merge(&d);
|
||||
// 3 s + 2 s, the gap between the two is ignored
|
||||
assert_eq!(c.total_time, Some(5_000));
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::f64::consts::PI;
|
||||
|
||||
use crate::LngLat;
|
||||
|
||||
static TO_RADIANS: f64 = PI / 180.0;
|
||||
static EARTH_RADIUS: f64 = 6371.0088;
|
||||
const TO_RADIANS: f64 = PI / 180.0;
|
||||
const 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 {
|
||||
@@ -18,16 +18,13 @@ pub fn distance(p1: LngLat, p2: LngLat) -> f64 {
|
||||
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,
|
||||
}
|
||||
pub fn time_diff(a: Option<i64>, b: Option<i64>) -> Option<i32> {
|
||||
Some((a? - b?) as i32)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
distance / (time as f64 / 3_600_000.0)
|
||||
}
|
||||
|
||||
pub fn slope(ele: f64, distance: f64) -> f64 {
|
||||
@@ -38,88 +35,78 @@ pub fn slope(ele: f64, distance: f64) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
static METERS_PER_LATITUDE_DEGREE: f64 = 111320.0;
|
||||
const 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
|
||||
/// Approximate planar projection, in meters, around the latitude of `origin` (ignores the
|
||||
/// curvature of the earth).
|
||||
struct Planar {
|
||||
meters_per_longitude_degree: f64,
|
||||
}
|
||||
|
||||
// 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);
|
||||
impl Planar {
|
||||
fn around(origin: LngLat) -> Self {
|
||||
Self {
|
||||
meters_per_longitude_degree: (origin.lat * TO_RADIANS).cos()
|
||||
* METERS_PER_LATITUDE_DEGREE,
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
fn to_meters(&self, p: LngLat) -> (f64, f64) {
|
||||
(
|
||||
p.lng * self.meters_per_longitude_degree,
|
||||
p.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
|
||||
fn to_degrees(&self, (x, y): (f64, f64)) -> LngLat {
|
||||
LngLat {
|
||||
lng: proj_x / meters_per_longitude_degree,
|
||||
lat: proj_y / METERS_PER_LATITUDE_DEGREE,
|
||||
lng: x / self.meters_per_longitude_degree,
|
||||
lat: 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 {
|
||||
/// Position, between 0 (at `(x1, y1)`) and 1 (at `(x2, y2)`), of the point of the segment that is
|
||||
/// the closest to `(x3, y3)`. A degenerate segment (same ends) is a point.
|
||||
fn closest_on_segment(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()
|
||||
0.0
|
||||
} 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()
|
||||
(((x3 - x1) * dx + (y3 - y1) * dy) / segment_length_squared).clamp(0.0, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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).
|
||||
pub fn projected(p1: LngLat, p2: LngLat, p3: LngLat) -> LngLat {
|
||||
let planar = Planar::around(p1);
|
||||
let (x1, y1) = planar.to_meters(p1);
|
||||
let (x2, y2) = planar.to_meters(p2);
|
||||
let (x3, y3) = planar.to_meters(p3);
|
||||
let t = closest_on_segment(x1, y1, x2, y2, x3, y3);
|
||||
planar.to_degrees((x1 + t * (x2 - x1), y1 + t * (y2 - y1)))
|
||||
}
|
||||
|
||||
/// 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).
|
||||
pub fn crossarc_lnglat(p1: LngLat, p2: LngLat, p3: LngLat) -> f64 {
|
||||
let planar = Planar::around(p1);
|
||||
let (x1, y1) = planar.to_meters(p1);
|
||||
let (x2, y2) = planar.to_meters(p2);
|
||||
let (x3, y3) = planar.to_meters(p3);
|
||||
crossarc(x1, y1, x2, y2, x3, y3)
|
||||
}
|
||||
|
||||
/// Distance from the point `(x3, y3)` to the segment `(x1, y1)`-`(x2, y2)`, in the units of the
|
||||
/// coordinates (planar geometry).
|
||||
pub fn crossarc(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> f64 {
|
||||
let t = closest_on_segment(x1, y1, x2, y2, x3, y3);
|
||||
(x3 - (x1 + t * (x2 - x1))).hypot(y3 - (y1 + t * (y2 - y1)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -145,10 +132,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_time_diff() {
|
||||
assert_eq!(time_diff(&Some(5000), &Some(2000)), Some(3000));
|
||||
assert_eq!(time_diff(&Some(2000), &Some(5000)), Some(-3000));
|
||||
assert_eq!(time_diff(&None, &Some(1)), None);
|
||||
assert_eq!(time_diff(&Some(1), &None), None);
|
||||
assert_eq!(time_diff(Some(5000), Some(2000)), Some(3000));
|
||||
assert_eq!(time_diff(Some(2000), Some(5000)), Some(-3000));
|
||||
assert_eq!(time_diff(None, Some(1)), None);
|
||||
assert_eq!(time_diff(Some(1), None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -194,12 +194,11 @@ fn copy_track(track: &Track) -> Track {
|
||||
}
|
||||
|
||||
fn copy_file(file: &File) -> File {
|
||||
let mut copy = File {
|
||||
File {
|
||||
id: Default::default(),
|
||||
trk: file.trk.iter().map(copy_track).collect(),
|
||||
..file.clone()
|
||||
};
|
||||
copy
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -25,12 +25,10 @@ impl CoordinatesCache {
|
||||
.get(&file.id)
|
||||
.is_none_or(|(r, _)| *r != file.wpt_rev_id)
|
||||
{
|
||||
let coordinates = file
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.flat_map(|wpt| [wpt.coordinates.lng, wpt.coordinates.lat])
|
||||
.collect();
|
||||
let mut coordinates = Vec::new();
|
||||
for wpt in file.wpt.iter().flat_map(|chunk| &chunk.wpt) {
|
||||
coordinates.extend([wpt.coordinates.lng, wpt.coordinates.lat]);
|
||||
}
|
||||
self.waypoints
|
||||
.insert(file.id, (file.wpt_rev_id, coordinates));
|
||||
}
|
||||
@@ -41,10 +39,10 @@ impl CoordinatesCache {
|
||||
.get(&seg.id)
|
||||
.is_none_or(|(r, _)| *r != seg.rev_id)
|
||||
{
|
||||
let coordinates = seg
|
||||
.iter()
|
||||
.flat_map(|trkpt| [trkpt.coordinates.lng, trkpt.coordinates.lat])
|
||||
.collect();
|
||||
let mut coordinates = Vec::with_capacity(seg.len() * 2);
|
||||
for trkpt in seg.iter() {
|
||||
coordinates.extend([trkpt.coordinates.lng, trkpt.coordinates.lat]);
|
||||
}
|
||||
self.segments.insert(seg.id, (seg.rev_id, coordinates));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ pub struct FileStructureCache {
|
||||
|
||||
impl FileStructureCache {
|
||||
/// Brings the cache in line with `files`, given what changed since the last update.
|
||||
pub fn update(&mut self, files: Option<&Files>, diff: &Option<Diff>) {
|
||||
pub fn update(&mut self, files: Option<&Files>, diff: Option<&Diff>) {
|
||||
if let Some(diff) = diff {
|
||||
for id in diff.removed.iter() {
|
||||
self.map.remove(id);
|
||||
@@ -48,7 +48,7 @@ mod tests {
|
||||
let mut cache = FileStructureCache::default();
|
||||
cache.update(
|
||||
Some(&fx.files),
|
||||
&Some(Diff {
|
||||
Some(&Diff {
|
||||
added: vec![a, b],
|
||||
..Default::default()
|
||||
}),
|
||||
@@ -60,7 +60,7 @@ mod tests {
|
||||
fx.files.insert(a, Rc::new(file));
|
||||
cache.update(
|
||||
Some(&fx.files),
|
||||
&Some(Diff {
|
||||
Some(&Diff {
|
||||
modified: vec![a],
|
||||
..Default::default()
|
||||
}),
|
||||
@@ -71,7 +71,7 @@ mod tests {
|
||||
fx.files.remove(&b);
|
||||
cache.update(
|
||||
Some(&fx.files),
|
||||
&Some(Diff {
|
||||
Some(&Diff {
|
||||
removed: vec![b],
|
||||
..Default::default()
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// TODO license in every file
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{
|
||||
Action, Apply, Command, CoordinatesCache, Diff, FileId, FileOrder, FileStructure,
|
||||
FileStructureCache, Selection, Stack, State, StatisticsBuffer, StatisticsCache, TrackSegmentId,
|
||||
@@ -37,8 +35,8 @@ impl Engine {
|
||||
}
|
||||
|
||||
/// Which files the last action added, removed or modified.
|
||||
pub fn last_diff(&self) -> &Option<Diff> {
|
||||
&self.diff
|
||||
pub fn last_diff(&self) -> Option<&Diff> {
|
||||
self.diff.as_ref()
|
||||
}
|
||||
|
||||
/// Whether the last action changed the order of the files.
|
||||
@@ -143,7 +141,7 @@ impl Engine {
|
||||
}
|
||||
self.statistics_cache.update(current);
|
||||
self.coordinates_cache.update(current);
|
||||
self.structure_cache.update(current, &self.diff);
|
||||
self.structure_cache.update(current, self.diff.as_ref());
|
||||
self.statistics_buffer
|
||||
.update(&self.statistics_cache.get(current, &self.selection));
|
||||
}
|
||||
@@ -343,18 +341,14 @@ mod tests {
|
||||
assert!(engine.order().is_empty());
|
||||
load(&mut engine, "data/simple.gpx");
|
||||
let loaded_id = engine.order()[0];
|
||||
let diff = engine.last_diff();
|
||||
assert!(diff.is_some());
|
||||
let diff = diff.as_ref().unwrap();
|
||||
let diff = engine.last_diff().unwrap();
|
||||
assert_eq!(diff.added, vec![loaded_id]);
|
||||
assert!(engine.order_changed());
|
||||
|
||||
new(&mut engine, "empty");
|
||||
let empty_id = engine.order()[1];
|
||||
// only the new file is reported, the other one is not recomputed
|
||||
let diff = engine.last_diff();
|
||||
assert!(diff.is_some());
|
||||
let diff = diff.as_ref().unwrap();
|
||||
let diff = engine.last_diff().unwrap();
|
||||
assert_eq!(diff.added, vec![empty_id]);
|
||||
assert!(diff.modified.is_empty() && diff.removed.is_empty());
|
||||
assert_eq!(engine.file_structure(&empty_id).unwrap().name, "empty");
|
||||
@@ -384,9 +378,7 @@ mod tests {
|
||||
|
||||
// undo removes the empty file, redoing the load removes the other one too
|
||||
assert!(engine.execute(Action::Undo));
|
||||
let diff = engine.last_diff();
|
||||
assert!(diff.is_some());
|
||||
let diff = diff.as_ref().unwrap();
|
||||
let diff = engine.last_diff().unwrap();
|
||||
assert_eq!(diff.removed, vec![empty_id]);
|
||||
assert!(engine.order_changed());
|
||||
assert!(engine.file_structure(&empty_id).is_none());
|
||||
|
||||
@@ -67,18 +67,12 @@ impl Stack {
|
||||
}
|
||||
|
||||
fn get(&self, index: usize) -> Option<&StackEntry> {
|
||||
if index < self.entries.len() {
|
||||
Some(&self.entries[index])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
self.entries.get(index)
|
||||
}
|
||||
|
||||
fn push(&mut self, entry: StackEntry) {
|
||||
if let Some(i) = self.index {
|
||||
if i + 1 < self.entries.len() {
|
||||
self.entries.truncate(i + 1);
|
||||
}
|
||||
self.entries.truncate(i + 1);
|
||||
}
|
||||
|
||||
self.entries.push(entry);
|
||||
@@ -103,7 +97,7 @@ impl Stack {
|
||||
for id in prev_ids.intersection(&cur_ids) {
|
||||
let before = prev.map_or_default(|e| e.get(id));
|
||||
let after = cur.map_or_default(|e| e.get(id));
|
||||
if before != after {
|
||||
if !before.zip(after).is_some_and(|(b, a)| Rc::ptr_eq(b, a)) && before != after {
|
||||
modified.push(*id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user