mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-10 07:36:29 +00:00
progress
This commit is contained in:
1 parent
b085a1d476
commit
42a55e6759
51 files changed
+5756
-3854
No files matched your search
@@ -0,0 +1,34 @@
|
||||
//! Editing the metadata, the style and the structure of the files.
|
||||
|
||||
use gpx_engine::{self as engine, Command};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::ts::Outcome;
|
||||
|
||||
use crate::session::*;
|
||||
|
||||
// Edit commands
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn metadata(name: &str, desc: &str) -> Outcome {
|
||||
edit(Command::Metadata(engine::Metadata { name, desc }))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn style(color: Option<String>, opacity: Option<f64>, width: Option<f64>) -> Outcome {
|
||||
edit(Command::Style(engine::Style {
|
||||
color: color.as_deref(),
|
||||
opacity,
|
||||
width,
|
||||
}))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn new_track() -> Outcome {
|
||||
edit(Command::NewTrack(engine::NewTrack))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn new_track_segment() -> Outcome {
|
||||
edit(Command::NewTrackSegment(engine::NewTrackSegment))
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//! Creating, loading, deleting, moving, copying and pasting files and their elements.
|
||||
|
||||
use gpx_engine::{self as engine, Action, Command};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::convert::*;
|
||||
use crate::ids::*;
|
||||
use crate::session::*;
|
||||
use crate::ts::*;
|
||||
|
||||
// File commands
|
||||
|
||||
/// Creates a file and selects it. With `lng` and `lat`, it starts with a track and a segment that
|
||||
/// hold that trackpoint (at `ele`, 0 by default).
|
||||
#[wasm_bindgen]
|
||||
pub fn new_file(name: &str, 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(Command::New(engine::New { name, trackpoint }))
|
||||
}
|
||||
|
||||
/// Loads files as a single command (one undo step). The files are in `data`, one after the
|
||||
/// other, `lengths` being the number of bytes of each one. `names`: the name of each file when
|
||||
/// its data has none (the name on disk, without extension). The files that cannot be read are
|
||||
/// skipped, the first one that was read is selected.
|
||||
#[wasm_bindgen]
|
||||
pub fn load_files(data: &[u8], lengths: &[u32], names: Vec<String>) -> Outcome {
|
||||
if lengths.len() != names.len() {
|
||||
return fail(format!(
|
||||
"{} lengths for {} names",
|
||||
lengths.len(),
|
||||
names.len()
|
||||
));
|
||||
}
|
||||
let mut files = Vec::with_capacity(names.len());
|
||||
let mut offset = 0;
|
||||
for (length, name) in lengths.iter().zip(&names) {
|
||||
let end = offset + *length as usize;
|
||||
let Some(data) = data.get(offset..end) else {
|
||||
return fail(format!("the data of {name} is shorter than its length"));
|
||||
};
|
||||
files.push(engine::Load { data, name });
|
||||
offset = end;
|
||||
}
|
||||
edit(Command::LoadFiles(engine::LoadFiles { files }))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn duplicate() -> Outcome {
|
||||
edit(Command::Duplicate(engine::Duplicate))
|
||||
}
|
||||
|
||||
/// Deletes the selected elements. With `whole_files`, the files holding the selected elements are
|
||||
/// deleted instead, even if only a track or a waypoint is selected.
|
||||
#[wasm_bindgen]
|
||||
pub fn delete(whole_files: bool) -> Outcome {
|
||||
edit(Command::Delete(engine::Delete { whole_files }))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn delete_all() -> Outcome {
|
||||
edit(Command::DeleteAll(engine::DeleteAll))
|
||||
}
|
||||
|
||||
/// Moves the files to `index` among the other files, in the given order (not undoable).
|
||||
/// `file_ids_bytes`: concatenated 16-byte UUIDs.
|
||||
#[wasm_bindgen]
|
||||
pub fn reorder(file_ids_bytes: &[u8], index: usize) -> Outcome {
|
||||
match file_ids(file_ids_bytes) {
|
||||
Some(file_ids) => execute(Action::Reorder { file_ids, index }),
|
||||
None => fail("the file ids are not a list of UUIDs"),
|
||||
}
|
||||
}
|
||||
|
||||
// Clipboard
|
||||
|
||||
/// Puts the selected elements in the clipboard. Nothing happens when nothing is selected.
|
||||
#[wasm_bindgen]
|
||||
pub fn copy() -> Outcome {
|
||||
execute(Action::Copy)
|
||||
}
|
||||
|
||||
/// Like `copy`, but the elements are moved when they are pasted.
|
||||
#[wasm_bindgen]
|
||||
pub fn cut() -> Outcome {
|
||||
execute(Action::Cut)
|
||||
}
|
||||
|
||||
/// Pastes the clipboard according to the selection (see `can_paste`). What is pasted is always
|
||||
/// the elements as they were when they were copied or cut, even if they were changed or deleted
|
||||
/// since. It can be pasted several times: what was copied is copied again, what was cut is moved
|
||||
/// the first time (and the clipboard is not cut after that), then it is copied.
|
||||
#[wasm_bindgen]
|
||||
pub fn paste() -> Outcome {
|
||||
edit(Command::Paste(engine::Paste))
|
||||
}
|
||||
|
||||
/// Whether the clipboard can be pasted with the current selection.
|
||||
#[wasm_bindgen]
|
||||
pub fn can_paste() -> bool {
|
||||
with_engine(|e| e.can_paste()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Moves elements (given as a `Selection`) to a place of the file tree, keeping their ids, like
|
||||
/// dragging and dropping them does. The moved elements are selected.
|
||||
///
|
||||
/// Files can only go among the files; tracks among the files (each becomes a file) or the tracks
|
||||
/// of a file; segments among the files (each becomes a file), the tracks of a file (each becomes
|
||||
/// a track) or the segments of a track; waypoints, or all the waypoints of a file, among the
|
||||
/// waypoints of a file.
|
||||
#[wasm_bindgen]
|
||||
pub fn move_elements(what: Selection, to: MoveTarget) -> Outcome {
|
||||
match parse_selection(&what).zip(parse_move_target(&to)) {
|
||||
Some((what, to)) => edit(Command::Move(engine::Move { what, to })),
|
||||
None => fail("the elements or the target are not valid"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//! Undo and redo.
|
||||
|
||||
use gpx_engine::Action;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::ts::Outcome;
|
||||
|
||||
use crate::session::*;
|
||||
|
||||
// Undo-redo
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn undo() -> Outcome {
|
||||
execute(Action::Undo)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn redo() -> Outcome {
|
||||
execute(Action::Redo)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn can_undo() -> bool {
|
||||
with_engine(|e| e.can_undo()).unwrap_or(false)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn can_redo() -> bool {
|
||||
with_engine(|e| e.can_redo()).unwrap_or(false)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub(crate) mod edit;
|
||||
pub(crate) mod files;
|
||||
pub(crate) mod history;
|
||||
pub(crate) mod routing;
|
||||
pub(crate) mod tools;
|
||||
pub(crate) mod waypoints;
|
||||
@@ -0,0 +1,144 @@
|
||||
//! The commands that refer to trackpoints by their index in the routing buffers.
|
||||
|
||||
use gpx_engine::{self as engine, Command};
|
||||
use js_sys::{Array, Uint8Array, Uint32Array};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::convert::*;
|
||||
use crate::ids::*;
|
||||
use crate::queries::routing::with_routing;
|
||||
use crate::session::*;
|
||||
use crate::ts::*;
|
||||
use crate::types::*;
|
||||
|
||||
// Routing
|
||||
//
|
||||
// The indices of the trackpoints and of the anchors are the ones of the routing buffers: they only
|
||||
// mean something for the `revision` they were read with (`routing_revision`), and the commands
|
||||
// that use them do nothing if the selection is not the one of that revision anymore.
|
||||
|
||||
/// Makes the call `f`, unless the selection is not the one of the revision anymore.
|
||||
pub(crate) fn at_revision(revision: u32, f: impl FnOnce() -> Outcome) -> Outcome {
|
||||
if with_routing(|r| r.revision == revision).unwrap_or(false) {
|
||||
f()
|
||||
} else {
|
||||
fail("the selection changed since the routing buffers were read")
|
||||
}
|
||||
}
|
||||
|
||||
/// A category of the trackpoints of a route: see `RouteCategory` in `engine`.
|
||||
pub(crate) struct ParsedCategory {
|
||||
starts: Vec<u32>,
|
||||
values: Vec<u8>,
|
||||
names: Vec<String>,
|
||||
}
|
||||
|
||||
impl ParsedCategory {
|
||||
fn parse(attributes: &JsValue, key: &str) -> Option<Self> {
|
||||
let Some(category) = property(attributes, key) else {
|
||||
return Some(Self {
|
||||
starts: vec![],
|
||||
values: vec![],
|
||||
names: vec![],
|
||||
});
|
||||
};
|
||||
Some(Self {
|
||||
starts: property(&category, "starts")?
|
||||
.unchecked_into::<Uint32Array>()
|
||||
.to_vec(),
|
||||
values: property(&category, "values")?
|
||||
.unchecked_into::<Uint8Array>()
|
||||
.to_vec(),
|
||||
names: Array::from(&property(&category, "names")?)
|
||||
.iter()
|
||||
.map(|name| name.as_string())
|
||||
.collect::<Option<_>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn view(&self) -> engine::RouteCategory<'_> {
|
||||
engine::RouteCategory {
|
||||
starts: &self.starts,
|
||||
values: &self.values,
|
||||
names: &self.names,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the trackpoints `start..end` of the selection by the given ones, in a single segment
|
||||
/// (see `RoutingBuffer`). A pure insertion if `start == end`, a pure removal without new points.
|
||||
/// `attributes` holds the surface, highway, SAC scale and MTB scale of the new points, as
|
||||
/// intervals, and `anchors` the indices among the new points of the ones that become anchors.
|
||||
/// Does nothing, and says why, if the revision of the routing buffers is not the given one.
|
||||
#[wasm_bindgen]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn route(
|
||||
revision: u32,
|
||||
start: u32,
|
||||
end: u32,
|
||||
lng: &[f64],
|
||||
lat: &[f64],
|
||||
ele: &[f64],
|
||||
attributes: RouteAttributes,
|
||||
anchors: &[u32],
|
||||
) -> Outcome {
|
||||
let parse = |key| ParsedCategory::parse(&attributes, key);
|
||||
let (Some(surface), Some(highway), Some(sac_scale), Some(mtb_scale)) = (
|
||||
parse("surface"),
|
||||
parse("highway"),
|
||||
parse("sacScale"),
|
||||
parse("mtbScale"),
|
||||
) else {
|
||||
return fail("the attributes of the route are not valid");
|
||||
};
|
||||
if !same_len(lng, lat, ele) {
|
||||
return fail("lng, lat and ele differ in length");
|
||||
}
|
||||
at_revision(revision, || {
|
||||
edit(Command::Route(engine::Route {
|
||||
start,
|
||||
end,
|
||||
lng,
|
||||
lat,
|
||||
ele,
|
||||
surface: surface.view(),
|
||||
highway: highway.view(),
|
||||
sac_scale: sac_scale.view(),
|
||||
mtb_scale: mtb_scale.view(),
|
||||
anchors,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
/// Makes an anchor of the point of the selected segments that is the closest to the coordinates,
|
||||
/// inserting a trackpoint on the path if none is there. Does nothing, and says why, if the
|
||||
/// revision of the routing buffers is not the given one.
|
||||
#[wasm_bindgen]
|
||||
pub fn insert_anchor(revision: u32, lng: f64, lat: f64) -> Outcome {
|
||||
at_revision(revision, || {
|
||||
edit(Command::InsertAnchor(engine::InsertAnchor { lng, lat }))
|
||||
})
|
||||
}
|
||||
|
||||
/// Makes the trackpoint `index` of the selection the start of its segment, which is a loop (the
|
||||
/// trackpoints before it go to the end, and the loop closes on it). Does nothing, and says why, if the revision of the
|
||||
/// routing buffers is not the given one.
|
||||
#[wasm_bindgen]
|
||||
pub fn change_loop_start(revision: u32, index: u32) -> Outcome {
|
||||
at_revision(revision, || {
|
||||
edit(Command::ChangeLoopStart(engine::ChangeLoopStart { index }))
|
||||
})
|
||||
}
|
||||
|
||||
/// Cuts the file, the track or the segment (see `SplitType`) of the trackpoint `at` of the
|
||||
/// selection in two, there: the trackpoint ends the first part and starts the second one. Does
|
||||
/// nothing, and returns false, if the revision of the routing buffers is not the given one.
|
||||
#[wasm_bindgen]
|
||||
pub fn split(revision: u32, at: u32, split_type: SplitType) -> Outcome {
|
||||
at_revision(revision, || {
|
||||
edit(Command::Split(engine::Split {
|
||||
at,
|
||||
split_type: split_type.into(),
|
||||
}))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//! The tools: reversing, cropping, reducing, timestamps, merging, cleaning...
|
||||
|
||||
use gpx_engine::{self as engine, Command, LngLat, LngLatBounds};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::ts::Outcome;
|
||||
|
||||
use crate::session::*;
|
||||
use crate::types::*;
|
||||
|
||||
// Tools
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn reverse() -> Outcome {
|
||||
edit(Command::Reverse(engine::Reverse))
|
||||
}
|
||||
|
||||
/// Adds to each selected segment a reversed copy of itself (without repeating its last point), so that it goes back to where it
|
||||
/// started.
|
||||
#[wasm_bindgen]
|
||||
pub fn round_trip() -> Outcome {
|
||||
edit(Command::RoundTrip(engine::RoundTrip))
|
||||
}
|
||||
|
||||
/// Keeps the trackpoints `start` to `end` (both included) of the selection, counted over its
|
||||
/// segments one after the other, and removes the others (and what is left empty).
|
||||
#[wasm_bindgen]
|
||||
pub fn crop(start: u32, end: u32) -> Outcome {
|
||||
if start > end {
|
||||
return fail(format!("the range {start}..{end} is reversed"));
|
||||
}
|
||||
edit(Command::Crop(engine::Crop { start, end }))
|
||||
}
|
||||
|
||||
/// Removes the trackpoints of the selection that are less than `tolerance` meters away from the
|
||||
/// line of their neighbours, except the ends of the segments.
|
||||
#[wasm_bindgen]
|
||||
pub fn reduce(tolerance: f64) -> Outcome {
|
||||
edit(Command::Reduce(engine::Reduce { tolerance }))
|
||||
}
|
||||
|
||||
/// Sets the timestamps of the selection, which starts at `start_time` (ms since the epoch): the
|
||||
/// trackpoints that have some keep their durations multiplied by `ratio`, the others follow the
|
||||
/// previous one at `speed` (km/h).
|
||||
#[wasm_bindgen(js_name = changeTimestamps)]
|
||||
pub fn change_timestamps(start_time: f64, speed: f64, ratio: f64) -> Outcome {
|
||||
edit(Command::Time(engine::Time {
|
||||
start_time: start_time as i64,
|
||||
kind: engine::TimeKind::Change { speed, ratio },
|
||||
}))
|
||||
}
|
||||
|
||||
/// Makes up timestamps for the selection, which starts at `start_time` (ms since the epoch) and
|
||||
/// lasts `total_time` seconds.
|
||||
#[wasm_bindgen(js_name = createArtificialTimestamps)]
|
||||
pub fn create_artificial_timestamps(start_time: f64, total_time: f64) -> Outcome {
|
||||
edit(Command::Time(engine::Time {
|
||||
start_time: start_time as i64,
|
||||
kind: engine::TimeKind::Artificial { total_time },
|
||||
}))
|
||||
}
|
||||
|
||||
/// Merges the selection into its first element (see `MergeType`). `remove_gaps` brings the parts
|
||||
/// that are connected closer in time, if they have timestamps.
|
||||
#[wasm_bindgen]
|
||||
pub fn merge(type_: MergeType, remove_gaps: bool) -> Outcome {
|
||||
edit(Command::Merge(engine::Merge {
|
||||
type_: type_.into(),
|
||||
remove_gaps,
|
||||
}))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn extract() -> Outcome {
|
||||
edit(Command::Extract(engine::Extract))
|
||||
}
|
||||
|
||||
/// Sets the elevation of the trackpoints of the selection, one elevation per trackpoint.
|
||||
#[wasm_bindgen]
|
||||
pub fn elevation(ele: &[f64]) -> Outcome {
|
||||
edit(Command::Elevation(engine::Elevation { ele }))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn clean(
|
||||
west: f64,
|
||||
south: f64,
|
||||
east: f64,
|
||||
north: f64,
|
||||
type_: CleanType,
|
||||
trkpt: bool,
|
||||
wpt: bool,
|
||||
) -> Outcome {
|
||||
edit(Command::Clean(engine::Clean {
|
||||
bounds: LngLatBounds {
|
||||
sw: LngLat {
|
||||
lng: west,
|
||||
lat: south,
|
||||
},
|
||||
ne: LngLat {
|
||||
lng: east,
|
||||
lat: north,
|
||||
},
|
||||
},
|
||||
type_: type_.into(),
|
||||
trkpt,
|
||||
wpt,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//! Creating, editing, moving and deleting waypoints.
|
||||
|
||||
use gpx_engine::{self as engine, Command};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::ts::Outcome;
|
||||
|
||||
use crate::ids::*;
|
||||
use crate::session::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn new_waypoint(
|
||||
lng: f64,
|
||||
lat: f64,
|
||||
ele: f64,
|
||||
name: &str,
|
||||
desc: &str,
|
||||
icon: &str,
|
||||
link: &str,
|
||||
) -> Outcome {
|
||||
edit(Command::NewWaypoint(engine::NewWaypoint {
|
||||
lng,
|
||||
lat,
|
||||
ele,
|
||||
name,
|
||||
desc,
|
||||
icon,
|
||||
link,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Moves a waypoint of a file, whatever is selected.
|
||||
#[wasm_bindgen]
|
||||
pub fn move_waypoint(file_id: &str, waypoint_id: &str, lng: f64, lat: f64, ele: f64) -> Outcome {
|
||||
match (parse_file_id(file_id), parse_waypoint_id(waypoint_id)) {
|
||||
(Some(file_id), Some(waypoint_id)) => edit(Command::MoveWaypoint(engine::MoveWaypoint {
|
||||
file_id,
|
||||
waypoint_id,
|
||||
lng,
|
||||
lat,
|
||||
ele,
|
||||
})),
|
||||
_ => fail("the file or waypoint id is not a UUID"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a waypoint of a file, whatever is selected.
|
||||
#[wasm_bindgen]
|
||||
pub fn delete_waypoint(file_id: &str, waypoint_id: &str) -> Outcome {
|
||||
match (parse_file_id(file_id), parse_waypoint_id(waypoint_id)) {
|
||||
(Some(file_id), Some(waypoint_id)) => {
|
||||
edit(Command::DeleteWaypoint(engine::DeleteWaypoint {
|
||||
file_id,
|
||||
waypoint_id,
|
||||
}))
|
||||
}
|
||||
_ => fail("the file or waypoint id is not a UUID"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes a waypoint of a file, whatever is selected: its name, description, icon, link, position
|
||||
/// and elevation. The strings that are empty remove the field.
|
||||
#[wasm_bindgen]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn update_waypoint(
|
||||
file_id: &str,
|
||||
waypoint_id: &str,
|
||||
lng: f64,
|
||||
lat: f64,
|
||||
ele: f64,
|
||||
name: &str,
|
||||
desc: &str,
|
||||
icon: &str,
|
||||
link: &str,
|
||||
) -> Outcome {
|
||||
match (parse_file_id(file_id), parse_waypoint_id(waypoint_id)) {
|
||||
(Some(file_id), Some(waypoint_id)) => edit(Command::EditWaypoint(engine::EditWaypoint {
|
||||
file_id,
|
||||
waypoint_id,
|
||||
lng,
|
||||
lat,
|
||||
ele,
|
||||
name,
|
||||
desc,
|
||||
icon,
|
||||
link,
|
||||
})),
|
||||
_ => fail("the file or waypoint id is not a UUID"),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user