This commit is contained in:
vcoppe
2026-10-03 20:54:05 +02:00
parent 9afba53e5d
commit f1904bc12f
17 changed files with 222 additions and 310 deletions
-1
View File
@@ -1 +0,0 @@
-3
View File
@@ -1,3 +0,0 @@
mod file;
pub use file::*;
-1
View File
@@ -2,4 +2,3 @@ mod simplify;
mod smooth;
pub use simplify::*;
pub use smooth::*;
+1 -3
View File
@@ -59,13 +59,11 @@ fn ramer_douglas_peucker_helper<F>(
cur = trkseg.next_index(cur);
}
if let Some(idx) = max_idx {
if max_dist > epsilon {
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);
}
}
}
#[cfg(test)]
+2 -2
View File
@@ -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 {
+6 -5
View File
@@ -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);
}
}
+11 -7
View File
@@ -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]
+52 -110
View File
@@ -38,15 +38,13 @@ enum GPXElement {
fn parse_coordinates(attributes: Attributes<'_>) -> LngLat {
let mut coordinates = LngLat::default();
for attr in attributes {
if let Ok(attr) = attr {
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,13 +66,11 @@ 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 {
for attr in e.attributes().flatten() {
if attr.key.as_ref() == "href" {
link.href = attr.value.to_string();
}
}
}
stack.push(GPXElement::Link(link));
}
"text" => stack.push(GPXElement::Text),
@@ -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();
}
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() {
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(trkpt_chunk);
trkpt_chunk = TrackpointChunk::default();
}
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,12 +216,9 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
}
Some(GPXElement::Source) => {
stack.pop();
match stack.last_mut() {
Some(GPXElement::Track(trk)) => {
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
trk.info.src = Some(e.to_string());
}
_ => (),
}
}
Some(GPXElement::Text) => {
stack.pop();
@@ -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,12 +272,9 @@ pub fn parse(data: &[u8]) -> Result<File, Error> {
}
Some(GPXElement::Symbol) => {
stack.pop();
match stack.last_mut() {
Some(GPXElement::Waypoint(wpt)) => {
if let Some(GPXElement::Waypoint(wpt)) = stack.last_mut() {
wpt.sym = Some(e.to_string());
}
_ => (),
}
}
Some(GPXElement::Type) => {
stack.pop();
@@ -303,32 +290,21 @@ 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)) => {
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)) => {
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];
-2
View File
@@ -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::*;
+32 -24
View File
@@ -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));
+64 -77
View File
@@ -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);
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,
impl Planar {
fn around(origin: LngLat) -> Self {
Self {
meters_per_longitude_degree: (origin.lat * TO_RADIANS).cos()
* 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,
fn to_meters(&self, p: LngLat) -> (f64, f64) {
(
p.lng * self.meters_per_longitude_degree,
p.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 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()
}),
+6 -14
View File
@@ -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());
+2 -8
View File
@@ -67,19 +67,13 @@ 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.push(entry);
self.index = Some(self.entries.len() - 1);
@@ -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);
}
}
+15 -19
View File
@@ -49,13 +49,15 @@ impl From<CleanType> for engine::CleanType {
}
thread_local! {
static ENGINE: RefCell<Option<Engine>> = RefCell::new(None);
static ENGINE: RefCell<Option<Engine>> = const { RefCell::new(None) };
}
fn execute(action: Action) -> bool {
ENGINE.with(|engine| match engine.borrow_mut().as_mut() {
Some(engine) => engine.execute(action),
None => false,
ENGINE.with(|engine| {
engine
.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.
fn file_ids(bytes: &[u8]) -> Option<Vec<FileId>> {
if bytes.len() % 16 != 0 {
return None;
}
bytes
.chunks_exact(16)
.map(|chunk| uuid::Uuid::from_slice(chunk).ok().map(FileId))
let (chunks, rest) = bytes.as_chunks::<16>();
rest.is_empty().then(|| {
chunks
.iter()
.map(|c| FileId(uuid::Uuid::from_bytes(*c)))
.collect()
})
}
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.
#[wasm_bindgen]
pub fn select(file_ids_bytes: &[u8]) -> bool {
match file_ids(file_ids_bytes) {
Some(file_ids) => execute(Action::Select { file_ids }),
None => false,
}
file_ids(file_ids_bytes).is_some_and(|file_ids| execute(Action::Select { file_ids }))
}
/// `file_ids_bytes`: concatenated 16-byte UUIDs.
#[wasm_bindgen]
pub fn add_select(file_ids_bytes: &[u8]) -> bool {
match file_ids(file_ids_bytes) {
Some(file_ids) => execute(Action::AddSelect { file_ids }),
None => false,
}
file_ids(file_ids_bytes).is_some_and(|file_ids| execute(Action::AddSelect { file_ids }))
}
#[wasm_bindgen]
@@ -513,7 +509,7 @@ pub fn selection() -> Selection {
pub fn last_update() -> FilesUpdate {
let update = Object::new();
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, "selectionChanged", e.selection_changed());
set(&update, "added", ids(&diff.added));