mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-16 08:19:59 +00:00
progress
This commit is contained in:
1 parent
b085a1d476
commit
42a55e6759
51 files changed
+5756
-3854
No files matched your search
@@ -1,4 +1,91 @@
|
||||
use crate::{Trackpoint, distance};
|
||||
use crate::{Statistics, TrackSegment, Trackpoint, distance};
|
||||
|
||||
/// The new timestamps of the trackpoints of `segments`, one after the other: each segment goes on
|
||||
/// from the end of the previous one, the first one starts at `start_time` (in milliseconds).
|
||||
///
|
||||
/// A segment that has timestamps keeps its durations multiplied by `ratio`; one that has none
|
||||
/// (judged by its first trackpoint) follows the previous trackpoint at `speed` (km/h). See
|
||||
/// [`shifted_and_compressed`] and [`with_timestamps`].
|
||||
pub fn changed_segment_times(
|
||||
segments: &[&TrackSegment],
|
||||
start_time: i64,
|
||||
speed: f64,
|
||||
ratio: f64,
|
||||
) -> Vec<Vec<Option<i64>>> {
|
||||
let mut last: Option<Trackpoint> = None;
|
||||
segments
|
||||
.iter()
|
||||
.map(|segment| {
|
||||
let points: Vec<Trackpoint> = segment.iter().cloned().collect();
|
||||
let Some(first) = points.first() else {
|
||||
return vec![];
|
||||
};
|
||||
let start = last.clone().unwrap_or_else(|| {
|
||||
let mut start = first.clone();
|
||||
start.time = Some(start_time);
|
||||
start
|
||||
});
|
||||
let points = if first.time.is_none() {
|
||||
with_timestamps(points, Some(speed), Some(&start), Some(start_time))
|
||||
} else {
|
||||
shifted_and_compressed(points, Some(speed), ratio, &start)
|
||||
};
|
||||
last = points.last().cloned();
|
||||
points.iter().map(|point| point.time).collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Makes up timestamps for the trackpoints of `segments`, one after the other: they start at
|
||||
/// `start_time` (in milliseconds) and last `total_time` seconds together, the longer and the
|
||||
/// steeper an interval the more of it. Each segment goes on from the end of the previous one.
|
||||
pub fn artificial_segment_times(
|
||||
segments: &[&TrackSegment],
|
||||
start_time: i64,
|
||||
total_time: f64,
|
||||
) -> Vec<Vec<Option<i64>>> {
|
||||
let mut weights: Vec<Vec<f64>> = segments
|
||||
.iter()
|
||||
.map(|segment| {
|
||||
let points: Vec<Trackpoint> = segment.iter().cloned().collect();
|
||||
let slopes: Vec<f64> = Statistics::compute(segment)
|
||||
.local
|
||||
.iter()
|
||||
.map(|l| l.slope)
|
||||
.collect();
|
||||
artificial_weights(&points, &slopes)
|
||||
})
|
||||
.collect();
|
||||
let mut total: f64 = weights.iter().flatten().sum();
|
||||
if total.is_nan() || total <= 0.0 {
|
||||
// no distance to share the time by: every interval takes as long
|
||||
for weights in &mut weights {
|
||||
weights.iter_mut().for_each(|w| *w = 1.0);
|
||||
}
|
||||
total = weights.iter().map(Vec::len).sum::<usize>() as f64;
|
||||
}
|
||||
let ms_per_weight = if total > 0.0 {
|
||||
total_time * 1000.0 / total
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let mut start = start_time;
|
||||
segments
|
||||
.iter()
|
||||
.zip(&weights)
|
||||
.map(|(segment, weights)| {
|
||||
let points: Vec<Trackpoint> = segment.iter().cloned().collect();
|
||||
if points.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let points = with_artificial_timestamps(points, weights, ms_per_weight, start);
|
||||
// the next segment goes on from the end of this one
|
||||
start = points.last().and_then(|point| point.time).unwrap_or(start);
|
||||
points.iter().map(|point| point.time).collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Replaces the trackpoints `start..end` of `trkpt` by `points`, keeping the timestamps of the
|
||||
/// whole path consistent. It is the work of the old `replaceTrackPoints` of the TS library.
|
||||
|
||||
@@ -3,10 +3,17 @@
|
||||
///
|
||||
/// The codes are the positions of the names in the order of first appearance. The table only
|
||||
/// grows: a code never changes meaning, so it stays valid in every state of the history.
|
||||
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Categories(Vec<String>);
|
||||
|
||||
impl Categories {
|
||||
/// A table with these names, in the order of their codes (at most [`Self::CAPACITY`] of them
|
||||
/// are kept).
|
||||
pub fn from_names(mut names: Vec<String>) -> Self {
|
||||
names.truncate(Self::CAPACITY);
|
||||
Self(names)
|
||||
}
|
||||
|
||||
/// Maximum number of distinct values.
|
||||
pub const CAPACITY: usize = u8::MAX as usize;
|
||||
|
||||
@@ -36,7 +43,7 @@ impl Categories {
|
||||
|
||||
/// The categories of the data of the trackpoints. There is one table per engine, shared by all
|
||||
/// its files.
|
||||
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct TrackpointCategories {
|
||||
pub surface: Categories,
|
||||
pub highway: Categories,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
macro_rules! uuid_id {
|
||||
($(#[$meta:meta])* $name:ident) => {
|
||||
$(#[$meta])*
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct $name(pub uuid::Uuid);
|
||||
|
||||
impl Default for $name {
|
||||
@@ -15,13 +15,13 @@ macro_rules! uuid_id {
|
||||
|
||||
pub(crate) use uuid_id;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct Link {
|
||||
pub href: String,
|
||||
pub text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct LngLat {
|
||||
pub lng: f64,
|
||||
pub lat: f64,
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct File {
|
||||
pub wpt: Waypoints,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct FileInfo {
|
||||
pub name: String,
|
||||
pub desc: Option<String>,
|
||||
@@ -21,7 +21,7 @@ pub struct FileInfo {
|
||||
pub time: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Author {
|
||||
pub name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
|
||||
@@ -11,7 +11,7 @@ pub struct Track {
|
||||
pub trkseg: Vec<TrackSegment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct TrackInfo {
|
||||
pub name: Option<String>,
|
||||
pub cmt: Option<String>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::LngLat;
|
||||
|
||||
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Trackpoint {
|
||||
pub coordinates: LngLat,
|
||||
pub ele: f64,
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::common::uuid_id;
|
||||
|
||||
uuid_id!(WaypointId);
|
||||
|
||||
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Waypoint {
|
||||
pub id: WaypointId,
|
||||
pub coordinates: LngLat,
|
||||
|
||||
+16
-19
@@ -69,18 +69,18 @@ impl Statistics {
|
||||
|
||||
self.global.total_distance += dist;
|
||||
|
||||
if let Some(time) = time {
|
||||
let speed = speed(dist, time);
|
||||
if (0.5..=1500.0).contains(&speed) {
|
||||
self.global.moving_distance = self
|
||||
.global
|
||||
.moving_distance
|
||||
.map_or(Some(dist), |d| Some(d + dist));
|
||||
self.global.moving_time = self
|
||||
.global
|
||||
.moving_time
|
||||
.map_or(Some(time), |t| Some(t + time));
|
||||
}
|
||||
let moving = time
|
||||
.zip(time.and_then(|time| speed(dist, time)))
|
||||
.filter(|(_, speed)| (0.5..=1500.0).contains(speed));
|
||||
if let Some((time, _)) = moving {
|
||||
self.global.moving_distance = self
|
||||
.global
|
||||
.moving_distance
|
||||
.map_or(Some(dist), |d| Some(d + dist));
|
||||
self.global.moving_time = self
|
||||
.global
|
||||
.moving_time
|
||||
.map_or(Some(time), |t| Some(t + time));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +119,7 @@ impl Statistics {
|
||||
};
|
||||
let (left, right) = (timed(left), timed(right));
|
||||
self.local[i.flat].speed = time_diff(trkseg[right].time, trkseg[left].time)
|
||||
.filter(|time| *time > 0)
|
||||
.map(|time| {
|
||||
.and_then(|time| {
|
||||
speed(
|
||||
self.local[right.flat].total_distance
|
||||
- self.local[left.flat].total_distance,
|
||||
@@ -286,16 +285,14 @@ impl GlobalStatistics {
|
||||
/// Average speed over the total time, unknown if that is not a positive duration (for
|
||||
/// example a range with a single timestamp, or timestamps going backwards).
|
||||
pub fn total_speed(&self) -> Option<f64> {
|
||||
self.total_time
|
||||
.filter(|t| *t > 0)
|
||||
.map(|t| speed(self.total_distance, t))
|
||||
self.total_time.and_then(|t| speed(self.total_distance, t))
|
||||
}
|
||||
|
||||
/// Average speed over the moving time, unknown if there is no moving time.
|
||||
pub fn moving_speed(&self) -> Option<f64> {
|
||||
self.moving_distance
|
||||
.zip(self.moving_time.filter(|t| *t > 0))
|
||||
.map(|(d, t)| speed(d, t))
|
||||
.zip(self.moving_time)
|
||||
.and_then(|(d, t)| speed(d, t))
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: &GlobalStatistics) {
|
||||
@@ -1,5 +1,5 @@
|
||||
mod statistics;
|
||||
mod compute;
|
||||
mod utils;
|
||||
|
||||
pub use statistics::*;
|
||||
pub use compute::*;
|
||||
pub use utils::*;
|
||||
@@ -22,9 +22,10 @@ pub fn time_diff(a: Option<i64>, b: Option<i64>) -> Option<i64> {
|
||||
Some(a? - b?)
|
||||
}
|
||||
|
||||
/// Computes the speed for a given distance in kilometers and a time in milliseconds
|
||||
pub fn speed(distance: f64, time: i64) -> f64 {
|
||||
distance / (time as f64 / 3_600_000.0)
|
||||
/// Computes the speed in km/h for a given distance in kilometers and a time in milliseconds.
|
||||
/// Unknown if the time is not a positive duration.
|
||||
pub fn speed(distance: f64, time: i64) -> Option<f64> {
|
||||
(time > 0).then(|| distance / (time as f64 / 3_600_000.0))
|
||||
}
|
||||
|
||||
pub fn slope(ele: f64, distance: f64) -> f64 {
|
||||
@@ -125,7 +126,7 @@ mod tests {
|
||||
assert_eq!(time_diff(None, Some(1)), None);
|
||||
assert_eq!(time_diff(Some(1), None), None);
|
||||
// 1 km in one hour
|
||||
assert!((speed(1.0, 3_600_000) - 1.0).abs() < 1e-12);
|
||||
assert!((speed(1.0, 3_600_000).unwrap() - 1.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -154,9 +155,13 @@ mod tests {
|
||||
#[test]
|
||||
fn test_speed() {
|
||||
// 1 km in 1 h
|
||||
assert!((speed(1.0, 3_600_000) - 1.0).abs() < 1e-12);
|
||||
assert!((speed(1.0, 3_600_000).unwrap() - 1.0).abs() < 1e-12);
|
||||
// 10 km in 30 min
|
||||
assert!((speed(10.0, 1_800_000) - 20.0).abs() < 1e-12);
|
||||
assert!((speed(10.0, 1_800_000).unwrap() - 20.0).abs() < 1e-12);
|
||||
// no duration, no speed
|
||||
assert_eq!(speed(1.0, 0), None);
|
||||
assert_eq!(speed(0.0, 0), None);
|
||||
assert_eq!(speed(1.0, -1000), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user