2026-10-14 09:22:07 +02:00
|
|
|
//! What crosses to the frontend, as plain data (serde): the shapes of the objects that the frontend
|
|
|
|
|
//! reads, which `wasm/src/ts.rs` declares in TypeScript. Ids are hyphenated UUID strings, times
|
|
|
|
|
//! are numbers (ms since the epoch), absent fields are skipped.
|
2026-10-14 08:51:38 +02:00
|
|
|
|
2026-10-14 09:22:07 +02:00
|
|
|
use crate as engine;
|
2026-10-16 09:02:31 +02:00
|
|
|
use engine::{FileId, History};
|
2026-10-14 08:51:38 +02:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
pub fn ids(items: &[FileId]) -> Vec<String> {
|
|
|
|
|
items.iter().map(|id| id.0.to_string()).collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Outcome
|
|
|
|
|
|
|
|
|
|
/// What a call that edits the files or changes the selection did.
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
#[serde(rename_all = "camelCase")]
|
|
|
|
|
pub struct Outcome {
|
2026-10-14 19:15:02 +02:00
|
|
|
pub changed: bool,
|
2026-10-14 08:51:38 +02:00
|
|
|
order_changed: bool,
|
|
|
|
|
selection_changed: bool,
|
|
|
|
|
clipboard_changed: bool,
|
|
|
|
|
added: Vec<String>,
|
|
|
|
|
modified: Vec<String>,
|
|
|
|
|
removed: Vec<String>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
error: Option<String>,
|
2026-10-14 09:22:07 +02:00
|
|
|
/// Whether the files changed in a way that has to be saved (not sent).
|
|
|
|
|
#[serde(skip)]
|
|
|
|
|
pub saves: bool,
|
2026-10-14 08:51:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Outcome {
|
|
|
|
|
pub fn new(outcome: &engine::Outcome, error: Option<String>) -> Self {
|
|
|
|
|
let diff = outcome.diff.clone().unwrap_or_default();
|
|
|
|
|
Self {
|
|
|
|
|
changed: outcome.changed,
|
|
|
|
|
order_changed: outcome.order_changed,
|
|
|
|
|
selection_changed: outcome.selection_changed,
|
|
|
|
|
clipboard_changed: outcome.clipboard_changed,
|
|
|
|
|
added: ids(&diff.added),
|
|
|
|
|
modified: ids(&diff.modified),
|
|
|
|
|
removed: ids(&diff.removed),
|
|
|
|
|
error,
|
2026-10-14 09:22:07 +02:00
|
|
|
saves: outcome.changed && (outcome.diff.is_some() || outcome.order_changed),
|
2026-10-14 08:51:38 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-14 19:15:02 +02:00
|
|
|
/// The outcome of a frontend that knows nothing of the engine: all the files are added, and the
|
|
|
|
|
/// order, the selection and the clipboard are to be read.
|
2026-10-16 09:02:31 +02:00
|
|
|
pub fn everything<H: History>(engine: &engine::Engine<H>) -> Self {
|
2026-10-14 19:15:02 +02:00
|
|
|
Self {
|
|
|
|
|
changed: true,
|
|
|
|
|
order_changed: true,
|
|
|
|
|
selection_changed: true,
|
|
|
|
|
clipboard_changed: true,
|
|
|
|
|
added: ids(engine.order()),
|
|
|
|
|
modified: vec![],
|
|
|
|
|
removed: vec![],
|
|
|
|
|
error: None,
|
|
|
|
|
saves: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-14 08:51:38 +02:00
|
|
|
/// The outcome of a call that did nothing and has nothing to say.
|
|
|
|
|
pub fn failed_none() -> Self {
|
|
|
|
|
Self::new(&engine::Outcome::default(), None)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-16 18:14:59 +02:00
|
|
|
/// The ids of the files that the call added.
|
|
|
|
|
pub fn added(&self) -> &[String] {
|
|
|
|
|
&self.added
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-14 19:15:02 +02:00
|
|
|
/// Whether the call did nothing and says why.
|
|
|
|
|
pub fn has_error(&self) -> bool {
|
|
|
|
|
self.error.is_some()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// What two calls that were made one after the other did together.
|
|
|
|
|
pub fn merge(mut self, other: Self) -> Self {
|
|
|
|
|
fn union(into: &mut Vec<String>, from: Vec<String>) {
|
|
|
|
|
for id in from {
|
|
|
|
|
if !into.contains(&id) {
|
|
|
|
|
into.push(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.changed |= other.changed;
|
|
|
|
|
self.order_changed |= other.order_changed;
|
|
|
|
|
self.selection_changed |= other.selection_changed;
|
|
|
|
|
self.clipboard_changed |= other.clipboard_changed;
|
|
|
|
|
self.saves |= other.saves;
|
|
|
|
|
// a file that was added and then modified is still an added one, and one that was added
|
|
|
|
|
// and then removed is not there at all
|
|
|
|
|
let added_before = self.added.clone();
|
|
|
|
|
union(&mut self.added, other.added);
|
|
|
|
|
for id in other.modified {
|
|
|
|
|
if !self.added.contains(&id) && !self.modified.contains(&id) {
|
|
|
|
|
self.modified.push(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for id in other.removed {
|
|
|
|
|
self.added.retain(|added| *added != id);
|
|
|
|
|
self.modified.retain(|modified| *modified != id);
|
|
|
|
|
if !added_before.contains(&id) && !self.removed.contains(&id) {
|
|
|
|
|
self.removed.push(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.error = self.error.or(other.error);
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-14 08:51:38 +02:00
|
|
|
/// What a call that does nothing returns.
|
|
|
|
|
pub fn failed(message: impl Into<String>) -> Self {
|
|
|
|
|
Self::new(&engine::Outcome::default(), Some(message.into()))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Selection and clipboard
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
|
#[serde(
|
|
|
|
|
tag = "type",
|
|
|
|
|
rename_all = "camelCase",
|
|
|
|
|
rename_all_fields = "camelCase"
|
|
|
|
|
)]
|
|
|
|
|
pub enum Selection {
|
|
|
|
|
Empty,
|
|
|
|
|
File {
|
|
|
|
|
file_ids: Vec<Uuid>,
|
|
|
|
|
},
|
2026-10-26 18:51:56 +01:00
|
|
|
Route {
|
|
|
|
|
file_id: Uuid,
|
|
|
|
|
route_ids: Vec<Uuid>,
|
|
|
|
|
},
|
2026-10-14 08:51:38 +02:00
|
|
|
Track {
|
|
|
|
|
file_id: Uuid,
|
|
|
|
|
track_ids: Vec<Uuid>,
|
|
|
|
|
},
|
|
|
|
|
Segment {
|
|
|
|
|
file_id: Uuid,
|
|
|
|
|
track_id: Uuid,
|
|
|
|
|
segment_ids: Vec<Uuid>,
|
|
|
|
|
},
|
|
|
|
|
Waypoints {
|
|
|
|
|
file_id: Uuid,
|
|
|
|
|
},
|
|
|
|
|
Waypoint {
|
|
|
|
|
file_id: Uuid,
|
|
|
|
|
waypoint_ids: Vec<Uuid>,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&engine::Selection> for Selection {
|
|
|
|
|
fn from(selection: &engine::Selection) -> Self {
|
|
|
|
|
use engine::Selection as S;
|
|
|
|
|
match selection {
|
|
|
|
|
S::Empty => Self::Empty,
|
|
|
|
|
S::File { file_ids } => Self::File {
|
|
|
|
|
file_ids: file_ids.iter().map(|id| id.0).collect(),
|
|
|
|
|
},
|
2026-10-26 18:51:56 +01:00
|
|
|
S::Route { file_id, rte_ids } => Self::Route {
|
|
|
|
|
file_id: file_id.0,
|
|
|
|
|
route_ids: rte_ids.iter().map(|id| id.0).collect(),
|
|
|
|
|
},
|
2026-10-14 08:51:38 +02:00
|
|
|
S::Track { file_id, trk_ids } => Self::Track {
|
|
|
|
|
file_id: file_id.0,
|
|
|
|
|
track_ids: trk_ids.iter().map(|id| id.0).collect(),
|
|
|
|
|
},
|
|
|
|
|
S::TrackSegment {
|
|
|
|
|
file_id,
|
|
|
|
|
trk_id,
|
|
|
|
|
trkseg_ids,
|
|
|
|
|
} => Self::Segment {
|
|
|
|
|
file_id: file_id.0,
|
|
|
|
|
track_id: trk_id.0,
|
|
|
|
|
segment_ids: trkseg_ids.iter().map(|id| id.0).collect(),
|
|
|
|
|
},
|
|
|
|
|
S::Waypoints { file_id } => Self::Waypoints { file_id: file_id.0 },
|
|
|
|
|
S::Waypoint { file_id, wpt_ids } => Self::Waypoint {
|
|
|
|
|
file_id: file_id.0,
|
|
|
|
|
waypoint_ids: wpt_ids.iter().map(|id| id.0).collect(),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<Selection> for engine::Selection {
|
|
|
|
|
fn from(selection: Selection) -> Self {
|
|
|
|
|
use engine::Selection as S;
|
|
|
|
|
match selection {
|
|
|
|
|
Selection::Empty => S::Empty,
|
|
|
|
|
Selection::File { file_ids } => S::File {
|
|
|
|
|
file_ids: file_ids.into_iter().map(FileId).collect(),
|
|
|
|
|
},
|
2026-10-26 18:51:56 +01:00
|
|
|
Selection::Route { file_id, route_ids } => S::Route {
|
|
|
|
|
file_id: FileId(file_id),
|
|
|
|
|
rte_ids: route_ids.into_iter().map(engine::RouteId).collect(),
|
|
|
|
|
},
|
2026-10-14 08:51:38 +02:00
|
|
|
Selection::Track { file_id, track_ids } => S::Track {
|
|
|
|
|
file_id: FileId(file_id),
|
|
|
|
|
trk_ids: track_ids.into_iter().map(engine::TrackId).collect(),
|
|
|
|
|
},
|
|
|
|
|
Selection::Segment {
|
|
|
|
|
file_id,
|
|
|
|
|
track_id,
|
|
|
|
|
segment_ids,
|
|
|
|
|
} => S::TrackSegment {
|
|
|
|
|
file_id: FileId(file_id),
|
|
|
|
|
trk_id: engine::TrackId(track_id),
|
|
|
|
|
trkseg_ids: segment_ids
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(engine::TrackSegmentId)
|
|
|
|
|
.collect(),
|
|
|
|
|
},
|
|
|
|
|
Selection::Waypoints { file_id } => S::Waypoints {
|
|
|
|
|
file_id: FileId(file_id),
|
|
|
|
|
},
|
|
|
|
|
Selection::Waypoint {
|
|
|
|
|
file_id,
|
|
|
|
|
waypoint_ids,
|
|
|
|
|
} => S::Waypoint {
|
|
|
|
|
file_id: FileId(file_id),
|
|
|
|
|
wpt_ids: waypoint_ids.into_iter().map(engine::WaypointId).collect(),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Where moved elements go. A negative index is the start, an infinite one the end (which JSON
|
|
|
|
|
/// writes as `null`, so the frontend sends a very large number instead).
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
#[serde(
|
|
|
|
|
tag = "type",
|
|
|
|
|
rename_all = "camelCase",
|
|
|
|
|
rename_all_fields = "camelCase"
|
|
|
|
|
)]
|
|
|
|
|
pub enum MoveTarget {
|
|
|
|
|
Files {
|
|
|
|
|
index: f64,
|
|
|
|
|
},
|
2026-10-26 18:51:56 +01:00
|
|
|
Routes {
|
|
|
|
|
file_id: Uuid,
|
|
|
|
|
index: f64,
|
|
|
|
|
},
|
2026-10-14 08:51:38 +02:00
|
|
|
Tracks {
|
|
|
|
|
file_id: Uuid,
|
|
|
|
|
index: f64,
|
|
|
|
|
},
|
|
|
|
|
Segments {
|
|
|
|
|
file_id: Uuid,
|
|
|
|
|
track_id: Uuid,
|
|
|
|
|
index: f64,
|
|
|
|
|
},
|
|
|
|
|
Waypoints {
|
|
|
|
|
file_id: Uuid,
|
|
|
|
|
index: f64,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<MoveTarget> for engine::MoveTarget {
|
|
|
|
|
fn from(target: MoveTarget) -> Self {
|
|
|
|
|
use engine::MoveTarget as T;
|
|
|
|
|
let index = |index: f64| index.max(0.0) as usize;
|
|
|
|
|
match target {
|
|
|
|
|
MoveTarget::Files { index: i } => T::Files { index: index(i) },
|
2026-10-26 18:51:56 +01:00
|
|
|
MoveTarget::Routes { file_id, index: i } => T::Routes {
|
|
|
|
|
file_id: FileId(file_id),
|
|
|
|
|
index: index(i),
|
|
|
|
|
},
|
2026-10-14 08:51:38 +02:00
|
|
|
MoveTarget::Tracks { file_id, index: i } => T::Tracks {
|
|
|
|
|
file_id: FileId(file_id),
|
|
|
|
|
index: index(i),
|
|
|
|
|
},
|
|
|
|
|
MoveTarget::Segments {
|
|
|
|
|
file_id,
|
|
|
|
|
track_id,
|
|
|
|
|
index: i,
|
|
|
|
|
} => T::Segments {
|
|
|
|
|
file_id: FileId(file_id),
|
|
|
|
|
trk_id: engine::TrackId(track_id),
|
|
|
|
|
index: index(i),
|
|
|
|
|
},
|
|
|
|
|
MoveTarget::Waypoints { file_id, index: i } => T::Waypoints {
|
|
|
|
|
file_id: FileId(file_id),
|
|
|
|
|
index: index(i),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct Clipboard {
|
|
|
|
|
#[serde(rename = "type")]
|
|
|
|
|
kind: &'static str,
|
|
|
|
|
ids: Vec<String>,
|
|
|
|
|
cut: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Clipboard {
|
|
|
|
|
pub fn new(clipboard: &engine::Clipboard) -> Self {
|
|
|
|
|
let strings = |ids: Vec<Uuid>| ids.iter().map(Uuid::to_string).collect();
|
|
|
|
|
let (kind, ids) = match clipboard.content.ids() {
|
|
|
|
|
engine::ClipboardIds::Files(ids) => {
|
|
|
|
|
("files", strings(ids.into_iter().map(|id| id.0).collect()))
|
|
|
|
|
}
|
2026-10-26 18:51:56 +01:00
|
|
|
engine::ClipboardIds::Routes(ids) => {
|
|
|
|
|
("routes", strings(ids.into_iter().map(|id| id.0).collect()))
|
|
|
|
|
}
|
2026-10-14 08:51:38 +02:00
|
|
|
engine::ClipboardIds::Tracks(ids) => {
|
|
|
|
|
("tracks", strings(ids.into_iter().map(|id| id.0).collect()))
|
|
|
|
|
}
|
|
|
|
|
engine::ClipboardIds::Segments(ids) => (
|
|
|
|
|
"segments",
|
|
|
|
|
strings(ids.into_iter().map(|id| id.0).collect()),
|
|
|
|
|
),
|
|
|
|
|
engine::ClipboardIds::Waypoints(ids) => (
|
|
|
|
|
"waypoints",
|
|
|
|
|
strings(ids.into_iter().map(|id| id.0).collect()),
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
Self {
|
|
|
|
|
kind,
|
|
|
|
|
ids,
|
|
|
|
|
cut: clipboard.cut,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Files
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
#[serde(rename_all = "camelCase")]
|
|
|
|
|
pub struct FileStructure {
|
|
|
|
|
id: String,
|
|
|
|
|
name: String,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
desc: Option<String>,
|
2026-10-26 18:51:56 +01:00
|
|
|
routes: Vec<RouteNode>,
|
2026-10-14 08:51:38 +02:00
|
|
|
tracks: Vec<TrackNode>,
|
|
|
|
|
waypoints: Vec<WaypointNode>,
|
|
|
|
|
waypoints_rev: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-26 18:51:56 +01:00
|
|
|
/// The name, description and style of a route or a track.
|
2026-10-14 08:51:38 +02:00
|
|
|
#[derive(Serialize)]
|
2026-10-26 18:51:56 +01:00
|
|
|
struct PathInfo {
|
2026-10-14 08:51:38 +02:00
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
name: Option<String>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
desc: Option<String>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
color: Option<String>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
opacity: Option<f64>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
width: Option<f64>,
|
2026-10-26 18:51:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&engine::PathInfo> for PathInfo {
|
|
|
|
|
fn from(info: &engine::PathInfo) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
name: info.name.clone(),
|
|
|
|
|
desc: info.desc.clone(),
|
|
|
|
|
color: info.color.clone(),
|
|
|
|
|
opacity: info.opacity,
|
|
|
|
|
width: info.width,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct RouteNode {
|
|
|
|
|
id: String,
|
|
|
|
|
#[serde(flatten)]
|
|
|
|
|
info: PathInfo,
|
|
|
|
|
segment: SegmentNode,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct TrackNode {
|
|
|
|
|
id: String,
|
|
|
|
|
#[serde(flatten)]
|
|
|
|
|
info: PathInfo,
|
2026-10-14 08:51:38 +02:00
|
|
|
segments: Vec<SegmentNode>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct SegmentNode {
|
|
|
|
|
id: String,
|
|
|
|
|
rev: String,
|
|
|
|
|
length: f64,
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-26 18:51:56 +01:00
|
|
|
impl From<&engine::SegmentNode> for SegmentNode {
|
|
|
|
|
fn from(seg: &engine::SegmentNode) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
id: seg.id.0.to_string(),
|
|
|
|
|
rev: seg.rev_id.0.to_string(),
|
|
|
|
|
length: seg.len as f64,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-14 08:51:38 +02:00
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct WaypointNode {
|
|
|
|
|
id: String,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
name: Option<String>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
sym: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FileStructure {
|
|
|
|
|
pub fn new(file: &engine::FileStructure) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
id: file.id.0.to_string(),
|
|
|
|
|
name: file.name.clone(),
|
|
|
|
|
desc: file.desc.clone(),
|
2026-10-26 18:51:56 +01:00
|
|
|
routes: file
|
|
|
|
|
.routes
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|rte| RouteNode {
|
|
|
|
|
id: rte.id.0.to_string(),
|
|
|
|
|
info: (&rte.info).into(),
|
|
|
|
|
segment: (&rte.segment).into(),
|
|
|
|
|
})
|
|
|
|
|
.collect(),
|
2026-10-14 08:51:38 +02:00
|
|
|
tracks: file
|
|
|
|
|
.tracks
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|trk| TrackNode {
|
|
|
|
|
id: trk.id.0.to_string(),
|
2026-10-26 18:51:56 +01:00
|
|
|
info: (&trk.info).into(),
|
|
|
|
|
segments: trk.segments.iter().map(Into::into).collect(),
|
2026-10-14 08:51:38 +02:00
|
|
|
})
|
|
|
|
|
.collect(),
|
|
|
|
|
waypoints: file
|
|
|
|
|
.waypoints
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|wpt| WaypointNode {
|
|
|
|
|
id: wpt.id.0.to_string(),
|
|
|
|
|
name: wpt.name.clone(),
|
|
|
|
|
sym: wpt.sym.clone(),
|
|
|
|
|
})
|
|
|
|
|
.collect(),
|
|
|
|
|
waypoints_rev: file.wpt_rev_id.0.to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
#[serde(rename_all = "camelCase")]
|
|
|
|
|
pub struct GlobalStatistics {
|
|
|
|
|
total_distance: f64,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
moving_distance: Option<f64>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
total_time: Option<f64>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
moving_time: Option<f64>,
|
|
|
|
|
elevation_gain: f64,
|
|
|
|
|
elevation_loss: f64,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
start_time: Option<f64>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
end_time: Option<f64>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
total_speed: Option<f64>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
moving_speed: Option<f64>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
bounds: Option<Bounds>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct Bounds {
|
|
|
|
|
west: f64,
|
|
|
|
|
south: f64,
|
|
|
|
|
east: f64,
|
|
|
|
|
north: f64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl GlobalStatistics {
|
|
|
|
|
pub fn new(stats: &engine::GlobalStatistics) -> Self {
|
|
|
|
|
let seconds = |ms: Option<i64>| ms.map(|ms| ms as f64 / 1000.0);
|
|
|
|
|
let (sw, ne) = (&stats.bounds.sw, &stats.bounds.ne);
|
|
|
|
|
Self {
|
|
|
|
|
total_distance: stats.total_distance,
|
|
|
|
|
moving_distance: stats.moving_distance,
|
|
|
|
|
total_time: seconds(stats.total_time),
|
|
|
|
|
moving_time: seconds(stats.moving_time),
|
|
|
|
|
elevation_gain: stats.elevation_gain,
|
|
|
|
|
elevation_loss: stats.elevation_loss,
|
|
|
|
|
start_time: stats.start_time.map(|t| t as f64),
|
|
|
|
|
end_time: stats.end_time.map(|t| t as f64),
|
|
|
|
|
total_speed: stats.total_speed(),
|
|
|
|
|
moving_speed: stats.moving_speed(),
|
|
|
|
|
bounds: (sw.lng <= ne.lng && sw.lat <= ne.lat).then_some(Bounds {
|
|
|
|
|
west: sw.lng,
|
|
|
|
|
south: sw.lat,
|
|
|
|
|
east: ne.lng,
|
|
|
|
|
north: ne.lat,
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct WaypointDetails {
|
|
|
|
|
id: String,
|
|
|
|
|
lng: f64,
|
|
|
|
|
lat: f64,
|
|
|
|
|
ele: f64,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
time: Option<f64>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
name: Option<String>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
desc: Option<String>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
cmt: Option<String>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
sym: Option<String>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
links: Option<Vec<Link>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct Link {
|
|
|
|
|
href: String,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
text: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl WaypointDetails {
|
|
|
|
|
pub fn new(wpt: &engine::Waypoint) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
id: wpt.id.0.to_string(),
|
|
|
|
|
lng: wpt.coordinates.lng,
|
|
|
|
|
lat: wpt.coordinates.lat,
|
|
|
|
|
ele: wpt.ele,
|
|
|
|
|
time: wpt.time.map(|t| t as f64),
|
|
|
|
|
name: wpt.name.clone(),
|
|
|
|
|
desc: wpt.desc.clone(),
|
|
|
|
|
cmt: wpt.cmt.clone(),
|
|
|
|
|
sym: wpt.sym.clone(),
|
|
|
|
|
links: (!wpt.links.is_empty()).then(|| {
|
|
|
|
|
wpt.links
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|link| Link {
|
|
|
|
|
href: link.href.clone(),
|
|
|
|
|
text: link.text.clone(),
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct TrackpointDetails {
|
|
|
|
|
lng: f64,
|
|
|
|
|
lat: f64,
|
|
|
|
|
ele: f64,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
time: Option<f64>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TrackpointDetails {
|
|
|
|
|
pub fn new(pt: &engine::Trackpoint) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
lng: pt.coordinates.lng,
|
|
|
|
|
lat: pt.coordinates.lat,
|
|
|
|
|
ele: pt.ele,
|
|
|
|
|
time: pt.time.map(|t| t as f64),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-18 12:35:32 +02:00
|
|
|
/// A trackpoint of the selection: what the statistics buffers do not hold for each trackpoint.
|
|
|
|
|
/// Absent fields are unknown.
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
#[serde(rename_all = "camelCase")]
|
|
|
|
|
pub struct SelectionPoint {
|
2026-10-18 16:20:32 +02:00
|
|
|
/// Slope (%) at the trackpoint.
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub slope: Option<f64>,
|
2026-10-18 12:35:32 +02:00
|
|
|
/// ms since the epoch.
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub time: Option<f64>,
|
|
|
|
|
/// Slope (%) and length (km) of the smoothed elevation segment of the trackpoint.
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub slope_segment_slope: Option<f64>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub slope_segment_distance: Option<f64>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub surface: Option<String>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub highway: Option<String>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub sac_scale: Option<String>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub mtb_scale: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-14 08:51:38 +02:00
|
|
|
// Routing
|
|
|
|
|
|
|
|
|
|
/// A category of the trackpoints of a route, as in `RouteCategory` of the engine.
|
|
|
|
|
#[derive(Default, Deserialize)]
|
|
|
|
|
pub struct RouteCategory {
|
|
|
|
|
pub starts: Vec<u32>,
|
|
|
|
|
pub values: Vec<u8>,
|
|
|
|
|
pub names: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RouteCategory {
|
|
|
|
|
pub fn view(&self) -> engine::RouteCategory<'_> {
|
|
|
|
|
engine::RouteCategory {
|
|
|
|
|
starts: &self.starts,
|
|
|
|
|
values: &self.values,
|
|
|
|
|
names: &self.names,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Default, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "camelCase", default)]
|
|
|
|
|
pub struct RouteAttributes {
|
|
|
|
|
pub surface: RouteCategory,
|
|
|
|
|
pub highway: RouteCategory,
|
|
|
|
|
pub sac_scale: RouteCategory,
|
|
|
|
|
pub mtb_scale: RouteCategory,
|
|
|
|
|
}
|