This commit is contained in:
vcoppe committed 2026-10-14 09:22:07 +02:00
1 parent 41b9116c6a
commit 97d597e139
181 files changed
+1970 -2643

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
//! Typed arrays and files cross as raw bytes (little-endian), not as JSON.
pub type Bytes = tauri::ipc::Response;
pub fn f64s(values: &[f64]) -> Bytes {
Bytes::new(
values
.iter()
.flat_map(|v| v.to_le_bytes())
.collect::<Vec<u8>>(),
)
}
pub fn u32s(values: &[u32]) -> Bytes {
Bytes::new(
values
.iter()
.flat_map(|v| v.to_le_bytes())
.collect::<Vec<u8>>(),
)
}
pub fn u8s(values: &[u8]) -> Bytes {
Bytes::new(values.to_vec())
}
/// An array that can be absent: the first byte tells whether it is there, the array follows.
pub fn optional(array: Option<Vec<u8>>) -> Bytes {
let mut bytes = vec![u8::from(array.is_some())];
bytes.extend(array.unwrap_or_default());
Bytes::new(bytes)
}
pub fn f64_bytes(values: &[f64]) -> Vec<u8> {
values.iter().flat_map(|v| v.to_le_bytes()).collect()
}
pub fn i64_bytes(values: &[i64]) -> Vec<u8> {
values.iter().flat_map(|v| v.to_le_bytes()).collect()
}
+98 -126
View File
@@ -1,22 +1,15 @@
//! Editing the metadata, the style, the structure and the waypoints of the files, and the tools:
//! reversing, cropping, reducing, timestamps, merging, cleaning...
use gpx_engine::{self as engine, Command, LngLat, LngLatBounds};
use gpx_engine::{api, CleanType, MergeType, SplitType};
use tauri::State;
use uuid::Uuid;
use crate::dto::*;
use crate::state::{act, edit, AppState};
use crate::state::AppState;
#[tauri::command(async)]
pub fn metadata(state: State<AppState>, name: String, desc: String) -> Outcome {
edit!(
state,
Command::Metadata(engine::Metadata {
name: &name,
desc: &desc,
})
)
pub fn metadata(state: State<AppState>, name: String, desc: String) -> api::Outcome {
state.run(move |e| api::metadata(e, &name, &desc))
}
#[tauri::command(async)]
@@ -25,25 +18,18 @@ pub fn style(
color: Option<String>,
opacity: Option<f64>,
width: Option<f64>,
) -> Outcome {
edit!(
state,
Command::Style(engine::Style {
color: color.as_deref(),
opacity,
width,
})
)
) -> api::Outcome {
state.run(move |e| api::style(e, color.as_deref(), opacity, width))
}
#[tauri::command(async)]
pub fn new_track(state: State<AppState>) -> Outcome {
edit!(state, Command::NewTrack(engine::NewTrack))
pub fn new_track(state: State<AppState>) -> api::Outcome {
state.run(api::new_track)
}
#[tauri::command(async)]
pub fn new_track_segment(state: State<AppState>) -> Outcome {
edit!(state, Command::NewTrackSegment(engine::NewTrackSegment))
pub fn new_track_segment(state: State<AppState>) -> api::Outcome {
state.run(api::new_track_segment)
}
// Waypoints
@@ -59,19 +45,8 @@ pub fn new_waypoint(
desc: String,
icon: String,
link: String,
) -> Outcome {
edit!(
state,
Command::NewWaypoint(engine::NewWaypoint {
lng,
lat,
ele,
name: &name,
desc: &desc,
icon: &icon,
link: &link,
})
)
) -> api::Outcome {
state.run(move |e| api::new_waypoint(e, lng, lat, ele, &name, &desc, &icon, &link))
}
#[tauri::command(async)]
@@ -82,28 +57,13 @@ pub fn move_waypoint(
lng: f64,
lat: f64,
ele: f64,
) -> Outcome {
edit!(
state,
Command::MoveWaypoint(engine::MoveWaypoint {
file_id: engine::FileId(file_id),
waypoint_id: engine::WaypointId(waypoint_id),
lng,
lat,
ele,
})
)
) -> api::Outcome {
state.run(move |e| api::move_waypoint(e, file_id, waypoint_id, lng, lat, ele))
}
#[tauri::command(async)]
pub fn delete_waypoint(state: State<AppState>, file_id: Uuid, waypoint_id: Uuid) -> Outcome {
edit!(
state,
Command::DeleteWaypoint(engine::DeleteWaypoint {
file_id: engine::FileId(file_id),
waypoint_id: engine::WaypointId(waypoint_id),
})
)
pub fn delete_waypoint(state: State<AppState>, file_id: Uuid, waypoint_id: Uuid) -> api::Outcome {
state.run(move |e| api::delete_waypoint(e, file_id, waypoint_id))
}
#[tauri::command(async)]
@@ -119,46 +79,43 @@ pub fn update_waypoint(
desc: String,
icon: String,
link: String,
) -> Outcome {
edit!(
state,
Command::EditWaypoint(engine::EditWaypoint {
file_id: engine::FileId(file_id),
waypoint_id: engine::WaypointId(waypoint_id),
) -> api::Outcome {
state.run(move |e| {
api::update_waypoint(
e,
file_id,
waypoint_id,
lng,
lat,
ele,
name: &name,
desc: &desc,
icon: &icon,
link: &link,
})
)
&name,
&desc,
&icon,
&link,
)
})
}
// Tools
#[tauri::command(async)]
pub fn reverse(state: State<AppState>) -> Outcome {
edit!(state, Command::Reverse(engine::Reverse))
pub fn reverse(state: State<AppState>) -> api::Outcome {
state.run(api::reverse)
}
#[tauri::command(async)]
pub fn round_trip(state: State<AppState>) -> Outcome {
edit!(state, Command::RoundTrip(engine::RoundTrip))
pub fn round_trip(state: State<AppState>) -> api::Outcome {
state.run(api::round_trip)
}
#[tauri::command(async)]
pub fn crop(state: State<AppState>, start: u32, end: u32) -> Outcome {
if start > end {
return Outcome::failed(format!("the range {start}..{end} is reversed"));
}
edit!(state, Command::Crop(engine::Crop { start, end }))
pub fn crop(state: State<AppState>, start: u32, end: u32) -> api::Outcome {
state.run(move |e| api::crop(e, start, end))
}
#[tauri::command(async)]
pub fn reduce(state: State<AppState>, tolerance: f64) -> Outcome {
edit!(state, Command::Reduce(engine::Reduce { tolerance }))
pub fn reduce(state: State<AppState>, tolerance: f64) -> api::Outcome {
state.run(move |e| api::reduce(e, tolerance))
}
#[tauri::command(async)]
@@ -167,14 +124,8 @@ pub fn change_timestamps(
start_time: f64,
speed: f64,
ratio: f64,
) -> Outcome {
edit!(
state,
Command::Time(engine::Time {
start_time: start_time as i64,
kind: engine::TimeKind::Change { speed, ratio },
})
)
) -> api::Outcome {
state.run(move |e| api::change_timestamps(e, start_time, speed, ratio))
}
#[tauri::command(async)]
@@ -182,35 +133,23 @@ pub fn create_artificial_timestamps(
state: State<AppState>,
start_time: f64,
total_time: f64,
) -> Outcome {
edit!(
state,
Command::Time(engine::Time {
start_time: start_time as i64,
kind: engine::TimeKind::Artificial { total_time },
})
)
) -> api::Outcome {
state.run(move |e| api::create_artificial_timestamps(e, start_time, total_time))
}
#[tauri::command(async)]
pub fn merge(state: State<AppState>, merge_type: MergeType, remove_gaps: bool) -> Outcome {
edit!(
state,
Command::Merge(engine::Merge {
type_: merge_type.into(),
remove_gaps,
})
)
pub fn merge(state: State<AppState>, merge_type: MergeType, remove_gaps: bool) -> api::Outcome {
state.run(move |e| api::merge(e, merge_type, remove_gaps))
}
#[tauri::command(async)]
pub fn extract(state: State<AppState>) -> Outcome {
edit!(state, Command::Extract(engine::Extract))
pub fn extract(state: State<AppState>) -> api::Outcome {
state.run(api::extract)
}
#[tauri::command(async)]
pub fn elevation(state: State<AppState>, ele: Vec<f64>) -> Outcome {
edit!(state, Command::Elevation(engine::Elevation { ele: &ele }))
pub fn elevation(state: State<AppState>, ele: Vec<f64>) -> api::Outcome {
state.run(move |e| api::elevation(e, &ele))
}
#[tauri::command(async)]
@@ -224,23 +163,56 @@ pub fn clean(
clean_type: CleanType,
trkpt: bool,
wpt: bool,
) -> Outcome {
edit!(
state,
Command::Clean(engine::Clean {
bounds: LngLatBounds {
sw: LngLat {
lng: west,
lat: south,
},
ne: LngLat {
lng: east,
lat: north,
},
},
type_: clean_type.into(),
trkpt,
wpt,
})
)
) -> api::Outcome {
state.run(move |e| api::clean(e, west, south, east, north, clean_type, trkpt, wpt))
}
// Routing
#[tauri::command(async)]
#[allow(clippy::too_many_arguments)]
pub fn route(
state: State<AppState>,
revision: u32,
start: u32,
end: u32,
lng: Vec<f64>,
lat: Vec<f64>,
ele: Vec<f64>,
attributes: api::RouteAttributes,
anchors: Vec<u32>,
) -> api::Outcome {
state.run(move |e| {
api::route(
e,
revision,
start,
end,
&lng,
&lat,
&ele,
&attributes,
&anchors,
)
})
}
#[tauri::command(async)]
pub fn insert_anchor(state: State<AppState>, revision: u32, lng: f64, lat: f64) -> api::Outcome {
state.run(move |e| api::insert_anchor(e, revision, lng, lat))
}
#[tauri::command(async)]
pub fn change_loop_start(state: State<AppState>, revision: u32, index: u32) -> api::Outcome {
state.run(move |e| api::change_loop_start(e, revision, index))
}
#[tauri::command(async)]
pub fn split(
state: State<AppState>,
revision: u32,
at: u32,
split_type: SplitType,
) -> api::Outcome {
state.run(move |e| api::split(e, revision, at, split_type))
}
+37 -84
View File
@@ -1,12 +1,11 @@
//! Creating, loading, deleting, moving, copying and pasting files and their elements, and undoing.
use gpx_engine::{self as engine, Action, Command};
use gpx_engine::api;
use tauri::ipc::{InvokeBody, Request};
use tauri::State;
use uuid::Uuid;
use crate::dto::*;
use crate::state::{act, edit, AppState};
use crate::state::AppState;
#[tauri::command(async)]
pub fn new_file(
@@ -15,73 +14,35 @@ pub fn new_file(
lng: Option<f64>,
lat: Option<f64>,
ele: Option<f64>,
) -> Outcome {
let trackpoint = lng.zip(lat).map(|(lng, lat)| engine::NewTrackpoint {
lng,
lat,
ele: ele.unwrap_or_default(),
});
edit!(
state,
Command::New(engine::New {
name: &name,
trackpoint,
})
)
) -> api::Outcome {
state.run(move |e| api::new_file(e, &name, lng, lat, ele))
}
/// The files are in the body, one after the other, the headers `lengths` and `names` (JSON) telling
/// the number of bytes and the name of each one.
/// The files are in the body, one after the other, the headers `lengths` and `names` (JSON,
/// percent-encoded) telling the number of bytes and the name of each one.
#[tauri::command(async)]
pub fn load_files(state: State<AppState>, request: Request<'_>) -> Outcome {
pub fn load_files(state: State<AppState>, request: Request<'_>) -> api::Outcome {
let InvokeBody::Raw(data) = request.body() else {
return Outcome::failed("the files are missing");
return api::Outcome::failed("the files are missing");
};
let header = |key: &str| {
request
.headers()
.get(key)
.and_then(|value| value.to_str().ok())
.and_then(urlencoding_decode)
.and_then(percent_decode)
};
let (Some(lengths), Some(names)) = (
header("lengths").and_then(|json| serde_json::from_str::<Vec<usize>>(&json).ok()),
header("names").and_then(|json| serde_json::from_str::<Vec<String>>(&json).ok()),
) else {
return Outcome::failed("the lengths or the names of the files are not valid");
return api::Outcome::failed("the lengths or the names of the files are not valid");
};
if lengths.len() != names.len() {
return Outcome::failed(format!(
"{} lengths for {} names",
lengths.len(),
names.len()
));
}
let (data, mut offset) = (data.clone(), 0);
let mut ranges = Vec::with_capacity(names.len());
for (length, name) in lengths.iter().zip(&names) {
let end = offset + length;
if end > data.len() {
return Outcome::failed(format!("the data of {name} is shorter than its length"));
}
ranges.push(offset..end);
offset = end;
}
edit!(state, {
let files = ranges
.into_iter()
.zip(&names)
.map(|(range, name)| engine::Load {
data: &data[range],
name,
})
.collect();
Command::LoadFiles(engine::LoadFiles { files })
})
let data = data.clone();
state.run(move |e| api::load_files(e, &data, &lengths, &names))
}
/// Headers are ASCII: the frontend percent-encodes the JSON.
fn urlencoding_decode(value: &str) -> Option<String> {
fn percent_decode(value: &str) -> Option<String> {
let bytes = value.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut i = 0;
@@ -99,63 +60,55 @@ fn urlencoding_decode(value: &str) -> Option<String> {
}
#[tauri::command(async)]
pub fn duplicate(state: State<AppState>) -> Outcome {
edit!(state, Command::Duplicate(engine::Duplicate))
pub fn duplicate(state: State<AppState>) -> api::Outcome {
state.run(api::duplicate)
}
#[tauri::command(async)]
pub fn delete(state: State<AppState>, whole_files: bool) -> Outcome {
edit!(state, Command::Delete(engine::Delete { whole_files }))
pub fn delete(state: State<AppState>, whole_files: bool) -> api::Outcome {
state.run(move |e| api::delete(e, whole_files))
}
#[tauri::command(async)]
pub fn delete_all(state: State<AppState>) -> Outcome {
edit!(state, Command::DeleteAll(engine::DeleteAll))
pub fn delete_all(state: State<AppState>) -> api::Outcome {
state.run(api::delete_all)
}
#[tauri::command(async)]
pub fn reorder(state: State<AppState>, file_ids: Vec<Uuid>, index: usize) -> Outcome {
act!(
state,
Action::Reorder {
file_ids: file_ids.into_iter().map(engine::FileId).collect(),
index,
}
)
pub fn reorder(state: State<AppState>, file_ids: Vec<Uuid>, index: usize) -> api::Outcome {
state.run(move |e| api::reorder(e, &file_ids, index))
}
#[tauri::command(async)]
pub fn copy(state: State<AppState>) -> Outcome {
act!(state, Action::Copy)
pub fn copy(state: State<AppState>) -> api::Outcome {
state.run(api::copy)
}
#[tauri::command(async)]
pub fn cut(state: State<AppState>) -> Outcome {
act!(state, Action::Cut)
pub fn cut(state: State<AppState>) -> api::Outcome {
state.run(api::cut)
}
#[tauri::command(async)]
pub fn paste(state: State<AppState>) -> Outcome {
edit!(state, Command::Paste(engine::Paste))
pub fn paste(state: State<AppState>) -> api::Outcome {
state.run(api::paste)
}
#[tauri::command(async)]
pub fn move_elements(state: State<AppState>, what: Selection, to: MoveTarget) -> Outcome {
edit!(
state,
Command::Move(engine::Move {
what: what.into(),
to: to.into(),
})
)
pub fn move_elements(
state: State<AppState>,
what: api::Selection,
to: api::MoveTarget,
) -> api::Outcome {
state.run(move |e| api::move_elements(e, what, to))
}
#[tauri::command(async)]
pub fn undo(state: State<AppState>) -> Outcome {
act!(state, Action::Undo)
pub fn undo(state: State<AppState>) -> api::Outcome {
state.run(api::undo)
}
#[tauri::command(async)]
pub fn redo(state: State<AppState>) -> Outcome {
act!(state, Action::Redo)
pub fn redo(state: State<AppState>) -> api::Outcome {
state.run(api::redo)
}
-1
View File
@@ -8,7 +8,6 @@
pub mod edit;
pub mod files;
pub mod queries;
pub mod routing;
pub mod selection;
pub mod statistics;
pub mod storage;
+24 -46
View File
@@ -1,54 +1,51 @@
//! The files, their structure and statistics, the selection, the clipboard, and the coordinates of
//! the trackpoints and waypoints (flat `[lng, lat, ...]` little-endian `f64` arrays).
use gpx_engine::{self as engine, FileId};
use gpx_engine::{api, ExportOptions};
use tauri::State;
use uuid::Uuid;
use crate::dto::*;
use crate::bytes::*;
use crate::state::AppState;
#[tauri::command(async)]
pub fn file_order(state: State<AppState>) -> Vec<String> {
state.read(|e| ids(e.order()))
state.read(api::file_order)
}
#[tauri::command(async)]
pub fn file_structure(state: State<AppState>, file_id: Uuid) -> Option<FileStructure> {
state.read(move |e| e.file_structure(&FileId(file_id)).map(FileStructure::new))
pub fn file_structure(state: State<AppState>, file_id: Uuid) -> Option<api::FileStructure> {
state.read(move |e| api::file_structure(e, file_id))
}
#[tauri::command(async)]
pub fn file_statistics(state: State<AppState>, file_id: Uuid) -> Option<GlobalStatistics> {
state.read(move |e| {
e.file_statistics(&FileId(file_id))
.map(|s| GlobalStatistics::new(&s))
})
pub fn file_statistics(state: State<AppState>, file_id: Uuid) -> Option<api::GlobalStatistics> {
state.read(move |e| api::file_statistics(e, file_id))
}
#[tauri::command(async)]
pub fn selection(state: State<AppState>) -> Selection {
state.read(|e| e.selection().into())
pub fn selection(state: State<AppState>) -> api::Selection {
state.read(api::selection)
}
#[tauri::command(async)]
pub fn clipboard(state: State<AppState>) -> Option<Clipboard> {
state.read(|e| e.clipboard().map(Clipboard::new))
pub fn clipboard(state: State<AppState>) -> Option<api::Clipboard> {
state.read(api::clipboard)
}
#[tauri::command(async)]
pub fn can_undo(state: State<AppState>) -> bool {
state.read(|e| e.can_undo())
state.read(api::can_undo)
}
#[tauri::command(async)]
pub fn can_redo(state: State<AppState>) -> bool {
state.read(|e| e.can_redo())
state.read(api::can_redo)
}
#[tauri::command(async)]
pub fn can_paste(state: State<AppState>) -> bool {
state.read(|e| e.can_paste())
state.read(api::can_paste)
}
#[tauri::command(async)]
@@ -56,11 +53,8 @@ pub fn waypoint(
state: State<AppState>,
file_id: Uuid,
waypoint_id: Uuid,
) -> Option<WaypointDetails> {
state.read(move |e| {
e.waypoint(&FileId(file_id), &engine::WaypointId(waypoint_id))
.map(WaypointDetails::new)
})
) -> Option<api::WaypointDetails> {
state.read(move |e| api::waypoint(e, file_id, waypoint_id))
}
#[tauri::command(async)]
@@ -69,11 +63,8 @@ pub fn trackpoint(
file_id: Uuid,
segment_id: Uuid,
index: usize,
) -> Option<TrackpointDetails> {
state.read(move |e| {
e.trackpoint(&FileId(file_id), &engine::TrackSegmentId(segment_id), index)
.map(TrackpointDetails::new)
})
) -> Option<api::TrackpointDetails> {
state.read(move |e| api::trackpoint(e, file_id, segment_id, index))
}
/// A file as GPX (UTF-8 bytes), the first byte telling whether the file exists.
@@ -90,7 +81,7 @@ pub fn export_file(
osm: bool,
as_route: bool,
) -> Bytes {
let options = engine::ExportOptions {
let options = ExportOptions {
time,
hr,
cad,
@@ -99,38 +90,25 @@ pub fn export_file(
osm,
as_route,
};
optional(state.read(move |e| e.export(&FileId(file_id), options)))
optional(state.read(move |e| api::export_file(e, file_id, options)))
}
/// The data that some files have: a bit for the time (1), heart rate (2), cadence (4), temperature
/// (8), power (16) and the OpenStreetMap data (32).
#[tauri::command(async)]
pub fn exportable_data(state: State<AppState>, file_ids: Vec<Uuid>) -> u8 {
state.read(move |e| {
let ids: Vec<FileId> = file_ids.into_iter().map(FileId).collect();
let data = e.exportable_data(&ids);
[
data.time, data.hr, data.cad, data.atemp, data.power, data.osm,
]
.iter()
.enumerate()
.fold(0, |bits, (i, on)| bits | (u8::from(*on) << i))
})
state.read(move |e| api::exportable_data(e, &file_ids))
}
#[tauri::command(async)]
pub fn reduction_distances(state: State<AppState>) -> Bytes {
Bytes::new(state.read(|e| f64s(&e.reduction_distances())))
f64s(&state.read(api::reduction_distances))
}
#[tauri::command(async)]
pub fn segment_coordinates(state: State<AppState>, segment_id: Uuid) -> Bytes {
Bytes::new(
state.read(move |e| f64s(e.segment_coordinates(&engine::TrackSegmentId(segment_id)))),
)
f64s(&state.read(move |e| api::segment_coordinates(e, segment_id).to_vec()))
}
#[tauri::command(async)]
pub fn waypoint_coordinates(state: State<AppState>, file_id: Uuid) -> Bytes {
Bytes::new(state.read(move |e| f64s(e.waypoint_coordinates(&FileId(file_id)))))
f64s(&state.read(move |e| api::waypoint_coordinates(e, file_id).to_vec()))
}
-73
View File
@@ -1,73 +0,0 @@
//! The commands that refer to trackpoints by their index in the routing buffers, which only mean
//! something for the `revision` they were read with: the commands do nothing if the selection is
//! not the one of that revision anymore.
use gpx_engine::{self as engine, Command};
use tauri::State;
use crate::dto::*;
use crate::state::{edit_at, AppState};
#[tauri::command(async)]
#[allow(clippy::too_many_arguments)]
pub fn route(
state: State<AppState>,
revision: u32,
start: u32,
end: u32,
lng: Vec<f64>,
lat: Vec<f64>,
ele: Vec<f64>,
attributes: RouteAttributes,
anchors: Vec<u32>,
) -> Outcome {
if lng.len() != lat.len() || lat.len() != ele.len() {
return Outcome::failed("lng, lat and ele differ in length");
}
edit_at!(
state,
revision,
Command::Route(engine::Route {
start,
end,
lng: &lng,
lat: &lat,
ele: &ele,
surface: attributes.surface.view(),
highway: attributes.highway.view(),
sac_scale: attributes.sac_scale.view(),
mtb_scale: attributes.mtb_scale.view(),
anchors: &anchors,
})
)
}
#[tauri::command(async)]
pub fn insert_anchor(state: State<AppState>, revision: u32, lng: f64, lat: f64) -> Outcome {
edit_at!(
state,
revision,
Command::InsertAnchor(engine::InsertAnchor { lng, lat })
)
}
#[tauri::command(async)]
pub fn change_loop_start(state: State<AppState>, revision: u32, index: u32) -> Outcome {
edit_at!(
state,
revision,
Command::ChangeLoopStart(engine::ChangeLoopStart { index })
)
}
#[tauri::command(async)]
pub fn split(state: State<AppState>, revision: u32, at: u32, split_type: SplitType) -> Outcome {
edit_at!(
state,
revision,
Command::Split(engine::Split {
at,
split_type: split_type.into(),
})
)
}
+21 -61
View File
@@ -1,49 +1,24 @@
//! Changing what is selected.
use gpx_engine::{self as engine, Action};
use gpx_engine::{api, SelectMode};
use tauri::State;
use uuid::Uuid;
use crate::dto::*;
use crate::state::{act, AppState};
use crate::state::AppState;
fn select_elements(
state: &AppState,
selection: Option<engine::Selection>,
mode: SelectMode,
) -> Outcome {
match selection {
Some(selection) => act!(
state,
Action::Select {
selection,
mode: mode.into(),
}
),
None => Outcome::failed("the ids are not UUIDs"),
}
}
fn to_file_ids(ids: Vec<Uuid>) -> Vec<engine::FileId> {
ids.into_iter().map(engine::FileId).collect()
#[tauri::command(async)]
pub fn select(state: State<AppState>, file_ids: Vec<Uuid>, mode: SelectMode) -> api::Outcome {
state.run(move |e| api::select(e, &file_ids, mode))
}
#[tauri::command(async)]
pub fn select(state: State<AppState>, file_ids: Vec<Uuid>, mode: SelectMode) -> Outcome {
let selection = engine::Selection::File {
file_ids: to_file_ids(file_ids).into_iter().collect(),
};
select_elements(&state, Some(selection), mode)
pub fn select_all(state: State<AppState>) -> api::Outcome {
state.run(api::select_all)
}
#[tauri::command(async)]
pub fn select_all(state: State<AppState>) -> Outcome {
act!(state, Action::SelectAll)
}
#[tauri::command(async)]
pub fn arrow_select(state: State<AppState>, down: bool, add: bool) -> Outcome {
act!(state, Action::ArrowSelect { down, add })
pub fn arrow_select(state: State<AppState>, down: bool, add: bool) -> api::Outcome {
state.run(move |e| api::arrow_select(e, down, add))
}
#[tauri::command(async)]
@@ -52,12 +27,8 @@ pub fn select_tracks(
file_id: Uuid,
track_ids: Vec<Uuid>,
mode: SelectMode,
) -> Outcome {
let selection = engine::Selection::Track {
file_id: engine::FileId(file_id),
trk_ids: track_ids.into_iter().map(engine::TrackId).collect(),
};
select_elements(&state, Some(selection), mode)
) -> api::Outcome {
state.run(move |e| api::select_tracks(e, file_id, &track_ids, mode))
}
#[tauri::command(async)]
@@ -67,24 +38,17 @@ pub fn select_segments(
track_id: Uuid,
segment_ids: Vec<Uuid>,
mode: SelectMode,
) -> Outcome {
let selection = engine::Selection::TrackSegment {
file_id: engine::FileId(file_id),
trk_id: engine::TrackId(track_id),
trkseg_ids: segment_ids
.into_iter()
.map(engine::TrackSegmentId)
.collect(),
};
select_elements(&state, Some(selection), mode)
) -> api::Outcome {
state.run(move |e| api::select_segments(e, file_id, track_id, &segment_ids, mode))
}
#[tauri::command(async)]
pub fn select_waypoint_group(state: State<AppState>, file_id: Uuid, mode: SelectMode) -> Outcome {
let selection = engine::Selection::Waypoints {
file_id: engine::FileId(file_id),
};
select_elements(&state, Some(selection), mode)
pub fn select_waypoint_group(
state: State<AppState>,
file_id: Uuid,
mode: SelectMode,
) -> api::Outcome {
state.run(move |e| api::select_waypoint_group(e, file_id, mode))
}
#[tauri::command(async)]
@@ -93,10 +57,6 @@ pub fn select_waypoints(
file_id: Uuid,
waypoint_ids: Vec<Uuid>,
mode: SelectMode,
) -> Outcome {
let selection = engine::Selection::Waypoint {
file_id: engine::FileId(file_id),
wpt_ids: waypoint_ids.into_iter().map(engine::WaypointId).collect(),
};
select_elements(&state, Some(selection), mode)
) -> api::Outcome {
state.run(move |e| api::select_waypoints(e, file_id, &waypoint_ids, mode))
}
+27 -54
View File
@@ -2,16 +2,17 @@
//! (one per call, copies of the buffers of the engine). Optional ones start with a byte telling
//! whether they are there.
use gpx_engine::api;
use tauri::State;
use crate::dto::*;
use crate::bytes::*;
use crate::state::AppState;
macro_rules! f64_getters {
($($name:ident),*) => {$(
#[tauri::command(async)]
pub fn $name(state: State<AppState>) -> Bytes {
Bytes::new(state.read(|e| f64s(&e.statistics().$name)))
f64s(&state.read(|e| api::$name(e).to_vec()))
}
)*};
}
@@ -30,7 +31,7 @@ macro_rules! optional_f64_getters {
($($name:ident),*) => {$(
#[tauri::command(async)]
pub fn $name(state: State<AppState>) -> Bytes {
optional(state.read(|e| e.statistics().$name.as_deref().map(f64s)))
optional(state.read(|e| api::$name(e).map(f64_bytes)))
}
)*};
}
@@ -40,12 +41,12 @@ optional_f64_getters!(speed, hr, cad, atemp, power);
/// Timestamps in milliseconds since the epoch (`NO_TIME` when missing), as `i64`.
#[tauri::command(async)]
pub fn timestamps(state: State<AppState>) -> Bytes {
optional(state.read(|e| e.statistics().time.as_deref().map(i64s)))
optional(state.read(|e| api::timestamps(e).map(i64_bytes)))
}
#[tauri::command(async)]
pub fn selection_statistics(state: State<AppState>) -> GlobalStatistics {
state.read(|e| GlobalStatistics::new(&e.statistics().global))
pub fn selection_statistics(state: State<AppState>) -> api::GlobalStatistics {
state.read(api::selection_statistics)
}
#[tauri::command(async)]
@@ -53,83 +54,55 @@ pub fn slice_statistics(
state: State<AppState>,
start: usize,
end: usize,
) -> Option<GlobalStatistics> {
state.read(move |e| {
e.statistics()
.slice(start, end)
.map(|s| GlobalStatistics::new(&s))
})
) -> Option<api::GlobalStatistics> {
state.read(move |e| api::slice_statistics(e, start, end))
}
macro_rules! intervals_getters {
($starts:ident, $values:ident, $field:ident) => {
macro_rules! category_getters {
($starts:ident, $values:ident, $names:ident) => {
#[tauri::command(async)]
pub fn $starts(state: State<AppState>) -> Bytes {
Bytes::new(state.read(|e| u32s(&e.statistics().$field.starts)))
u32s(&state.read(|e| api::$starts(e).to_vec()))
}
#[tauri::command(async)]
pub fn $values(state: State<AppState>) -> Bytes {
Bytes::new(state.read(|e| e.statistics().$field.values.clone()))
u8s(&state.read(|e| api::$values(e).to_vec()))
}
#[tauri::command(async)]
pub fn $names(state: State<AppState>) -> Vec<String> {
state.read(|e| api::$names(e).to_vec())
}
};
}
intervals_getters!(surface_starts, surface_values, surface);
intervals_getters!(highway_starts, highway_values, highway);
intervals_getters!(sac_scale_starts, sac_scale_values, sac_scale);
intervals_getters!(mtb_scale_starts, mtb_scale_values, mtb_scale);
/// The names of the categories, in the order of their codes. The lists only grow.
#[tauri::command(async)]
pub fn surfaces(state: State<AppState>) -> Vec<String> {
state.read(|e| e.categories().surface.names().to_vec())
}
#[tauri::command(async)]
pub fn highways(state: State<AppState>) -> Vec<String> {
state.read(|e| e.categories().highway.names().to_vec())
}
#[tauri::command(async)]
pub fn sac_scales(state: State<AppState>) -> Vec<String> {
state.read(|e| e.categories().sac_scale.names().to_vec())
}
#[tauri::command(async)]
pub fn mtb_scales(state: State<AppState>) -> Vec<String> {
state.read(|e| e.categories().mtb_scale.names().to_vec())
}
// Routing buffers
category_getters!(surface_starts, surface_values, surfaces);
category_getters!(highway_starts, highway_values, highways);
category_getters!(sac_scale_starts, sac_scale_values, sac_scales);
category_getters!(mtb_scale_starts, mtb_scale_values, mtb_scales);
#[tauri::command(async)]
pub fn anchor_indices(state: State<AppState>) -> Bytes {
Bytes::new(state.read(|e| u32s(&e.routing().anchor_indices)))
u32s(&state.read(|e| api::anchor_indices(e).to_vec()))
}
#[tauri::command(async)]
pub fn anchor_zooms(state: State<AppState>) -> Bytes {
Bytes::new(state.read(|e| e.routing().anchor_zooms.clone()))
u8s(&state.read(|e| api::anchor_zooms(e).to_vec()))
}
#[tauri::command(async)]
pub fn segment_starts(state: State<AppState>) -> Bytes {
Bytes::new(state.read(|e| u32s(&e.routing().segment_starts)))
u32s(&state.read(|e| api::segment_starts(e).to_vec()))
}
#[tauri::command(async)]
pub fn segment_ids(state: State<AppState>) -> Vec<String> {
state.read(|e| {
e.routing()
.segment_ids
.iter()
.map(|id| id.0.to_string())
.collect()
})
state.read(api::segment_ids)
}
#[tauri::command(async)]
pub fn routing_revision(state: State<AppState>) -> u32 {
state.read(|e| e.routing().revision)
state.read(api::routing_revision)
}
+1 -1
View File
@@ -6,8 +6,8 @@
use serde::Serialize;
use tauri::State;
use crate::dto::Outcome;
use crate::state::AppState;
use gpx_engine::api::Outcome;
/// What opening the storage found.
#[derive(Serialize)]
-578
View File
@@ -1,578 +0,0 @@
//! What crosses to the frontend, as JSON: the same shapes as the objects of the WASM interface
//! (`gpx-rs/wasm/src/ts.rs` holds their TypeScript types).
use gpx_engine::{self as engine, FileId};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Bytes that cross as they are: typed arrays (little-endian), GPX files.
pub type Bytes = tauri::ipc::Response;
pub fn f64s(values: &[f64]) -> Vec<u8> {
values.iter().flat_map(|v| v.to_le_bytes()).collect()
}
pub fn i64s(values: &[i64]) -> Vec<u8> {
values.iter().flat_map(|v| v.to_le_bytes()).collect()
}
pub fn u32s(values: &[u32]) -> Vec<u8> {
values.iter().flat_map(|v| v.to_le_bytes()).collect()
}
/// An array that can be absent: the first byte tells whether it is there, the array follows.
pub fn optional(array: Option<Vec<u8>>) -> Bytes {
let mut bytes = vec![u8::from(array.is_some())];
bytes.extend(array.unwrap_or_default());
Bytes::new(bytes)
}
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 {
changed: bool,
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>,
}
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,
}
}
/// 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<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>,
},
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(),
},
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(),
},
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<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) },
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()))
}
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>,
tracks: Vec<TrackNode>,
waypoints: Vec<WaypointNode>,
waypoints_rev: String,
}
#[derive(Serialize)]
struct TrackNode {
id: String,
#[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>,
segments: Vec<SegmentNode>,
}
#[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<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(),
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<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),
}
}
}
// Enumerations
#[derive(Clone, Copy, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SelectMode {
Replace,
Add,
Toggle,
}
impl From<SelectMode> for engine::SelectMode {
fn from(m: SelectMode) -> Self {
match m {
SelectMode::Replace => Self::Replace,
SelectMode::Add => Self::Add,
SelectMode::Toggle => Self::Toggle,
}
}
}
#[derive(Clone, Copy, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum MergeType {
Connect,
Group,
}
impl From<MergeType> for engine::MergeType {
fn from(t: MergeType) -> Self {
match t {
MergeType::Connect => Self::Connect,
MergeType::Group => Self::Group,
}
}
}
#[derive(Clone, Copy, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SplitType {
Files,
Tracks,
Segments,
}
impl From<SplitType> for engine::SplitType {
fn from(t: SplitType) -> Self {
match t {
SplitType::Files => Self::Files,
SplitType::Tracks => Self::Tracks,
SplitType::Segments => Self::Segments,
}
}
}
#[derive(Clone, Copy, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CleanType {
Inside,
Outside,
}
impl From<CleanType> for engine::CleanType {
fn from(t: CleanType) -> Self {
match t {
CleanType::Inside => Self::Inside,
CleanType::Outside => Self::Outside,
}
}
}
// 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,
}
+5 -5
View File
@@ -1,8 +1,8 @@
//! The desktop shell of gpx.studio. The engine of the files (`gpx-rs/engine`) runs in this
//! process, and the frontend talks to it through the commands of `commands`.
mod bytes;
mod commands;
mod dto;
mod state;
use commands::*;
@@ -59,10 +59,10 @@ fn with_engine<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::Builder<
edit::extract,
edit::elevation,
edit::clean,
routing::route,
routing::insert_anchor,
routing::change_loop_start,
routing::split,
edit::route,
edit::insert_anchor,
edit::change_loop_start,
edit::split,
// queries
queries::file_order,
queries::file_structure,
+1 -45
View File
@@ -8,14 +8,10 @@ use std::sync::mpsc::{self, Sender};
use std::sync::{Mutex, MutexGuard};
use std::thread;
use gpx_engine::{Action, Engine};
use crate::dto::Outcome;
use gpx_engine::Engine;
type Job = Box<dyn FnOnce(&mut Engine) + Send>;
/// What the commands talk to. The engine is the same as in the browser (`gpx-rs`); here it lives in
/// the process of the app, and the frontend reaches it through the commands only.
pub struct AppState {
jobs: Mutex<Sender<Job>>,
/// The settings of the frontend, as JSON strings by key.
@@ -65,43 +61,3 @@ impl AppState {
self.settings.lock().unwrap()
}
}
/// Executes an action and tells what it did, see `Outcome`.
pub fn execute(engine: &mut Engine, action: Action) -> Outcome {
match engine.execute(action) {
Ok(outcome) => Outcome::new(&outcome, None),
Err(error) => Outcome::failed(error.to_string()),
}
}
/// Executes an action on the engine. The values the action uses are moved to the engine thread.
macro_rules! act {
($state:expr, $action:expr) => {
$state.run(move |engine| $crate::state::execute(engine, $action))
};
}
/// Executes a command (the command can borrow what it uses).
macro_rules! edit {
($state:expr, $command:expr) => {
act!($state, ::gpx_engine::Action::Edit($command))
};
}
/// Like `edit!`, but does nothing if the selection is not the one of `revision` anymore (the
/// indices of the routing buffers only mean something for their revision).
macro_rules! edit_at {
($state:expr, $revision:expr, $command:expr) => {{
let revision = $revision;
$state.run(move |engine| {
if engine.routing().revision != revision {
return $crate::dto::Outcome::failed(
"the selection changed since the routing buffers were read",
);
}
$crate::state::execute(engine, ::gpx_engine::Action::Edit($command))
})
}};
}
pub(crate) use {act, edit, edit_at};
+5 -3
View File
@@ -60,7 +60,7 @@ impl App {
let mut data = vec![];
let mut lengths = vec![];
for name in names {
let file = std::fs::read(format!("../gpx-rs/engine/data/{name}.gpx")).unwrap();
let file = std::fs::read(format!("../gpx-rs/data/{name}.gpx")).unwrap();
lengths.push(file.len());
data.extend(file);
}
@@ -77,8 +77,10 @@ impl App {
fn f64s(bytes: &[u8]) -> Vec<f64> {
bytes
.chunks_exact(8)
.map(|chunk| f64::from_le_bytes(chunk.try_into().unwrap()))
.as_chunks::<8>()
.0
.iter()
.map(|chunk| f64::from_le_bytes(*chunk))
.collect()
}