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;
|
mod smooth;
|
||||||
|
|
||||||
pub use simplify::*;
|
pub use simplify::*;
|
||||||
pub use smooth::*;
|
|
||||||
|
|||||||
@@ -59,13 +59,11 @@ fn ramer_douglas_peucker_helper<F>(
|
|||||||
cur = trkseg.next_index(cur);
|
cur = trkseg.next_index(cur);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(idx) = max_idx {
|
if let Some(idx) = max_idx.filter(|_| max_dist > epsilon) {
|
||||||
if max_dist > epsilon {
|
|
||||||
ramer_douglas_peucker_helper(trkseg, start, idx, mapping, epsilon, indices);
|
ramer_douglas_peucker_helper(trkseg, start, idx, mapping, epsilon, indices);
|
||||||
indices.push(idx);
|
indices.push(idx);
|
||||||
ramer_douglas_peucker_helper(trkseg, idx, end, mapping, epsilon, indices);
|
ramer_douglas_peucker_helper(trkseg, idx, end, mapping, epsilon, indices);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ macro_rules! for_each_window {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{TrackSegment, Trackpoint, TrackpointChunk, for_each_window};
|
use crate::{TrackSegment, Trackpoint, TrackpointChunk};
|
||||||
|
|
||||||
fn segment(eles: &[f64]) -> TrackSegment {
|
fn segment(eles: &[f64]) -> TrackSegment {
|
||||||
let mut trkseg = TrackSegment::default();
|
let mut trkseg = TrackSegment::default();
|
||||||
@@ -116,7 +116,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_window_sub_range() {
|
fn test_window_sub_range() {
|
||||||
let trkseg = segment(&vec![0.0; 10]);
|
let trkseg = segment(&[0.0; 10]);
|
||||||
let mut centers = vec![];
|
let mut centers = vec![];
|
||||||
let mut first = trkseg.first_index();
|
let mut first = trkseg.first_index();
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::{Trackpoint, Waypoint};
|
use crate::{Trackpoint, Waypoint};
|
||||||
|
|
||||||
static MAX_TRKPT_CHUNK_SIZE: usize = 4096;
|
const MAX_TRKPT_CHUNK_SIZE: usize = 4096;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct TrackpointChunkId(Uuid);
|
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)]
|
#[derive(Debug, Default)]
|
||||||
pub struct WaypointChunk {
|
pub struct WaypointChunk {
|
||||||
@@ -91,9 +91,10 @@ mod tests {
|
|||||||
let a = TrackpointChunk::default();
|
let a = TrackpointChunk::default();
|
||||||
let b = TrackpointChunk::default();
|
let b = TrackpointChunk::default();
|
||||||
assert_ne!(a, b);
|
assert_ne!(a, b);
|
||||||
let mut c = TrackpointChunk::default();
|
let c = TrackpointChunk {
|
||||||
c.id = TrackpointChunkId(a.id.0);
|
id: TrackpointChunkId(a.id.0),
|
||||||
c.trkpt.push(Trackpoint::default());
|
trkpt: vec![Trackpoint::default()],
|
||||||
|
};
|
||||||
assert_eq!(a, c);
|
assert_eq!(a, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,10 @@ impl TrackSegment {
|
|||||||
self.cumul_length.last().copied().unwrap_or_default()
|
self.cumul_length.last().copied().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.chunks.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn iter(&self) -> TrackSegmentIterator<'_> {
|
pub fn iter(&self) -> TrackSegmentIterator<'_> {
|
||||||
TrackSegmentIterator::new(self)
|
TrackSegmentIterator::new(self)
|
||||||
}
|
}
|
||||||
@@ -248,9 +252,10 @@ mod tests {
|
|||||||
for n in 1..=nb_chunks {
|
for n in 1..=nb_chunks {
|
||||||
let mut chunk = TrackpointChunk::default();
|
let mut chunk = TrackpointChunk::default();
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
let mut trkpt = Trackpoint::default();
|
chunk.trkpt.push(Trackpoint {
|
||||||
trkpt.ele = count as f64;
|
ele: count as f64,
|
||||||
chunk.trkpt.push(trkpt);
|
..Default::default()
|
||||||
|
});
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
trkseg.push(chunk);
|
trkseg.push(chunk);
|
||||||
@@ -367,7 +372,7 @@ mod tests {
|
|||||||
fn test_index_out_of_bounds_1() {
|
fn test_index_out_of_bounds_1() {
|
||||||
let nb_chunks = 10;
|
let nb_chunks = 10;
|
||||||
let trkseg = create_track_segment(nb_chunks);
|
let trkseg = create_track_segment(nb_chunks);
|
||||||
trkseg[trkseg.len()].ele;
|
let _ = trkseg[trkseg.len()];
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -375,12 +380,11 @@ mod tests {
|
|||||||
fn test_index_out_of_bounds_2() {
|
fn test_index_out_of_bounds_2() {
|
||||||
let nb_chunks = 10;
|
let nb_chunks = 10;
|
||||||
let trkseg = create_track_segment(nb_chunks);
|
let trkseg = create_track_segment(nb_chunks);
|
||||||
trkseg[TrackSegmentIndex {
|
let _ = trkseg[TrackSegmentIndex {
|
||||||
chunk: trkseg.chunks.len(),
|
chunk: trkseg.chunks.len(),
|
||||||
pos: 0,
|
pos: 0,
|
||||||
flat: 0,
|
flat: 0,
|
||||||
}]
|
}];
|
||||||
.ele;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -38,15 +38,13 @@ enum GPXElement {
|
|||||||
|
|
||||||
fn parse_coordinates(attributes: Attributes<'_>) -> LngLat {
|
fn parse_coordinates(attributes: Attributes<'_>) -> LngLat {
|
||||||
let mut coordinates = LngLat::default();
|
let mut coordinates = LngLat::default();
|
||||||
for attr in attributes {
|
for attr in attributes.flatten() {
|
||||||
if let Ok(attr) = attr {
|
|
||||||
match attr.key.as_ref() {
|
match attr.key.as_ref() {
|
||||||
"lat" => coordinates.lat = attr.value.parse().unwrap_or_default(),
|
"lat" => coordinates.lat = attr.value.parse().unwrap_or_default(),
|
||||||
"lon" => coordinates.lng = attr.value.parse().unwrap_or_default(),
|
"lon" => coordinates.lng = attr.value.parse().unwrap_or_default(),
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
coordinates
|
coordinates
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,13 +66,11 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
|||||||
"author" => stack.push(GPXElement::Author(Author::default())),
|
"author" => stack.push(GPXElement::Author(Author::default())),
|
||||||
"link" => {
|
"link" => {
|
||||||
let mut link = Link::default();
|
let mut link = Link::default();
|
||||||
for attr in e.attributes() {
|
for attr in e.attributes().flatten() {
|
||||||
if let Ok(attr) = attr {
|
|
||||||
if attr.key.as_ref() == "href" {
|
if attr.key.as_ref() == "href" {
|
||||||
link.href = attr.value.to_string();
|
link.href = attr.value.to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
stack.push(GPXElement::Link(link));
|
stack.push(GPXElement::Link(link));
|
||||||
}
|
}
|
||||||
"text" => stack.push(GPXElement::Text),
|
"text" => stack.push(GPXElement::Text),
|
||||||
@@ -83,14 +79,16 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
|||||||
stack.push(GPXElement::Segment(TrackSegment::default()));
|
stack.push(GPXElement::Segment(TrackSegment::default()));
|
||||||
}
|
}
|
||||||
"trkpt" => {
|
"trkpt" => {
|
||||||
let mut trkpt = Trackpoint::default();
|
stack.push(GPXElement::Trackpoint(Trackpoint {
|
||||||
trkpt.coordinates = parse_coordinates(e.attributes());
|
coordinates: parse_coordinates(e.attributes()),
|
||||||
stack.push(GPXElement::Trackpoint(trkpt));
|
..Default::default()
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
"wpt" => {
|
"wpt" => {
|
||||||
let mut wpt = Waypoint::default();
|
stack.push(GPXElement::Waypoint(Waypoint {
|
||||||
wpt.coordinates = parse_coordinates(e.attributes());
|
coordinates: parse_coordinates(e.attributes()),
|
||||||
stack.push(GPXElement::Waypoint(wpt));
|
..Default::default()
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
"ele" => stack.push(GPXElement::Elevation),
|
"ele" => stack.push(GPXElement::Elevation),
|
||||||
"time" => stack.push(GPXElement::Time),
|
"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() {
|
Ok(Event::End(e)) => match e.name().as_ref() {
|
||||||
"gpx" => {
|
"gpx" => {
|
||||||
if !wpt_chunk.wpt.is_empty() {
|
if !wpt_chunk.wpt.is_empty() {
|
||||||
gpx.wpt.push(Rc::new(wpt_chunk));
|
gpx.wpt.push(Rc::new(std::mem::take(&mut wpt_chunk)));
|
||||||
wpt_chunk = WaypointChunk::default();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"metadata" => {
|
"metadata" => {
|
||||||
@@ -143,24 +140,21 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"trkseg" => {
|
"trkseg" => {
|
||||||
if let Some(GPXElement::Segment(mut trkseg)) = stack.pop() {
|
if let Some(GPXElement::Segment(mut trkseg)) = stack.pop()
|
||||||
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
|
&& let Some(GPXElement::Track(trk)) = stack.last_mut()
|
||||||
if !trkpt_chunk.trkpt.is_empty() {
|
{
|
||||||
trkseg.push(trkpt_chunk);
|
// `push` ignores the chunk if it is empty
|
||||||
trkpt_chunk = TrackpointChunk::default();
|
trkseg.push(std::mem::take(&mut trkpt_chunk));
|
||||||
}
|
|
||||||
trk.trkseg.push(trkseg);
|
trk.trkseg.push(trkseg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
"trkpt" => {
|
"trkpt" => {
|
||||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.pop() {
|
if let Some(GPXElement::Trackpoint(trkpt)) = stack.pop()
|
||||||
if let Some(GPXElement::Segment(trkseg)) = stack.last_mut() {
|
&& let Some(GPXElement::Segment(trkseg)) = stack.last_mut()
|
||||||
|
{
|
||||||
trkpt_chunk.trkpt.push(trkpt);
|
trkpt_chunk.trkpt.push(trkpt);
|
||||||
if trkpt_chunk.is_full() {
|
if trkpt_chunk.is_full() {
|
||||||
trkseg.push(trkpt_chunk);
|
trkseg.push(std::mem::take(&mut trkpt_chunk));
|
||||||
trkpt_chunk = TrackpointChunk::default();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,8 +162,7 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
|||||||
if let Some(GPXElement::Waypoint(wpt)) = stack.pop() {
|
if let Some(GPXElement::Waypoint(wpt)) = stack.pop() {
|
||||||
wpt_chunk.wpt.push(wpt);
|
wpt_chunk.wpt.push(wpt);
|
||||||
if wpt_chunk.is_full() {
|
if wpt_chunk.is_full() {
|
||||||
gpx.wpt.push(Rc::new(wpt_chunk));
|
gpx.wpt.push(Rc::new(std::mem::take(&mut wpt_chunk)));
|
||||||
wpt_chunk = WaypointChunk::default();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,12 +216,9 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
|||||||
}
|
}
|
||||||
Some(GPXElement::Source) => {
|
Some(GPXElement::Source) => {
|
||||||
stack.pop();
|
stack.pop();
|
||||||
match stack.last_mut() {
|
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
|
||||||
Some(GPXElement::Track(trk)) => {
|
|
||||||
trk.info.src = Some(e.to_string());
|
trk.info.src = Some(e.to_string());
|
||||||
}
|
}
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some(GPXElement::Text) => {
|
Some(GPXElement::Text) => {
|
||||||
stack.pop();
|
stack.pop();
|
||||||
@@ -251,9 +241,9 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
|||||||
Some(GPXElement::Time) => {
|
Some(GPXElement::Time) => {
|
||||||
stack.pop();
|
stack.pop();
|
||||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||||
if let Ok(time) = DateTime::parse_from_rfc3339(e.as_ref()) {
|
trkpt.time = DateTime::parse_from_rfc3339(e.as_ref())
|
||||||
trkpt.time = Some(time.timestamp_millis());
|
.ok()
|
||||||
}
|
.map(|time| time.timestamp_millis());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(GPXElement::Temperature) => {
|
Some(GPXElement::Temperature) => {
|
||||||
@@ -282,12 +272,9 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
|||||||
}
|
}
|
||||||
Some(GPXElement::Symbol) => {
|
Some(GPXElement::Symbol) => {
|
||||||
stack.pop();
|
stack.pop();
|
||||||
match stack.last_mut() {
|
if let Some(GPXElement::Waypoint(wpt)) = stack.last_mut() {
|
||||||
Some(GPXElement::Waypoint(wpt)) => {
|
|
||||||
wpt.sym = Some(e.to_string());
|
wpt.sym = Some(e.to_string());
|
||||||
}
|
}
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some(GPXElement::Type) => {
|
Some(GPXElement::Type) => {
|
||||||
stack.pop();
|
stack.pop();
|
||||||
@@ -303,32 +290,21 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
|||||||
}
|
}
|
||||||
Some(GPXElement::Color) => {
|
Some(GPXElement::Color) => {
|
||||||
stack.pop();
|
stack.pop();
|
||||||
match stack.last_mut() {
|
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
|
||||||
Some(GPXElement::Track(trk)) => {
|
trk.info.color = Some(format!("#{}", &*e));
|
||||||
let mut color = "#".to_string();
|
|
||||||
color.push_str(&e);
|
|
||||||
trk.info.color = Some(color);
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(GPXElement::Opacity) => {
|
Some(GPXElement::Opacity) => {
|
||||||
stack.pop();
|
stack.pop();
|
||||||
match stack.last_mut() {
|
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
|
||||||
Some(GPXElement::Track(trk)) => {
|
|
||||||
trk.info.opacity = e.parse().ok();
|
trk.info.opacity = e.parse().ok();
|
||||||
}
|
}
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some(GPXElement::Width) => {
|
Some(GPXElement::Width) => {
|
||||||
stack.pop();
|
stack.pop();
|
||||||
match stack.last_mut() {
|
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
|
||||||
Some(GPXElement::Track(trk)) => {
|
|
||||||
trk.info.width = e.parse().ok();
|
trk.info.width = e.parse().ok();
|
||||||
}
|
}
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
},
|
},
|
||||||
@@ -343,16 +319,15 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{fs::File, io::Read};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn parse_data(name: &str) -> File {
|
||||||
|
parse(&std::fs::read(format!("data/{name}.gpx")).unwrap()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_simple() {
|
fn test_parse_simple() {
|
||||||
let mut f = File::open("data/simple.gpx").unwrap();
|
let gpx = parse_data("simple");
|
||||||
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_eq!(gpx.info.name, "simple");
|
||||||
assert!(gpx.info.desc.is_some_and(|d| d == "description"));
|
assert!(gpx.info.desc.is_some_and(|d| d == "description"));
|
||||||
@@ -392,10 +367,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_tracks() {
|
fn test_parse_tracks() {
|
||||||
let mut f = File::open("data/with_tracks.gpx").unwrap();
|
let gpx = parse_data("with_tracks");
|
||||||
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);
|
assert_eq!(gpx.trk.len(), 2);
|
||||||
let trk = &gpx.trk[0];
|
let trk = &gpx.trk[0];
|
||||||
@@ -425,10 +397,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_segments() {
|
fn test_parse_segments() {
|
||||||
let mut f = File::open("data/with_segments.gpx").unwrap();
|
let gpx = parse_data("with_segments");
|
||||||
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);
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
let trk = &gpx.trk[0];
|
let trk = &gpx.trk[0];
|
||||||
@@ -451,10 +420,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_tracks_and_segments() {
|
fn test_parse_tracks_and_segments() {
|
||||||
let mut f = File::open("data/with_tracks_and_segments.gpx").unwrap();
|
let gpx = parse_data("with_tracks_and_segments");
|
||||||
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);
|
assert_eq!(gpx.trk.len(), 2);
|
||||||
let trk = &gpx.trk[0];
|
let trk = &gpx.trk[0];
|
||||||
@@ -494,10 +460,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_waypoint() {
|
fn test_parse_waypoint() {
|
||||||
let mut f = File::open("data/with_waypoint.gpx").unwrap();
|
let gpx = parse_data("with_waypoint");
|
||||||
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);
|
assert_eq!(gpx.wpt.len(), 1);
|
||||||
let chunk = &gpx.wpt[0];
|
let chunk = &gpx.wpt[0];
|
||||||
@@ -526,106 +489,85 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_trackpoint_time() {
|
fn test_parse_trackpoint_time() {
|
||||||
let mut f = File::open("data/with_time.gpx").unwrap();
|
let gpx = parse_data("with_time");
|
||||||
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);
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
let trk = &gpx.trk[0];
|
let trk = &gpx.trk[0];
|
||||||
assert_eq!(trk.trkseg.len(), 1);
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
let trkseg = &trk.trkseg[0];
|
let trkseg = &trk.trkseg[0];
|
||||||
assert!(trkseg.len() > 0);
|
assert!(!trkseg.is_empty());
|
||||||
let trkpt = &trkseg[0];
|
let trkpt = &trkseg[0];
|
||||||
assert!(trkpt.time.is_some_and(|t| t == 1704063600000));
|
assert!(trkpt.time.is_some_and(|t| t == 1704063600000));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_trackpoint_hr() {
|
fn test_parse_trackpoint_hr() {
|
||||||
let mut f = File::open("data/with_hr.gpx").unwrap();
|
let gpx = parse_data("with_hr");
|
||||||
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);
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
let trk = &gpx.trk[0];
|
let trk = &gpx.trk[0];
|
||||||
assert_eq!(trk.trkseg.len(), 1);
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
let trkseg = &trk.trkseg[0];
|
let trkseg = &trk.trkseg[0];
|
||||||
assert!(trkseg.len() > 0);
|
assert!(!trkseg.is_empty());
|
||||||
let trkpt = &trkseg[0];
|
let trkpt = &trkseg[0];
|
||||||
assert!(trkpt.hr.is_some_and(|h| h == 150));
|
assert!(trkpt.hr.is_some_and(|h| h == 150));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_trackpoint_cad() {
|
fn test_parse_trackpoint_cad() {
|
||||||
let mut f = File::open("data/with_cad.gpx").unwrap();
|
let gpx = parse_data("with_cad");
|
||||||
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);
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
let trk = &gpx.trk[0];
|
let trk = &gpx.trk[0];
|
||||||
assert_eq!(trk.trkseg.len(), 1);
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
let trkseg = &trk.trkseg[0];
|
let trkseg = &trk.trkseg[0];
|
||||||
assert!(trkseg.len() > 0);
|
assert!(!trkseg.is_empty());
|
||||||
let trkpt = &trkseg[0];
|
let trkpt = &trkseg[0];
|
||||||
assert!(trkpt.cad.is_some_and(|c| c == 80));
|
assert!(trkpt.cad.is_some_and(|c| c == 80));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_trackpoint_power_1() {
|
fn test_parse_trackpoint_power_1() {
|
||||||
let mut f = File::open("data/with_power_1.gpx").unwrap();
|
let gpx = parse_data("with_power_1");
|
||||||
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);
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
let trk = &gpx.trk[0];
|
let trk = &gpx.trk[0];
|
||||||
assert_eq!(trk.trkseg.len(), 1);
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
let trkseg = &trk.trkseg[0];
|
let trkseg = &trk.trkseg[0];
|
||||||
assert!(trkseg.len() > 0);
|
assert!(!trkseg.is_empty());
|
||||||
let trkpt = &trkseg[0];
|
let trkpt = &trkseg[0];
|
||||||
assert!(trkpt.power.is_some_and(|p| p == 200));
|
assert!(trkpt.power.is_some_and(|p| p == 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_trackpoint_power_2() {
|
fn test_parse_trackpoint_power_2() {
|
||||||
let mut f = File::open("data/with_power_2.gpx").unwrap();
|
let gpx = parse_data("with_power_2");
|
||||||
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);
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
let trk = &gpx.trk[0];
|
let trk = &gpx.trk[0];
|
||||||
assert_eq!(trk.trkseg.len(), 1);
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
let trkseg = &trk.trkseg[0];
|
let trkseg = &trk.trkseg[0];
|
||||||
assert!(trkseg.len() > 0);
|
assert!(!trkseg.is_empty());
|
||||||
let trkpt = &trkseg[0];
|
let trkpt = &trkseg[0];
|
||||||
assert!(trkpt.power.is_some_and(|p| p == 200));
|
assert!(trkpt.power.is_some_and(|p| p == 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_trackpoint_atemp() {
|
fn test_parse_trackpoint_atemp() {
|
||||||
let mut f = File::open("data/with_temp.gpx").unwrap();
|
let gpx = parse_data("with_temp");
|
||||||
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);
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
let trk = &gpx.trk[0];
|
let trk = &gpx.trk[0];
|
||||||
assert_eq!(trk.trkseg.len(), 1);
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
let trkseg = &trk.trkseg[0];
|
let trkseg = &trk.trkseg[0];
|
||||||
assert!(trkseg.len() > 0);
|
assert!(!trkseg.is_empty());
|
||||||
let trkpt = &trkseg[0];
|
let trkpt = &trkseg[0];
|
||||||
assert!(trkpt.atemp.is_some_and(|t| t == 21));
|
assert!(trkpt.atemp.is_some_and(|t| t == 21));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_track_style() {
|
fn test_parse_track_style() {
|
||||||
let mut f = File::open("data/with_style.gpx").unwrap();
|
let gpx = parse_data("with_style");
|
||||||
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);
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
let trk = &gpx.trk[0];
|
let trk = &gpx.trk[0];
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
mod action;
|
|
||||||
mod algorithm;
|
mod algorithm;
|
||||||
pub(crate) mod gpx;
|
pub(crate) mod gpx;
|
||||||
mod io;
|
mod io;
|
||||||
pub(crate) mod statistics;
|
pub(crate) mod statistics;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub use action::*;
|
|
||||||
pub use algorithm::*;
|
pub use algorithm::*;
|
||||||
pub use gpx::*;
|
pub use gpx::*;
|
||||||
pub use io::*;
|
pub use io::*;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ impl Statistics {
|
|||||||
|
|
||||||
pub fn compute(trkseg: &TrackSegment) -> Self {
|
pub fn compute(trkseg: &TrackSegment) -> Self {
|
||||||
let mut stats = Self::default();
|
let mut stats = Self::default();
|
||||||
if trkseg.len() == 0 {
|
if trkseg.is_empty() {
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,18 +42,18 @@ impl Statistics {
|
|||||||
self.update_time_bounds(cur.time);
|
self.update_time_bounds(cur.time);
|
||||||
self.update_bounds(cur.coordinates);
|
self.update_bounds(cur.coordinates);
|
||||||
self.local
|
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) {
|
fn accumulate_distance_and_time(&mut self, prev: &Trackpoint, cur: &Trackpoint) {
|
||||||
let dist = distance(prev.coordinates, cur.coordinates);
|
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;
|
self.global.total_distance += dist;
|
||||||
|
|
||||||
if let Some(time) = time {
|
if let Some(time) = time {
|
||||||
let speed = speed(dist, 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
|
self.global.moving_distance = self
|
||||||
.global
|
.global
|
||||||
.moving_distance
|
.moving_distance
|
||||||
@@ -72,7 +72,7 @@ impl Statistics {
|
|||||||
self.global.start_time = Some(time);
|
self.global.start_time = Some(time);
|
||||||
}
|
}
|
||||||
self.global.end_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.first_index(),
|
||||||
trkseg.last_index(),
|
trkseg.last_index(),
|
||||||
Some(10000),
|
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| {
|
|i, left, right| {
|
||||||
self.local[i.flat].speed =
|
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(
|
speed(
|
||||||
self.local[right.flat].total_distance
|
self.local[right.flat].total_distance
|
||||||
- self.local[left.flat].total_distance,
|
- self.local[left.flat].total_distance,
|
||||||
@@ -370,16 +370,20 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_global_merge() {
|
fn test_global_merge() {
|
||||||
let mut a = GlobalStatistics::default();
|
let mut a = GlobalStatistics {
|
||||||
a.total_distance = 1.0;
|
total_distance: 1.0,
|
||||||
a.elevation_gain = 10.0;
|
elevation_gain: 10.0,
|
||||||
a.moving_time = Some(5);
|
moving_time: Some(5),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
a.bounds.extend(crate::LngLat { lng: 0.0, lat: 0.0 });
|
a.bounds.extend(crate::LngLat { lng: 0.0, lat: 0.0 });
|
||||||
let mut b = GlobalStatistics::default();
|
let mut b = GlobalStatistics {
|
||||||
b.total_distance = 2.0;
|
total_distance: 2.0,
|
||||||
b.elevation_loss = 4.0;
|
elevation_loss: 4.0,
|
||||||
b.moving_distance = Some(1.5);
|
moving_distance: Some(1.5),
|
||||||
b.moving_time = Some(7);
|
moving_time: Some(7),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
b.bounds.extend(crate::LngLat { lng: 2.0, lat: 3.0 });
|
b.bounds.extend(crate::LngLat { lng: 2.0, lat: 3.0 });
|
||||||
|
|
||||||
a.merge(&b);
|
a.merge(&b);
|
||||||
@@ -390,14 +394,18 @@ mod tests {
|
|||||||
assert_eq!(a.moving_time, Some(12));
|
assert_eq!(a.moving_time, Some(12));
|
||||||
assert_eq!(a.total_time, None);
|
assert_eq!(a.total_time, None);
|
||||||
|
|
||||||
let mut c = GlobalStatistics::default();
|
let mut c = GlobalStatistics {
|
||||||
c.start_time = Some(1_000);
|
start_time: Some(1_000),
|
||||||
c.end_time = Some(4_000);
|
end_time: Some(4_000),
|
||||||
c.total_time = Some(3_000);
|
total_time: Some(3_000),
|
||||||
let mut d = GlobalStatistics::default();
|
..Default::default()
|
||||||
d.start_time = Some(10_000);
|
};
|
||||||
d.end_time = Some(12_000);
|
let d = GlobalStatistics {
|
||||||
d.total_time = Some(2_000);
|
start_time: Some(10_000),
|
||||||
|
end_time: Some(12_000),
|
||||||
|
total_time: Some(2_000),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
c.merge(&d);
|
c.merge(&d);
|
||||||
// 3 s + 2 s, the gap between the two is ignored
|
// 3 s + 2 s, the gap between the two is ignored
|
||||||
assert_eq!(c.total_time, Some(5_000));
|
assert_eq!(c.total_time, Some(5_000));
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use std::f64::consts::PI;
|
|||||||
|
|
||||||
use crate::LngLat;
|
use crate::LngLat;
|
||||||
|
|
||||||
static TO_RADIANS: f64 = PI / 180.0;
|
const TO_RADIANS: f64 = PI / 180.0;
|
||||||
static EARTH_RADIUS: f64 = 6371.0088;
|
const EARTH_RADIUS: f64 = 6371.0088;
|
||||||
|
|
||||||
/// Computes the distance in kilometers between two coordinates using the Haversine formula
|
/// Computes the distance in kilometers between two coordinates using the Haversine formula
|
||||||
pub fn distance(p1: LngLat, p2: LngLat) -> f64 {
|
pub fn distance(p1: LngLat, p2: LngLat) -> f64 {
|
||||||
@@ -18,16 +18,13 @@ pub fn distance(p1: LngLat, p2: LngLat) -> f64 {
|
|||||||
EARTH_RADIUS * c
|
EARTH_RADIUS * c
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn time_diff(a: &Option<i64>, b: &Option<i64>) -> Option<i32> {
|
pub fn time_diff(a: Option<i64>, b: Option<i64>) -> Option<i32> {
|
||||||
match (a, b) {
|
Some((a? - b?) as i32)
|
||||||
(Some(t1), Some(t2)) => Some((t1 - t2) as i32),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Computes the speed for a given distance in kilometers and a time in milliseconds
|
/// Computes the speed for a given distance in kilometers and a time in milliseconds
|
||||||
pub fn speed(distance: f64, time: i32) -> f64 {
|
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 {
|
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 {
|
/// Approximate planar projection, in meters, around the latitude of `origin` (ignores the
|
||||||
((latitude * PI) / 180.0).cos() * METERS_PER_LATITUDE_DEGREE
|
/// curvature of the earth).
|
||||||
|
struct Planar {
|
||||||
|
meters_per_longitude_degree: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculates the point on the line segment defined by p1 and p2
|
impl Planar {
|
||||||
// that is closest to the third point, p3.
|
fn around(origin: LngLat) -> Self {
|
||||||
// Uses simple planar geometry (ignores earth curvature).
|
Self {
|
||||||
fn projected(p1: LngLat, p2: LngLat, p3: LngLat) -> LngLat {
|
meters_per_longitude_degree: (origin.lat * TO_RADIANS).cos()
|
||||||
// Convert to meters using approximate scaling
|
* METERS_PER_LATITUDE_DEGREE,
|
||||||
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
|
fn to_meters(&self, p: LngLat) -> (f64, f64) {
|
||||||
/// between a line segment (defined by p1 and p2) and a third point, p3.
|
(
|
||||||
/// Uses simple planar geometry (ignores earth curvature).
|
p.lng * self.meters_per_longitude_degree,
|
||||||
fn crossarc_lnglat(p1: LngLat, p2: LngLat, p3: LngLat) -> f64 {
|
p.lat * METERS_PER_LATITUDE_DEGREE,
|
||||||
// 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,
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_degrees(&self, (x, y): (f64, f64)) -> LngLat {
|
||||||
|
LngLat {
|
||||||
|
lng: x / self.meters_per_longitude_degree,
|
||||||
|
lat: y / 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 dx = x2 - x1;
|
||||||
let dy = y2 - y1;
|
let dy = y2 - y1;
|
||||||
let segment_length_squared = dx * dx + dy * dy;
|
let segment_length_squared = dx * dx + dy * dy;
|
||||||
|
|
||||||
if segment_length_squared == 0.0 {
|
if segment_length_squared == 0.0 {
|
||||||
// p1 and p2 are the same point
|
0.0
|
||||||
((x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1)).sqrt()
|
|
||||||
} else {
|
} else {
|
||||||
// Project p3 onto the line defined by p1 - p2
|
(((x3 - x1) * dx + (y3 - y1) * dy) / segment_length_squared).clamp(0.0, 1.0)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -145,10 +132,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_time_diff() {
|
fn test_time_diff() {
|
||||||
assert_eq!(time_diff(&Some(5000), &Some(2000)), Some(3000));
|
assert_eq!(time_diff(Some(5000), Some(2000)), Some(3000));
|
||||||
assert_eq!(time_diff(&Some(2000), &Some(5000)), Some(-3000));
|
assert_eq!(time_diff(Some(2000), Some(5000)), Some(-3000));
|
||||||
assert_eq!(time_diff(&None, &Some(1)), None);
|
assert_eq!(time_diff(None, Some(1)), None);
|
||||||
assert_eq!(time_diff(&Some(1), &None), None);
|
assert_eq!(time_diff(Some(1), None), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -194,12 +194,11 @@ fn copy_track(track: &Track) -> Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn copy_file(file: &File) -> File {
|
fn copy_file(file: &File) -> File {
|
||||||
let mut copy = File {
|
File {
|
||||||
id: Default::default(),
|
id: Default::default(),
|
||||||
trk: file.trk.iter().map(copy_track).collect(),
|
trk: file.trk.iter().map(copy_track).collect(),
|
||||||
..file.clone()
|
..file.clone()
|
||||||
};
|
}
|
||||||
copy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -25,12 +25,10 @@ impl CoordinatesCache {
|
|||||||
.get(&file.id)
|
.get(&file.id)
|
||||||
.is_none_or(|(r, _)| *r != file.wpt_rev_id)
|
.is_none_or(|(r, _)| *r != file.wpt_rev_id)
|
||||||
{
|
{
|
||||||
let coordinates = file
|
let mut coordinates = Vec::new();
|
||||||
.wpt
|
for wpt in file.wpt.iter().flat_map(|chunk| &chunk.wpt) {
|
||||||
.iter()
|
coordinates.extend([wpt.coordinates.lng, wpt.coordinates.lat]);
|
||||||
.flat_map(|chunk| &chunk.wpt)
|
}
|
||||||
.flat_map(|wpt| [wpt.coordinates.lng, wpt.coordinates.lat])
|
|
||||||
.collect();
|
|
||||||
self.waypoints
|
self.waypoints
|
||||||
.insert(file.id, (file.wpt_rev_id, coordinates));
|
.insert(file.id, (file.wpt_rev_id, coordinates));
|
||||||
}
|
}
|
||||||
@@ -41,10 +39,10 @@ impl CoordinatesCache {
|
|||||||
.get(&seg.id)
|
.get(&seg.id)
|
||||||
.is_none_or(|(r, _)| *r != seg.rev_id)
|
.is_none_or(|(r, _)| *r != seg.rev_id)
|
||||||
{
|
{
|
||||||
let coordinates = seg
|
let mut coordinates = Vec::with_capacity(seg.len() * 2);
|
||||||
.iter()
|
for trkpt in seg.iter() {
|
||||||
.flat_map(|trkpt| [trkpt.coordinates.lng, trkpt.coordinates.lat])
|
coordinates.extend([trkpt.coordinates.lng, trkpt.coordinates.lat]);
|
||||||
.collect();
|
}
|
||||||
self.segments.insert(seg.id, (seg.rev_id, coordinates));
|
self.segments.insert(seg.id, (seg.rev_id, coordinates));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub struct FileStructureCache {
|
|||||||
|
|
||||||
impl FileStructureCache {
|
impl FileStructureCache {
|
||||||
/// Brings the cache in line with `files`, given what changed since the last update.
|
/// 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 {
|
if let Some(diff) = diff {
|
||||||
for id in diff.removed.iter() {
|
for id in diff.removed.iter() {
|
||||||
self.map.remove(id);
|
self.map.remove(id);
|
||||||
@@ -48,7 +48,7 @@ mod tests {
|
|||||||
let mut cache = FileStructureCache::default();
|
let mut cache = FileStructureCache::default();
|
||||||
cache.update(
|
cache.update(
|
||||||
Some(&fx.files),
|
Some(&fx.files),
|
||||||
&Some(Diff {
|
Some(&Diff {
|
||||||
added: vec![a, b],
|
added: vec![a, b],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
@@ -60,7 +60,7 @@ mod tests {
|
|||||||
fx.files.insert(a, Rc::new(file));
|
fx.files.insert(a, Rc::new(file));
|
||||||
cache.update(
|
cache.update(
|
||||||
Some(&fx.files),
|
Some(&fx.files),
|
||||||
&Some(Diff {
|
Some(&Diff {
|
||||||
modified: vec![a],
|
modified: vec![a],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
@@ -71,7 +71,7 @@ mod tests {
|
|||||||
fx.files.remove(&b);
|
fx.files.remove(&b);
|
||||||
cache.update(
|
cache.update(
|
||||||
Some(&fx.files),
|
Some(&fx.files),
|
||||||
&Some(Diff {
|
Some(&Diff {
|
||||||
removed: vec![b],
|
removed: vec![b],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
// TODO license in every file
|
// TODO license in every file
|
||||||
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Action, Apply, Command, CoordinatesCache, Diff, FileId, FileOrder, FileStructure,
|
Action, Apply, Command, CoordinatesCache, Diff, FileId, FileOrder, FileStructure,
|
||||||
FileStructureCache, Selection, Stack, State, StatisticsBuffer, StatisticsCache, TrackSegmentId,
|
FileStructureCache, Selection, Stack, State, StatisticsBuffer, StatisticsCache, TrackSegmentId,
|
||||||
@@ -37,8 +35,8 @@ impl Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Which files the last action added, removed or modified.
|
/// Which files the last action added, removed or modified.
|
||||||
pub fn last_diff(&self) -> &Option<Diff> {
|
pub fn last_diff(&self) -> Option<&Diff> {
|
||||||
&self.diff
|
self.diff.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the last action changed the order of the files.
|
/// Whether the last action changed the order of the files.
|
||||||
@@ -143,7 +141,7 @@ impl Engine {
|
|||||||
}
|
}
|
||||||
self.statistics_cache.update(current);
|
self.statistics_cache.update(current);
|
||||||
self.coordinates_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
|
self.statistics_buffer
|
||||||
.update(&self.statistics_cache.get(current, &self.selection));
|
.update(&self.statistics_cache.get(current, &self.selection));
|
||||||
}
|
}
|
||||||
@@ -343,18 +341,14 @@ mod tests {
|
|||||||
assert!(engine.order().is_empty());
|
assert!(engine.order().is_empty());
|
||||||
load(&mut engine, "data/simple.gpx");
|
load(&mut engine, "data/simple.gpx");
|
||||||
let loaded_id = engine.order()[0];
|
let loaded_id = engine.order()[0];
|
||||||
let diff = engine.last_diff();
|
let diff = engine.last_diff().unwrap();
|
||||||
assert!(diff.is_some());
|
|
||||||
let diff = diff.as_ref().unwrap();
|
|
||||||
assert_eq!(diff.added, vec![loaded_id]);
|
assert_eq!(diff.added, vec![loaded_id]);
|
||||||
assert!(engine.order_changed());
|
assert!(engine.order_changed());
|
||||||
|
|
||||||
new(&mut engine, "empty");
|
new(&mut engine, "empty");
|
||||||
let empty_id = engine.order()[1];
|
let empty_id = engine.order()[1];
|
||||||
// only the new file is reported, the other one is not recomputed
|
// only the new file is reported, the other one is not recomputed
|
||||||
let diff = engine.last_diff();
|
let diff = engine.last_diff().unwrap();
|
||||||
assert!(diff.is_some());
|
|
||||||
let diff = diff.as_ref().unwrap();
|
|
||||||
assert_eq!(diff.added, vec![empty_id]);
|
assert_eq!(diff.added, vec![empty_id]);
|
||||||
assert!(diff.modified.is_empty() && diff.removed.is_empty());
|
assert!(diff.modified.is_empty() && diff.removed.is_empty());
|
||||||
assert_eq!(engine.file_structure(&empty_id).unwrap().name, "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
|
// undo removes the empty file, redoing the load removes the other one too
|
||||||
assert!(engine.execute(Action::Undo));
|
assert!(engine.execute(Action::Undo));
|
||||||
let diff = engine.last_diff();
|
let diff = engine.last_diff().unwrap();
|
||||||
assert!(diff.is_some());
|
|
||||||
let diff = diff.as_ref().unwrap();
|
|
||||||
assert_eq!(diff.removed, vec![empty_id]);
|
assert_eq!(diff.removed, vec![empty_id]);
|
||||||
assert!(engine.order_changed());
|
assert!(engine.order_changed());
|
||||||
assert!(engine.file_structure(&empty_id).is_none());
|
assert!(engine.file_structure(&empty_id).is_none());
|
||||||
|
|||||||
@@ -67,19 +67,13 @@ impl Stack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, index: usize) -> Option<&StackEntry> {
|
fn get(&self, index: usize) -> Option<&StackEntry> {
|
||||||
if index < self.entries.len() {
|
self.entries.get(index)
|
||||||
Some(&self.entries[index])
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push(&mut self, entry: StackEntry) {
|
fn push(&mut self, entry: StackEntry) {
|
||||||
if let Some(i) = self.index {
|
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);
|
self.entries.push(entry);
|
||||||
self.index = Some(self.entries.len() - 1);
|
self.index = Some(self.entries.len() - 1);
|
||||||
@@ -103,7 +97,7 @@ impl Stack {
|
|||||||
for id in prev_ids.intersection(&cur_ids) {
|
for id in prev_ids.intersection(&cur_ids) {
|
||||||
let before = prev.map_or_default(|e| e.get(id));
|
let before = prev.map_or_default(|e| e.get(id));
|
||||||
let after = cur.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);
|
modified.push(*id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-19
@@ -49,13 +49,15 @@ impl From<CleanType> for engine::CleanType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static ENGINE: RefCell<Option<Engine>> = RefCell::new(None);
|
static ENGINE: RefCell<Option<Engine>> = const { RefCell::new(None) };
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute(action: Action) -> bool {
|
fn execute(action: Action) -> bool {
|
||||||
ENGINE.with(|engine| match engine.borrow_mut().as_mut() {
|
ENGINE.with(|engine| {
|
||||||
Some(engine) => engine.execute(action),
|
engine
|
||||||
None => false,
|
.borrow_mut()
|
||||||
|
.as_mut()
|
||||||
|
.is_some_and(|engine| engine.execute(action))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,13 +67,13 @@ fn edit(command: Command) -> bool {
|
|||||||
|
|
||||||
/// Decodes concatenated 16-byte UUIDs.
|
/// Decodes concatenated 16-byte UUIDs.
|
||||||
fn file_ids(bytes: &[u8]) -> Option<Vec<FileId>> {
|
fn file_ids(bytes: &[u8]) -> Option<Vec<FileId>> {
|
||||||
if bytes.len() % 16 != 0 {
|
let (chunks, rest) = bytes.as_chunks::<16>();
|
||||||
return None;
|
rest.is_empty().then(|| {
|
||||||
}
|
chunks
|
||||||
bytes
|
.iter()
|
||||||
.chunks_exact(16)
|
.map(|c| FileId(uuid::Uuid::from_bytes(*c)))
|
||||||
.map(|chunk| uuid::Uuid::from_slice(chunk).ok().map(FileId))
|
|
||||||
.collect()
|
.collect()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn same_len(a: &[f64], b: &[f64], c: &[f64]) -> bool {
|
fn same_len(a: &[f64], b: &[f64], c: &[f64]) -> bool {
|
||||||
@@ -302,19 +304,13 @@ pub fn redo() -> bool {
|
|||||||
/// `file_ids_bytes`: concatenated 16-byte UUIDs.
|
/// `file_ids_bytes`: concatenated 16-byte UUIDs.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn select(file_ids_bytes: &[u8]) -> bool {
|
pub fn select(file_ids_bytes: &[u8]) -> bool {
|
||||||
match file_ids(file_ids_bytes) {
|
file_ids(file_ids_bytes).is_some_and(|file_ids| execute(Action::Select { file_ids }))
|
||||||
Some(file_ids) => execute(Action::Select { file_ids }),
|
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `file_ids_bytes`: concatenated 16-byte UUIDs.
|
/// `file_ids_bytes`: concatenated 16-byte UUIDs.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn add_select(file_ids_bytes: &[u8]) -> bool {
|
pub fn add_select(file_ids_bytes: &[u8]) -> bool {
|
||||||
match file_ids(file_ids_bytes) {
|
file_ids(file_ids_bytes).is_some_and(|file_ids| execute(Action::AddSelect { file_ids }))
|
||||||
Some(file_ids) => execute(Action::AddSelect { file_ids }),
|
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@@ -513,7 +509,7 @@ pub fn selection() -> Selection {
|
|||||||
pub fn last_update() -> FilesUpdate {
|
pub fn last_update() -> FilesUpdate {
|
||||||
let update = Object::new();
|
let update = Object::new();
|
||||||
with_engine(|e| {
|
with_engine(|e| {
|
||||||
let diff = e.last_diff().clone().unwrap_or_default();
|
let diff = e.last_diff().cloned().unwrap_or_default();
|
||||||
set(&update, "orderChanged", e.order_changed());
|
set(&update, "orderChanged", e.order_changed());
|
||||||
set(&update, "selectionChanged", e.selection_changed());
|
set(&update, "selectionChanged", e.selection_changed());
|
||||||
set(&update, "added", ids(&diff.added));
|
set(&update, "added", ids(&diff.added));
|
||||||
|
|||||||
Reference in New Issue
Block a user