mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-04 12:58:23 +00:00
progress
This commit is contained in:
@@ -14,7 +14,7 @@ where
|
||||
match trkseg.len() {
|
||||
0 => vec![],
|
||||
1 => vec![trkseg.first_index().unwrap()],
|
||||
2 => vec![trkseg.last_index().unwrap()],
|
||||
2 => vec![trkseg.first_index().unwrap(), trkseg.last_index().unwrap()],
|
||||
_ => {
|
||||
let first = trkseg.first_index().unwrap();
|
||||
let last = trkseg.last_index().unwrap();
|
||||
@@ -67,3 +67,69 @@ fn ramer_douglas_peucker_helper<F>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Trackpoint, TrackpointChunk};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn segment(eles: &[f64]) -> TrackSegment {
|
||||
let mut trkseg = TrackSegment::default();
|
||||
// several small chunks, to also exercise the chunk boundaries
|
||||
for part in eles.chunks(3) {
|
||||
let mut chunk = TrackpointChunk::default();
|
||||
for ele in part {
|
||||
chunk.trkpt.push(Trackpoint {
|
||||
ele: *ele,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
trkseg.push(chunk);
|
||||
}
|
||||
trkseg
|
||||
}
|
||||
|
||||
fn simplify(eles: &[f64], epsilon: f64) -> Vec<usize> {
|
||||
let trkseg = segment(eles);
|
||||
ramer_douglas_peucker(&trkseg, &|idx| (idx.flat as f64, trkseg[idx].ele), epsilon)
|
||||
.iter()
|
||||
.map(|idx| idx.flat)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_and_single_point() {
|
||||
assert!(simplify(&[], 1.0).is_empty());
|
||||
assert_eq!(simplify(&[5.0], 1.0), vec![0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collinear_points_are_removed() {
|
||||
let eles: Vec<f64> = (0..20).map(|i| i as f64 * 2.0).collect();
|
||||
assert_eq!(simplify(&eles, 0.1), vec![0, 19]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_peak_is_kept_when_above_epsilon() {
|
||||
let eles = [0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0];
|
||||
assert_eq!(simplify(&eles, 5.0), vec![0, 3, 6]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_peak_is_dropped_when_below_epsilon() {
|
||||
let eles = [0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0];
|
||||
assert_eq!(simplify(&eles, 1.0), vec![0, 6]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indices_are_sorted_and_keep_extremities() {
|
||||
let eles = [0.0, 5.0, -3.0, 8.0, 1.0, 9.0, -4.0, 2.0, 7.0, 0.0];
|
||||
let kept = simplify(&eles, 0.5);
|
||||
assert_eq!(kept.first(), Some(&0));
|
||||
assert_eq!(kept.last(), Some(&9));
|
||||
assert!(kept.windows(2).all(|w| w[0] < w[1]));
|
||||
// a larger epsilon never keeps more points
|
||||
assert!(simplify(&eles, 5.0).len() <= kept.len());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,3 +52,90 @@ macro_rules! for_each_window {
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{TrackSegment, Trackpoint, TrackpointChunk, for_each_window};
|
||||
|
||||
fn segment(eles: &[f64]) -> TrackSegment {
|
||||
let mut trkseg = TrackSegment::default();
|
||||
// several small chunks, to also exercise the chunk boundaries
|
||||
for part in eles.chunks(3) {
|
||||
let mut chunk = TrackpointChunk::default();
|
||||
for ele in part {
|
||||
chunk.trkpt.push(Trackpoint {
|
||||
ele: *ele,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
trkseg.push(chunk);
|
||||
}
|
||||
trkseg
|
||||
}
|
||||
|
||||
/// Returns (center, left, right) flat indices for each visited center.
|
||||
fn windows(len: usize, window: f64) -> Vec<(usize, usize, usize)> {
|
||||
let trkseg = segment(&vec![0.0; len]);
|
||||
let mut visited = vec![];
|
||||
for_each_window!(
|
||||
trkseg,
|
||||
trkseg.first_index(),
|
||||
trkseg.last_index(),
|
||||
window,
|
||||
|a, b| (b.flat - a.flat) as f64,
|
||||
|i, l, r| {
|
||||
visited.push((i.flat, l.flat, r.flat));
|
||||
},
|
||||
);
|
||||
visited
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_bounds() {
|
||||
let visited = windows(10, 2.0);
|
||||
assert_eq!(visited.len(), 10);
|
||||
for (i, l, r) in visited {
|
||||
assert_eq!(l, i.saturating_sub(2), "left of {i}");
|
||||
assert_eq!(r, (i + 2).min(9), "right of {i}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_always_includes_neighbours() {
|
||||
for (i, l, r) in windows(6, 0.0) {
|
||||
assert_eq!((l, r), (i.saturating_sub(1), (i + 1).min(5)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_larger_than_segment() {
|
||||
for (_, l, r) in windows(5, 100.0) {
|
||||
assert_eq!((l, r), (0, 4));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_sub_range() {
|
||||
let trkseg = segment(&vec![0.0; 10]);
|
||||
let mut centers = vec![];
|
||||
let mut first = trkseg.first_index();
|
||||
for _ in 0..3 {
|
||||
first = trkseg.next_index(first);
|
||||
}
|
||||
let mut last = first;
|
||||
for _ in 0..2 {
|
||||
last = trkseg.next_index(last);
|
||||
}
|
||||
for_each_window!(
|
||||
trkseg,
|
||||
first,
|
||||
last,
|
||||
1.0,
|
||||
|a, b| (b.flat - a.flat) as f64,
|
||||
|i, _l, _r| {
|
||||
centers.push(i.flat);
|
||||
},
|
||||
);
|
||||
assert_eq!(centers, vec![3, 4, 5]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,3 +59,41 @@ impl WaypointChunk {
|
||||
self.wpt.len() == MAX_WPT_CHUNK_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_trackpoint_chunk_full() {
|
||||
let mut chunk = TrackpointChunk::default();
|
||||
assert!(!chunk.is_full());
|
||||
for _ in 0..MAX_TRKPT_CHUNK_SIZE - 1 {
|
||||
chunk.trkpt.push(Trackpoint::default());
|
||||
}
|
||||
assert!(!chunk.is_full());
|
||||
chunk.trkpt.push(Trackpoint::default());
|
||||
assert!(chunk.is_full());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_waypoint_chunk_full() {
|
||||
let mut chunk = WaypointChunk::default();
|
||||
assert!(!chunk.is_full());
|
||||
for _ in 0..MAX_WPT_CHUNK_SIZE {
|
||||
chunk.wpt.push(Waypoint::default());
|
||||
}
|
||||
assert!(chunk.is_full());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunk_equality_is_by_id() {
|
||||
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());
|
||||
assert_eq!(a, c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,3 +46,52 @@ impl LngLatBounds {
|
||||
self.ne.lat = self.ne.lat.max(other.ne.lat);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bounds_extend() {
|
||||
let mut bounds = LngLatBounds::default();
|
||||
bounds.extend(LngLat {
|
||||
lng: 4.0,
|
||||
lat: 50.0,
|
||||
});
|
||||
assert_eq!((bounds.sw.lng, bounds.sw.lat), (4.0, 50.0));
|
||||
assert_eq!((bounds.ne.lng, bounds.ne.lat), (4.0, 50.0));
|
||||
|
||||
bounds.extend(LngLat {
|
||||
lng: 6.0,
|
||||
lat: 45.0,
|
||||
});
|
||||
bounds.extend(LngLat {
|
||||
lng: 5.0,
|
||||
lat: 48.0,
|
||||
});
|
||||
assert_eq!((bounds.sw.lng, bounds.sw.lat), (4.0, 45.0));
|
||||
assert_eq!((bounds.ne.lng, bounds.ne.lat), (6.0, 50.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bounds_merge() {
|
||||
let mut a = LngLatBounds::default();
|
||||
a.extend(LngLat { lng: 0.0, lat: 0.0 });
|
||||
a.extend(LngLat { lng: 1.0, lat: 1.0 });
|
||||
let mut b = LngLatBounds::default();
|
||||
b.extend(LngLat {
|
||||
lng: -2.0,
|
||||
lat: 0.5,
|
||||
});
|
||||
b.extend(LngLat { lng: 0.5, lat: 3.0 });
|
||||
|
||||
a.merge(&b);
|
||||
assert_eq!((a.sw.lng, a.sw.lat), (-2.0, 0.0));
|
||||
assert_eq!((a.ne.lng, a.ne.lat), (1.0, 3.0));
|
||||
|
||||
// merging an empty bounds changes nothing
|
||||
a.merge(&LngLatBounds::default());
|
||||
assert_eq!((a.sw.lng, a.sw.lat), (-2.0, 0.0));
|
||||
assert_eq!((a.ne.lng, a.ne.lat), (1.0, 3.0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,19 +81,20 @@ impl TrackSegment {
|
||||
pos: 0,
|
||||
flat: self.cumul_length.last().copied().unwrap_or_default(),
|
||||
});
|
||||
loop {
|
||||
if prev.pos == 0 {
|
||||
if prev.chunk == 0 {
|
||||
return None;
|
||||
}
|
||||
if prev.pos == 0 {
|
||||
while prev.chunk > 0 {
|
||||
prev.chunk -= 1;
|
||||
prev.pos = self.chunks[prev.chunk].trkpt.len();
|
||||
prev.flat -= 1;
|
||||
} else {
|
||||
prev.pos -= 1;
|
||||
prev.flat -= 1;
|
||||
return Some(prev);
|
||||
if !self.chunks[prev.chunk].trkpt.is_empty() {
|
||||
prev.pos = self.chunks[prev.chunk].trkpt.len() - 1;
|
||||
prev.flat -= 1;
|
||||
return Some(prev);
|
||||
}
|
||||
}
|
||||
None
|
||||
} else {
|
||||
prev.pos -= 1;
|
||||
prev.flat -= 1;
|
||||
Some(prev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,4 +269,42 @@ mod tests {
|
||||
assert!(std::ptr::eq(&trkseg[i], trkpt));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_last_index() {
|
||||
assert!(TrackSegment::default().first_index().is_none());
|
||||
assert!(TrackSegment::default().last_index().is_none());
|
||||
|
||||
let trkseg = create_track_segment(5); // 15 points in chunks of 1..=5
|
||||
let first = trkseg.first_index().unwrap();
|
||||
let last = trkseg.last_index().unwrap();
|
||||
assert_eq!((first.chunk, first.pos, first.flat), (0, 0, 0));
|
||||
assert_eq!((last.chunk, last.pos, last.flat), (4, 4, 14));
|
||||
assert_eq!(trkseg[last].ele, 14.0);
|
||||
assert_eq!(trkseg.locate(14), Some(last));
|
||||
assert!(trkseg.next_index(Some(last)).is_none());
|
||||
assert!(trkseg.prev_index(Some(first)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prev_next_are_inverse() {
|
||||
let trkseg = create_track_segment(6);
|
||||
let mut idx = trkseg.last_index();
|
||||
let mut visited = vec![];
|
||||
while let Some(i) = idx {
|
||||
assert_eq!(trkseg.locate(i.flat), Some(i));
|
||||
visited.push(i.flat);
|
||||
idx = trkseg.prev_index(Some(i));
|
||||
}
|
||||
assert_eq!(visited, (0..trkseg.len()).rev().collect::<Vec<_>>());
|
||||
|
||||
let mut idx = trkseg.first_index();
|
||||
while let Some(i) = idx {
|
||||
let next = trkseg.next_index(Some(i));
|
||||
if let Some(n) = next {
|
||||
assert_eq!(trkseg.prev_index(Some(n)), Some(i));
|
||||
}
|
||||
idx = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
LngLat, LngLatBounds, TrackSegment, TrackSegmentIndex, Trackpoint, distance, for_each_window,
|
||||
ramer_douglas_peucker, slope, speed, sum_options, time_diff,
|
||||
max_options, min_options, ramer_douglas_peucker, slope, speed, sum_options, time_diff,
|
||||
};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -10,10 +10,6 @@ pub struct Statistics {
|
||||
}
|
||||
|
||||
impl Statistics {
|
||||
pub fn total_time(&self) -> Option<i32> {
|
||||
self.global.total_time()
|
||||
}
|
||||
|
||||
pub fn total_speed(&self) -> Option<f64> {
|
||||
self.global.total_speed()
|
||||
}
|
||||
@@ -76,6 +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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +211,7 @@ impl Statistics {
|
||||
pub struct GlobalStatistics {
|
||||
pub total_distance: f64,
|
||||
pub moving_distance: Option<f64>,
|
||||
pub total_time: Option<i32>,
|
||||
pub moving_time: Option<i32>,
|
||||
pub elevation_gain: f64,
|
||||
pub elevation_loss: f64,
|
||||
@@ -223,12 +221,8 @@ pub struct GlobalStatistics {
|
||||
}
|
||||
|
||||
impl GlobalStatistics {
|
||||
pub fn total_time(&self) -> Option<i32> {
|
||||
time_diff(&self.start_time, &self.end_time)
|
||||
}
|
||||
|
||||
pub fn total_speed(&self) -> Option<f64> {
|
||||
self.total_time().map(|t| speed(self.total_distance, t))
|
||||
self.total_time.map(|t| speed(self.total_distance, t))
|
||||
}
|
||||
|
||||
pub fn moving_speed(&self) -> Option<f64> {
|
||||
@@ -240,9 +234,12 @@ impl GlobalStatistics {
|
||||
pub fn merge(&mut self, other: &GlobalStatistics) {
|
||||
self.total_distance += other.total_distance;
|
||||
self.moving_distance = sum_options(self.moving_distance, other.moving_distance);
|
||||
self.total_time = sum_options(self.total_time, other.total_time);
|
||||
self.moving_time = sum_options(self.moving_time, other.moving_time);
|
||||
self.elevation_gain += other.elevation_gain;
|
||||
self.elevation_loss += other.elevation_loss;
|
||||
self.start_time = min_options(self.start_time, other.start_time);
|
||||
self.end_time = max_options(self.end_time, other.end_time);
|
||||
self.bounds.merge(&other.bounds);
|
||||
}
|
||||
}
|
||||
@@ -265,7 +262,7 @@ impl TrackpointStatistics {
|
||||
Self {
|
||||
total_distance: stats.global.total_distance,
|
||||
moving_distance: stats.global.moving_distance,
|
||||
total_time: stats.total_time(),
|
||||
total_time: stats.global.total_time,
|
||||
moving_time: stats.global.moving_time,
|
||||
// stats below are computed later
|
||||
speed: None,
|
||||
@@ -309,5 +306,107 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO more tests
|
||||
fn load(path: &str) -> crate::File {
|
||||
let mut f = File::open(path).unwrap();
|
||||
let mut data = String::new();
|
||||
let _ = f.read_to_string(&mut data);
|
||||
parse(data.as_bytes()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_empty_segment() {
|
||||
let stats = Statistics::compute(&TrackSegment::default());
|
||||
assert!(stats.local.is_empty());
|
||||
assert_eq!(stats.global.total_distance, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_distance_and_bounds() {
|
||||
let gpx = load("data/simple.gpx");
|
||||
let trkseg = &gpx.trk[0].trkseg[0];
|
||||
let stats = Statistics::compute(trkseg);
|
||||
|
||||
assert_eq!(stats.local.len(), trkseg.len());
|
||||
assert_eq!(stats.local[0].total_distance, 0.0);
|
||||
assert!(stats.global.total_distance > 0.0);
|
||||
// cumulative distance never decreases and ends at the global distance
|
||||
assert!(
|
||||
stats
|
||||
.local
|
||||
.windows(2)
|
||||
.all(|w| w[0].total_distance <= w[1].total_distance)
|
||||
);
|
||||
let last = stats.local.last().unwrap();
|
||||
assert!((last.total_distance - stats.global.total_distance).abs() < 1e-9);
|
||||
assert!(stats.global.elevation_gain >= 0.0);
|
||||
assert!(stats.global.elevation_loss >= 0.0);
|
||||
|
||||
// every point lies inside the bounds
|
||||
let b = &stats.global.bounds;
|
||||
for trkpt in trkseg.iter() {
|
||||
assert!(b.sw.lng <= trkpt.coordinates.lng && trkpt.coordinates.lng <= b.ne.lng);
|
||||
assert!(b.sw.lat <= trkpt.coordinates.lat && trkpt.coordinates.lat <= b.ne.lat);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_time() {
|
||||
let gpx = load("data/with_time.gpx");
|
||||
let stats = Statistics::compute(&gpx.trk[0].trkseg[0]);
|
||||
let total_time = stats.global.total_time.unwrap();
|
||||
assert!(total_time > 0);
|
||||
let speed = stats.global.total_speed().unwrap();
|
||||
assert!((speed - 20.0).abs() < 0.5, "{speed}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_time_without_timestamps() {
|
||||
let gpx = load("data/simple.gpx");
|
||||
let stats = Statistics::compute(&gpx.trk[0].trkseg[0]);
|
||||
assert!(stats.global.total_time.is_none());
|
||||
assert!(stats.global.total_speed().is_none());
|
||||
assert!(stats.local.iter().all(|s| s.speed.is_none()));
|
||||
}
|
||||
|
||||
#[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);
|
||||
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);
|
||||
b.bounds.extend(crate::LngLat { lng: 2.0, lat: 3.0 });
|
||||
|
||||
a.merge(&b);
|
||||
assert_eq!(a.total_distance, 3.0);
|
||||
assert_eq!(a.elevation_gain, 10.0);
|
||||
assert_eq!(a.elevation_loss, 4.0);
|
||||
assert_eq!(a.moving_distance, Some(1.5));
|
||||
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);
|
||||
c.merge(&d);
|
||||
// 3 s + 2 s, the gap between the two is ignored
|
||||
assert_eq!(c.total_time, Some(5_000));
|
||||
let mut cumul = GlobalStatistics::default();
|
||||
cumul.merge(&c);
|
||||
cumul.merge(&d);
|
||||
assert_eq!(cumul.start_time, Some(1_000));
|
||||
assert_eq!(cumul.end_time, Some(12_000));
|
||||
assert_eq!(cumul.total_time, Some(7_000));
|
||||
assert_eq!((a.bounds.ne.lng, a.bounds.ne.lat), (2.0, 3.0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,44 @@
|
||||
use std::ops::Add;
|
||||
|
||||
pub fn combine_options<T>(a: Option<T>, b: Option<T>, f: impl FnOnce(T, T) -> T) -> Option<T> {
|
||||
match (a, b) {
|
||||
(Some(a), Some(b)) => Some(f(a, b)),
|
||||
(Some(v), None) | (None, Some(v)) => Some(v),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sum_options<T>(a: Option<T>, b: Option<T>) -> Option<T>
|
||||
where
|
||||
T: Add<Output = T>,
|
||||
{
|
||||
match (a, b) {
|
||||
(Some(a), Some(b)) => Some(a + b),
|
||||
(Some(a), None) => Some(a),
|
||||
(None, Some(b)) => Some(b),
|
||||
(None, None) => None,
|
||||
combine_options(a, b, |a, b| a + b)
|
||||
}
|
||||
|
||||
pub fn min_options<T>(a: Option<T>, b: Option<T>) -> Option<T>
|
||||
where
|
||||
T: Ord,
|
||||
{
|
||||
combine_options(a, b, |a, b| a.min(b))
|
||||
}
|
||||
|
||||
pub fn max_options<T>(a: Option<T>, b: Option<T>) -> Option<T>
|
||||
where
|
||||
T: Ord,
|
||||
{
|
||||
combine_options(a, b, |a, b| a.max(b))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sum_options() {
|
||||
assert_eq!(sum_options(Some(1), Some(2)), Some(3));
|
||||
assert_eq!(sum_options(Some(1), None), Some(1));
|
||||
assert_eq!(sum_options(None, Some(2)), Some(2));
|
||||
assert_eq!(sum_options::<i32>(None, None), None);
|
||||
assert_eq!(sum_options(Some(0.5), Some(0.25)), Some(0.75));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,3 +119,85 @@ pub fn crossarc(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> f64 {
|
||||
((x3 - proj_x) * (x3 - proj_x) + (y3 - proj_y) * (y3 - proj_y)).sqrt()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn p(lng: f64, lat: f64) -> LngLat {
|
||||
LngLat { lng, lat }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_distance() {
|
||||
assert_eq!(distance(p(4.0, 50.0), p(4.0, 50.0)), 0.0);
|
||||
// one degree of latitude
|
||||
let d = distance(p(0.0, 0.0), p(0.0, 1.0));
|
||||
assert!((d - 111.195).abs() < 0.01, "{d}");
|
||||
// symmetric
|
||||
let a = p(4.40, 50.79);
|
||||
let b = p(6.13, 45.90);
|
||||
assert!((distance(a, b) - distance(b, a)).abs() < 1e-9);
|
||||
// half the Earth's circumference, without NaN from rounding
|
||||
let antipodal = distance(p(0.0, 0.0), p(180.0, 0.0));
|
||||
assert!((antipodal - PI * EARTH_RADIUS).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_speed() {
|
||||
// 1 km in 1 h
|
||||
assert!((speed(1.0, 3_600_000) - 1.0).abs() < 1e-12);
|
||||
// 10 km in 30 min
|
||||
assert!((speed(10.0, 1_800_000) - 20.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slope() {
|
||||
// 10 m of elevation over 100 m (distance is expressed in km, hence the 0.1 factor)
|
||||
assert!((slope(10.0, 0.1) - 10.0).abs() < 1e-12);
|
||||
assert!((slope(-5.0, 0.1) + 5.0).abs() < 1e-12);
|
||||
assert_eq!(slope(0.0, 1.0), 0.0);
|
||||
assert_eq!(slope(10.0, 0.0), 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_crossarc() {
|
||||
// perpendicular distance to the segment (0,0)-(10,0)
|
||||
assert!((crossarc(0.0, 0.0, 10.0, 0.0, 5.0, 3.0) - 3.0).abs() < 1e-12);
|
||||
// beyond the ends: distance to the closest endpoint
|
||||
assert!((crossarc(0.0, 0.0, 10.0, 0.0, 13.0, 4.0) - 5.0).abs() < 1e-12);
|
||||
assert!((crossarc(0.0, 0.0, 10.0, 0.0, -3.0, 4.0) - 5.0).abs() < 1e-12);
|
||||
// point on the segment
|
||||
assert_eq!(crossarc(0.0, 0.0, 10.0, 0.0, 4.0, 0.0), 0.0);
|
||||
// degenerate segment
|
||||
assert!((crossarc(1.0, 1.0, 1.0, 1.0, 4.0, 5.0) - 5.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_projected() {
|
||||
let proj = projected(p(0.0, 0.0), p(1.0, 0.0), p(0.5, 1.0));
|
||||
assert!((proj.lng - 0.5).abs() < 1e-9);
|
||||
assert!(proj.lat.abs() < 1e-9);
|
||||
// clamped to the segment
|
||||
let proj = projected(p(0.0, 0.0), p(1.0, 0.0), p(2.0, 1.0));
|
||||
assert!((proj.lng - 1.0).abs() < 1e-9);
|
||||
// degenerate segment
|
||||
let proj = projected(p(3.0, 4.0), p(3.0, 4.0), p(5.0, 6.0));
|
||||
assert_eq!((proj.lng, proj.lat), (3.0, 4.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_crossarc_lnglat() {
|
||||
// about 1 degree of latitude away from an east-west segment on the equator
|
||||
let d = crossarc_lnglat(p(0.0, 0.0), p(1.0, 0.0), p(0.5, 1.0));
|
||||
assert!((d - METERS_PER_LATITUDE_DEGREE).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user