//! 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. use crate as engine; use engine::FileId; use serde::{Deserialize, Serialize}; use uuid::Uuid; pub fn ids(items: &[FileId]) -> Vec { 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 { changed: bool, order_changed: bool, selection_changed: bool, clipboard_changed: bool, added: Vec, modified: Vec, removed: Vec, #[serde(skip_serializing_if = "Option::is_none")] error: Option, /// Whether the files changed in a way that has to be saved (not sent). #[serde(skip)] pub saves: bool, } impl Outcome { pub fn new(outcome: &engine::Outcome, error: Option) -> 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, saves: outcome.changed && (outcome.diff.is_some() || outcome.order_changed), } } /// The outcome of a call that did nothing and has nothing to say. pub fn failed_none() -> Self { Self::new(&engine::Outcome::default(), None) } /// What a call that does nothing returns. pub fn failed(message: impl Into) -> 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, }, Track { file_id: Uuid, track_ids: Vec, }, Segment { file_id: Uuid, track_id: Uuid, segment_ids: Vec, }, Waypoints { file_id: Uuid, }, Waypoint { file_id: Uuid, waypoint_ids: Vec, }, } 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(), }, 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 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(), }, 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, }, Tracks { file_id: Uuid, index: f64, }, Segments { file_id: Uuid, track_id: Uuid, index: f64, }, Waypoints { file_id: Uuid, index: f64, }, } impl From 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) }, 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, cut: bool, } impl Clipboard { pub fn new(clipboard: &engine::Clipboard) -> Self { let strings = |ids: Vec| 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())) } 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, tracks: Vec, waypoints: Vec, waypoints_rev: String, } #[derive(Serialize)] struct TrackNode { id: String, #[serde(skip_serializing_if = "Option::is_none")] name: Option, #[serde(skip_serializing_if = "Option::is_none")] desc: Option, #[serde(skip_serializing_if = "Option::is_none")] color: Option, #[serde(skip_serializing_if = "Option::is_none")] opacity: Option, #[serde(skip_serializing_if = "Option::is_none")] width: Option, segments: Vec, } #[derive(Serialize)] struct SegmentNode { id: String, rev: String, length: f64, } #[derive(Serialize)] struct WaypointNode { id: String, #[serde(skip_serializing_if = "Option::is_none")] name: Option, #[serde(skip_serializing_if = "Option::is_none")] sym: Option, } impl FileStructure { pub fn new(file: &engine::FileStructure) -> Self { Self { id: file.id.0.to_string(), name: file.name.clone(), desc: file.desc.clone(), tracks: file .tracks .iter() .map(|trk| TrackNode { id: trk.id.0.to_string(), name: trk.name.clone(), desc: trk.desc.clone(), color: trk.color.clone(), opacity: trk.opacity, width: trk.width, segments: trk .segments .iter() .map(|seg| SegmentNode { id: seg.id.0.to_string(), rev: seg.rev_id.0.to_string(), length: seg.len as f64, }) .collect(), }) .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, #[serde(skip_serializing_if = "Option::is_none")] total_time: Option, #[serde(skip_serializing_if = "Option::is_none")] moving_time: Option, elevation_gain: f64, elevation_loss: f64, #[serde(skip_serializing_if = "Option::is_none")] start_time: Option, #[serde(skip_serializing_if = "Option::is_none")] end_time: Option, #[serde(skip_serializing_if = "Option::is_none")] total_speed: Option, #[serde(skip_serializing_if = "Option::is_none")] moving_speed: Option, #[serde(skip_serializing_if = "Option::is_none")] bounds: Option, } #[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| 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, #[serde(skip_serializing_if = "Option::is_none")] name: Option, #[serde(skip_serializing_if = "Option::is_none")] desc: Option, #[serde(skip_serializing_if = "Option::is_none")] cmt: Option, #[serde(skip_serializing_if = "Option::is_none")] sym: Option, #[serde(skip_serializing_if = "Option::is_none")] links: Option>, } #[derive(Serialize)] struct Link { href: String, #[serde(skip_serializing_if = "Option::is_none")] text: Option, } 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, } 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), } } } // Routing /// A category of the trackpoints of a route, as in `RouteCategory` of the engine. #[derive(Default, Deserialize)] pub struct RouteCategory { pub starts: Vec, pub values: Vec, pub names: Vec, } 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, }