mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-27 02:27:18 +00:00
route full support
This commit is contained in:
1 parent
c842da8dc1
commit
b6c0f5190a
91 files changed
+2523
-847
No files matched your search
@@ -1,4 +1,6 @@
|
||||
use crate::{Apply, CommandError, Edit, Editor, File, State, Track, update_selected};
|
||||
use crate::{
|
||||
Apply, CommandError, Edit, Editor, File, Route, State, Track, TrackInfo, update_selected,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Metadata<'a> {
|
||||
@@ -13,21 +15,33 @@ impl Apply for Metadata<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Metadata<'_> {
|
||||
fn set(&self, info: &mut TrackInfo) -> Edit {
|
||||
info.name = Some(self.name.to_owned());
|
||||
info.desc = Some(self.desc.to_owned());
|
||||
Edit::Changed
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor for Metadata<'_> {
|
||||
fn file(&mut self, file: &mut File) -> Edit {
|
||||
file.info.name = self.name.to_owned();
|
||||
file.info.desc = Some(self.desc.to_owned());
|
||||
// a single track is named like the file
|
||||
if let [track] = file.trk.as_mut_slice() {
|
||||
track.info.name = Some(self.name.to_owned());
|
||||
// a single route or track is named like the file
|
||||
match (file.rte.as_mut_slice(), file.trk.as_mut_slice()) {
|
||||
([route], []) => route.info.name = Some(self.name.to_owned()),
|
||||
([], [track]) => track.info.name = Some(self.name.to_owned()),
|
||||
_ => {}
|
||||
}
|
||||
Edit::Changed
|
||||
}
|
||||
|
||||
fn route(&mut self, route: &mut Route) -> Edit {
|
||||
self.set(&mut route.info)
|
||||
}
|
||||
|
||||
fn track(&mut self, track: &mut Track) -> Edit {
|
||||
track.info.name = Some(self.name.to_owned());
|
||||
track.info.desc = Some(self.desc.to_owned());
|
||||
Edit::Changed
|
||||
self.set(&mut track.info)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
mod metadata;
|
||||
mod new_route;
|
||||
mod new_track;
|
||||
mod new_track_segment;
|
||||
mod style;
|
||||
|
||||
pub use metadata::*;
|
||||
pub use new_route::*;
|
||||
pub use new_track::*;
|
||||
pub use new_track_segment::*;
|
||||
pub use style::*;
|
||||
@@ -0,0 +1,54 @@
|
||||
use crate::{Apply, CommandError, Edit, Editor, File, Route, State, update_selected};
|
||||
|
||||
use super::new_track::common_style;
|
||||
|
||||
/// Adds an empty route to the selected files, with the style that their routes and tracks agree
|
||||
/// on (see [`crate::NewTrack`]).
|
||||
#[derive(Debug)]
|
||||
pub struct NewRoute;
|
||||
|
||||
impl Apply for NewRoute {
|
||||
fn apply(mut self, state: &mut State) -> Result<(), CommandError> {
|
||||
update_selected(state, &mut self);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor for NewRoute {
|
||||
fn file(&mut self, file: &mut File) -> Edit {
|
||||
file.rte.push(Route {
|
||||
info: common_style(file),
|
||||
..Default::default()
|
||||
});
|
||||
Edit::Changed
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{New, engine::command::fixture::Fixture};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_route_has_the_style_of_the_tracks_and_an_empty_segment() {
|
||||
let mut fx = Fixture::default();
|
||||
New {
|
||||
name: "file",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
crate::NewTrack.apply(&mut fx.state()).unwrap();
|
||||
let file = std::rc::Rc::make_mut(fx.files.values_mut().next().unwrap());
|
||||
file.trk[0].info.color = Some("ff0000".into());
|
||||
NewRoute.apply(&mut fx.state()).unwrap();
|
||||
NewRoute.apply(&mut fx.state()).unwrap();
|
||||
|
||||
let file = fx.files.values().next().unwrap();
|
||||
assert_eq!(file.rte.len(), 2);
|
||||
assert_ne!(file.rte[0].id, file.rte[1].id);
|
||||
assert!(file.rte[0].seg.is_empty());
|
||||
assert_eq!(file.rte[1].info.color.as_deref(), Some("ff0000"));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Apply, CommandError, Edit, Editor, File, State, Track, update_selected};
|
||||
use crate::{Apply, CommandError, Edit, Editor, File, State, Track, TrackInfo, update_selected};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NewTrack;
|
||||
@@ -12,20 +12,41 @@ impl Apply for NewTrack {
|
||||
|
||||
impl Editor for NewTrack {
|
||||
fn file(&mut self, file: &mut File) -> Edit {
|
||||
let mut track = Track::default();
|
||||
track.info.color = common(file, |t| &t.info.color);
|
||||
track.info.opacity = common(file, |t| &t.info.opacity);
|
||||
track.info.width = common(file, |t| &t.info.width);
|
||||
file.trk.push(track);
|
||||
file.trk.push(Track {
|
||||
info: common_style(file),
|
||||
..Default::default()
|
||||
});
|
||||
Edit::Changed
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the value of a style attribute if all existing tracks agree on it.
|
||||
fn common<T: Clone + PartialEq>(file: &File, get: impl Fn(&Track) -> &Option<T>) -> Option<T> {
|
||||
let mut tracks = file.trk.iter();
|
||||
let first = get(tracks.next()?);
|
||||
tracks.all(|t| get(t) == first).then(|| first.clone())?
|
||||
/// The style that all the routes and tracks of the file agree on: each attribute is set if they
|
||||
/// all have the same value for it.
|
||||
pub(super) fn common_style(file: &File) -> TrackInfo {
|
||||
let infos: Vec<&TrackInfo> = file
|
||||
.rte
|
||||
.iter()
|
||||
.map(|rte| &rte.info)
|
||||
.chain(file.trk.iter().map(|trk| &trk.info))
|
||||
.collect();
|
||||
TrackInfo {
|
||||
color: common(&infos, |info| &info.color),
|
||||
opacity: common(&infos, |info| &info.opacity),
|
||||
width: common(&infos, |info| &info.width),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the value of a style attribute if all the infos agree on it.
|
||||
fn common<T: Clone + PartialEq>(
|
||||
infos: &[&TrackInfo],
|
||||
get: impl Fn(&TrackInfo) -> &Option<T>,
|
||||
) -> Option<T> {
|
||||
let mut infos = infos.iter();
|
||||
let first = get(infos.next()?);
|
||||
infos
|
||||
.all(|info| get(info) == first)
|
||||
.then(|| first.clone())?
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Apply, CommandError, Edit, Editor, State, Track, update_selected};
|
||||
use crate::{Apply, CommandError, Edit, Editor, Route, State, Track, TrackInfo, update_selected};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Style<'a> {
|
||||
@@ -14,21 +14,31 @@ impl Apply for Style<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor for Style<'_> {
|
||||
fn track(&mut self, track: &mut Track) -> Edit {
|
||||
impl Style<'_> {
|
||||
fn set(&self, info: &mut TrackInfo) -> Edit {
|
||||
if let Some(color) = self.color {
|
||||
track.info.color = Some(color.to_owned());
|
||||
info.color = Some(color.to_owned());
|
||||
}
|
||||
if self.opacity.is_some() {
|
||||
track.info.opacity = self.opacity;
|
||||
info.opacity = self.opacity;
|
||||
}
|
||||
if self.width.is_some() {
|
||||
track.info.width = self.width;
|
||||
info.width = self.width;
|
||||
}
|
||||
Edit::Changed
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor for Style<'_> {
|
||||
fn route(&mut self, route: &mut Route) -> Edit {
|
||||
self.set(&mut route.info)
|
||||
}
|
||||
|
||||
fn track(&mut self, track: &mut Track) -> Edit {
|
||||
self.set(&mut track.info)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::rc::Rc;
|
||||
use crate::{Apply, CommandError, FileId, Files, Selection, State, Waypoint};
|
||||
|
||||
/// 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.
|
||||
/// are deleted instead, even if only a route, a track or a waypoint is selected.
|
||||
#[derive(Debug)]
|
||||
pub struct Delete {
|
||||
pub whole_files: bool,
|
||||
@@ -23,6 +23,21 @@ impl Apply for Delete {
|
||||
state.order.0.retain(|id| !file_ids.contains(id));
|
||||
Selection::Empty
|
||||
}
|
||||
Selection::Route { file_id, rte_ids } => {
|
||||
let file = state
|
||||
.files
|
||||
.get_mut(file_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let file = Rc::make_mut(file);
|
||||
let len = file.rte.len();
|
||||
file.rte.retain(|rte| !rte_ids.contains(&rte.id));
|
||||
if file.rte.len() == len {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
Selection::File {
|
||||
file_ids: [*file_id].into(),
|
||||
}
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = state
|
||||
.files
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, FileId, Files, Selection, State, Waypoint, copy_file, copy_segment,
|
||||
copy_track, copy_waypoint,
|
||||
Apply, CommandError, FileId, Files, Selection, State, Waypoint, copy_file, copy_route,
|
||||
copy_segment, copy_track, copy_waypoint,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -35,6 +35,30 @@ impl Apply for Duplicate {
|
||||
state.order.0.splice(at..at, order);
|
||||
Selection::File { file_ids: copies }
|
||||
}
|
||||
Selection::Route { file_id, rte_ids } => {
|
||||
let file = state
|
||||
.files
|
||||
.get_mut(file_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let file = Rc::make_mut(file);
|
||||
let mut copies = HashSet::new();
|
||||
append_copies(
|
||||
&mut file.rte,
|
||||
|rte| rte_ids.contains(&rte.id),
|
||||
|rte| {
|
||||
let copy = copy_route(rte);
|
||||
copies.insert(copy.id);
|
||||
copy
|
||||
},
|
||||
);
|
||||
if copies.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
Selection::Route {
|
||||
file_id: *file_id,
|
||||
rte_ids: copies,
|
||||
}
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = state
|
||||
.files
|
||||
|
||||
@@ -9,6 +9,8 @@ use crate::{
|
||||
pub enum MoveTarget {
|
||||
/// The list of files.
|
||||
Files { index: usize },
|
||||
/// The routes of a file.
|
||||
Routes { file_id: FileId, index: usize },
|
||||
/// The tracks of a file.
|
||||
Tracks { file_id: FileId, index: usize },
|
||||
/// The segments of a track.
|
||||
@@ -26,9 +28,10 @@ pub enum MoveTarget {
|
||||
///
|
||||
/// What can go where:
|
||||
/// - files: among the files;
|
||||
/// - tracks: among the files (each track becomes a file), or among the tracks of a file;
|
||||
/// - segments: among the files (each segment becomes a file), among the tracks of a file (each
|
||||
/// segment becomes a track), or among the segments of a track;
|
||||
/// - routes and tracks: among the files (each one becomes a file), or among the routes or the
|
||||
/// tracks of a file, where they keep their kind (see [`Destination`]);
|
||||
/// - segments: among the files (each segment becomes a file), among the routes or the tracks of a
|
||||
/// file (each segment becomes a route or a track), or among the segments of a track;
|
||||
/// - waypoints, or all the waypoints of a file (the waypoints node): among the waypoints of a
|
||||
/// file.
|
||||
#[derive(Debug)]
|
||||
@@ -41,6 +44,9 @@ impl From<MoveTarget> for Destination {
|
||||
fn from(target: MoveTarget) -> Self {
|
||||
match target {
|
||||
MoveTarget::Files { index } => Destination::Files(Place::Index(index)),
|
||||
MoveTarget::Routes { file_id, index } => {
|
||||
Destination::Routes(file_id, Place::Index(index))
|
||||
}
|
||||
MoveTarget::Tracks { file_id, index } => {
|
||||
Destination::Tracks(file_id, Place::Index(index))
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
use std::{collections::HashSet, hash::Hash};
|
||||
|
||||
use crate::{
|
||||
Apply, ClipboardContent, CommandError, Destination, FileId, Place, Selection, State, TrackId,
|
||||
TrackSegmentId, WaypointId, transfer, waypoint_ids,
|
||||
Apply, ClipboardContent, CommandError, Destination, FileId, Place, RouteId, Selection, State,
|
||||
TrackId, TrackSegmentId, WaypointId, transfer, waypoint_ids,
|
||||
};
|
||||
|
||||
/// Pastes what was copied or cut (see [`crate::Clipboard`]).
|
||||
///
|
||||
/// Where it goes depends on the selection, see [`crate::Clipboard::can_paste`]:
|
||||
/// - nothing selected: the files are copied, and tracks and segments become new files (named
|
||||
/// like their track, or like the file they come from);
|
||||
/// - a file: the tracks are added to it, segments become new tracks in it, and the waypoints are
|
||||
/// added to its own;
|
||||
/// - a track: the segments are added to it, tracks go after it in its file;
|
||||
/// - nothing selected: the files are copied, and routes, tracks and segments become new files
|
||||
/// (named like their route or track, or like the file they come from);
|
||||
/// - a file: the routes and tracks are added to it, segments become new tracks in it, and the
|
||||
/// waypoints are added to its own;
|
||||
/// - a route: routes, and segments as new routes, go after it in its file, tracks go before the
|
||||
/// other tracks (right after the routes);
|
||||
/// - a track: the segments are added to it, tracks go after it in its file, and routes after the
|
||||
/// other routes;
|
||||
/// - a segment: the segments go after it in its track;
|
||||
/// - the waypoints or a waypoint: the waypoints are added to their file, after the waypoint.
|
||||
///
|
||||
@@ -46,6 +49,14 @@ impl Apply for Paste {
|
||||
}
|
||||
}
|
||||
|
||||
fn route_ids_of(state: &State, file_id: FileId) -> Vec<RouteId> {
|
||||
state
|
||||
.files
|
||||
.get(&file_id)
|
||||
.map(|file| file.rte.iter().map(|rte| rte.id).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn track_ids_of(state: &State, file_id: FileId) -> Vec<TrackId> {
|
||||
state
|
||||
.files
|
||||
@@ -79,14 +90,34 @@ fn destination(state: &State, content: &ClipboardContent) -> Result<Destination,
|
||||
};
|
||||
Ok(match (content, &*state.selection) {
|
||||
(ClipboardContent::Files(_), _) => Destination::Files(Place::End),
|
||||
(ClipboardContent::Tracks(_) | ClipboardContent::Segments(_), Selection::Empty) => {
|
||||
Destination::Files(Place::End)
|
||||
(
|
||||
ClipboardContent::Routes(_)
|
||||
| ClipboardContent::Tracks(_)
|
||||
| ClipboardContent::Segments(_),
|
||||
Selection::Empty,
|
||||
) => Destination::Files(Place::End),
|
||||
(ClipboardContent::Routes(_), Selection::File { file_ids }) => {
|
||||
Destination::Routes(last_file(file_ids)?, Place::End)
|
||||
}
|
||||
// routes and tracks keep their kind (see `Destination`)
|
||||
(
|
||||
ClipboardContent::Routes(_)
|
||||
| ClipboardContent::Tracks(_)
|
||||
| ClipboardContent::Segments(_),
|
||||
Selection::Route { file_id, rte_ids },
|
||||
) => {
|
||||
let last =
|
||||
last_selected(route_ids_of(state, *file_id).into_iter(), rte_ids).ok_or(nothing)?;
|
||||
Destination::Routes(*file_id, Place::After(last))
|
||||
}
|
||||
(
|
||||
ClipboardContent::Tracks(_) | ClipboardContent::Segments(_),
|
||||
Selection::File { file_ids },
|
||||
) => Destination::Tracks(last_file(file_ids)?, Place::End),
|
||||
(ClipboardContent::Tracks(_), Selection::Track { file_id, trk_ids }) => {
|
||||
(
|
||||
ClipboardContent::Routes(_) | ClipboardContent::Tracks(_),
|
||||
Selection::Track { file_id, trk_ids },
|
||||
) => {
|
||||
let last =
|
||||
last_selected(track_ids_of(state, *file_id).into_iter(), trk_ids).ok_or(nothing)?;
|
||||
Destination::Tracks(*file_id, Place::After(last))
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
ClipboardContent, ClipboardSegment, ClipboardTrack, CommandError, File, FileId, Selection,
|
||||
State, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint, WaypointId, copy_file,
|
||||
copy_segment, copy_track, copy_waypoint,
|
||||
ClipboardContent, ClipboardRoute, ClipboardSegment, ClipboardTrack, CommandError, File, FileId,
|
||||
Route, RouteId, Selection, State, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint,
|
||||
WaypointId, copy_file, copy_route, copy_segment, copy_track, copy_waypoint,
|
||||
};
|
||||
|
||||
/// Where elements are put, among the others of the same list.
|
||||
@@ -37,15 +37,20 @@ impl<T: Copy + PartialEq> Place<T> {
|
||||
|
||||
/// Where elements go. What can go where:
|
||||
/// - files: among the files;
|
||||
/// - routes: among the files (each route becomes a file, named like the route, or like the file
|
||||
/// it comes from), or among the routes of a file. Among the tracks of a file, they stay routes,
|
||||
/// which come before the tracks: they go after the other routes;
|
||||
/// - tracks: among the files (each track becomes a file, named like the track, or like the file
|
||||
/// it comes from), or among the tracks of a file;
|
||||
/// it comes from), or among the tracks of a file. Among the routes of a file, they stay tracks,
|
||||
/// which come after the routes: they go before the other tracks;
|
||||
/// - segments: among the files (each segment becomes a file with a track, named like its track,
|
||||
/// or like its file), among the tracks of a file (each segment becomes a track), or among the
|
||||
/// segments of a track;
|
||||
/// or like its file), among the routes or the tracks of a file (each segment becomes a route or
|
||||
/// a track), or among the segments of a track;
|
||||
/// - waypoints: among the waypoints of a file.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Destination {
|
||||
Files(Place<FileId>),
|
||||
Routes(FileId, Place<RouteId>),
|
||||
Tracks(FileId, Place<TrackId>),
|
||||
Segments(FileId, TrackId, Place<TrackSegmentId>),
|
||||
Waypoints(FileId, Place<WaypointId>),
|
||||
@@ -93,6 +98,14 @@ pub fn remove_elements(state: &mut State, content: &ClipboardContent) {
|
||||
state.files.retain(|id, _| !ids.contains(id));
|
||||
state.order.0.retain(|id| !ids.contains(id));
|
||||
}
|
||||
ClipboardContent::Routes(routes) => {
|
||||
let ids: HashSet<RouteId> = routes.iter().map(|rte| rte.route.id).collect();
|
||||
for file in state.files.values_mut() {
|
||||
if file.rte.iter().any(|rte| ids.contains(&rte.id)) {
|
||||
Rc::make_mut(file).rte.retain(|rte| !ids.contains(&rte.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
ClipboardContent::Tracks(tracks) => {
|
||||
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.track.id).collect();
|
||||
for file in state.files.values_mut() {
|
||||
@@ -141,7 +154,9 @@ pub fn transfer(
|
||||
// the destination has to be there, before anything is changed
|
||||
let exists = match to {
|
||||
Destination::Files(_) => true,
|
||||
Destination::Tracks(file_id, _) => state.files.contains_key(&file_id),
|
||||
Destination::Routes(file_id, _) | Destination::Tracks(file_id, _) => {
|
||||
state.files.contains_key(&file_id)
|
||||
}
|
||||
Destination::Segments(file_id, trk_id, _) => state
|
||||
.files
|
||||
.get(&file_id)
|
||||
@@ -152,17 +167,21 @@ pub fn transfer(
|
||||
(content, to),
|
||||
(ClipboardContent::Files(_), Destination::Files(_))
|
||||
| (
|
||||
ClipboardContent::Tracks(_),
|
||||
Destination::Files(_) | Destination::Tracks(..)
|
||||
ClipboardContent::Routes(_) | ClipboardContent::Tracks(_),
|
||||
Destination::Files(_) | Destination::Routes(..) | Destination::Tracks(..)
|
||||
)
|
||||
| (
|
||||
ClipboardContent::Segments(_),
|
||||
Destination::Files(_) | Destination::Tracks(..) | Destination::Segments(..),
|
||||
Destination::Files(_)
|
||||
| Destination::Routes(..)
|
||||
| Destination::Tracks(..)
|
||||
| Destination::Segments(..),
|
||||
)
|
||||
| (ClipboardContent::Waypoints(_), Destination::Waypoints(..))
|
||||
);
|
||||
let empty = match content {
|
||||
ClipboardContent::Files(files) => files.is_empty(),
|
||||
ClipboardContent::Routes(routes) => routes.is_empty(),
|
||||
ClipboardContent::Tracks(tracks) => tracks.is_empty(),
|
||||
ClipboardContent::Segments(segments) => segments.is_empty(),
|
||||
ClipboardContent::Waypoints(waypoints) => waypoints.is_empty(),
|
||||
@@ -189,6 +208,7 @@ pub fn transfer(
|
||||
.collect();
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
(ClipboardContent::Routes(routes), to) => transfer_routes(state, routes, copy, to),
|
||||
(ClipboardContent::Tracks(tracks), to) => transfer_tracks(state, tracks, copy, to),
|
||||
(ClipboardContent::Segments(segments), to) => transfer_segments(state, segments, copy, to),
|
||||
(ClipboardContent::Waypoints(waypoints), Destination::Waypoints(file_id, place)) => {
|
||||
@@ -198,6 +218,89 @@ pub fn transfer(
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts the routes among the routes of a file, at `place`, and gives the selection of them.
|
||||
fn insert_routes(
|
||||
state: &mut State,
|
||||
file_id: FileId,
|
||||
place: Place<RouteId>,
|
||||
routes: Vec<Route>,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let ids: HashSet<RouteId> = routes.iter().map(|rte| rte.id).collect();
|
||||
let file = file_mut(state, file_id)?;
|
||||
let existing: Vec<RouteId> = file.rte.iter().map(|rte| rte.id).collect();
|
||||
let at = place.position(&existing);
|
||||
file.rte.splice(at..at, routes);
|
||||
Ok(Selection::Route {
|
||||
file_id,
|
||||
rte_ids: ids,
|
||||
})
|
||||
}
|
||||
|
||||
fn transfer_routes(
|
||||
state: &mut State,
|
||||
routes: &[ClipboardRoute],
|
||||
copy: bool,
|
||||
to: Destination,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let routes: Vec<(Route, &str)> = routes
|
||||
.iter()
|
||||
.map(|clip| {
|
||||
let route = if copy {
|
||||
copy_route(&clip.route)
|
||||
} else {
|
||||
clip.route.clone()
|
||||
};
|
||||
(route, clip.file_name.as_str())
|
||||
})
|
||||
.collect();
|
||||
match to {
|
||||
Destination::Files(place) => {
|
||||
let files = routes
|
||||
.into_iter()
|
||||
.map(|(route, file_name)| {
|
||||
let name = route
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| file_name.to_owned());
|
||||
Rc::new(File {
|
||||
rte: vec![route],
|
||||
..new_file(&name, vec![])
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
Destination::Routes(file_id, place) => {
|
||||
let routes = routes.into_iter().map(|(route, _)| route).collect();
|
||||
insert_routes(state, file_id, place, routes)
|
||||
}
|
||||
Destination::Tracks(file_id, _) => {
|
||||
let routes = routes.into_iter().map(|(route, _)| route).collect();
|
||||
insert_routes(state, file_id, Place::End, routes)
|
||||
}
|
||||
_ => Err(CommandError::NothingToDo),
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts the tracks among the tracks of a file, at `place`, and gives the selection of them.
|
||||
fn insert_tracks(
|
||||
state: &mut State,
|
||||
file_id: FileId,
|
||||
place: Place<TrackId>,
|
||||
tracks: Vec<Track>,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.id).collect();
|
||||
let file = file_mut(state, file_id)?;
|
||||
let existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
|
||||
let at = place.position(&existing);
|
||||
file.trk.splice(at..at, tracks);
|
||||
Ok(Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids,
|
||||
})
|
||||
}
|
||||
|
||||
fn transfer_tracks(
|
||||
state: &mut State,
|
||||
tracks: &[ClipboardTrack],
|
||||
@@ -231,16 +334,12 @@ fn transfer_tracks(
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
Destination::Tracks(file_id, place) => {
|
||||
let tracks: Vec<Track> = tracks.into_iter().map(|(track, _)| track).collect();
|
||||
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.id).collect();
|
||||
let file = file_mut(state, file_id)?;
|
||||
let existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
|
||||
let at = place.position(&existing);
|
||||
file.trk.splice(at..at, tracks);
|
||||
Ok(Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids,
|
||||
})
|
||||
let tracks = tracks.into_iter().map(|(track, _)| track).collect();
|
||||
insert_tracks(state, file_id, place, tracks)
|
||||
}
|
||||
Destination::Routes(file_id, _) => {
|
||||
let tracks = tracks.into_iter().map(|(track, _)| track).collect();
|
||||
insert_tracks(state, file_id, Place::Index(0), tracks)
|
||||
}
|
||||
_ => Err(CommandError::NothingToDo),
|
||||
}
|
||||
@@ -276,20 +375,22 @@ fn transfer_segments(
|
||||
.collect();
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
Destination::Routes(file_id, place) => {
|
||||
let routes = segments
|
||||
.into_iter()
|
||||
.map(|(seg, _)| Route {
|
||||
seg,
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
insert_routes(state, file_id, place, routes)
|
||||
}
|
||||
Destination::Tracks(file_id, place) => {
|
||||
let tracks: Vec<Track> = segments
|
||||
let tracks = segments
|
||||
.into_iter()
|
||||
.map(|(segment, _)| own_track(segment))
|
||||
.collect();
|
||||
let ids = tracks.iter().map(|trk| trk.id).collect();
|
||||
let file = file_mut(state, file_id)?;
|
||||
let existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
|
||||
let at = place.position(&existing);
|
||||
file.trk.splice(at..at, tracks);
|
||||
Ok(Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids,
|
||||
})
|
||||
insert_tracks(state, file_id, place, tracks)
|
||||
}
|
||||
Destination::Segments(file_id, trk_id, place) => {
|
||||
let segments: Vec<TrackSegment> =
|
||||
|
||||
@@ -4,6 +4,8 @@ mod file;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod fixture;
|
||||
mod pattern;
|
||||
#[cfg(test)]
|
||||
mod routes_tests;
|
||||
mod tools;
|
||||
|
||||
pub use apply::*;
|
||||
@@ -33,11 +35,12 @@ pub enum Command<'a> {
|
||||
DeleteWaypoint(DeleteWaypoint),
|
||||
Metadata(Metadata<'a>),
|
||||
Style(Style<'a>),
|
||||
NewRoute(NewRoute),
|
||||
NewTrack(NewTrack),
|
||||
NewTrackSegment(NewTrackSegment),
|
||||
Reverse(Reverse),
|
||||
RoundTrip(RoundTrip),
|
||||
Route(Route<'a>),
|
||||
Reroute(Reroute<'a>),
|
||||
NewWaypoint(NewWaypoint<'a>),
|
||||
EditWaypoint(EditWaypoint<'a>),
|
||||
MoveWaypoint(MoveWaypoint),
|
||||
@@ -68,11 +71,12 @@ impl Apply for Command<'_> {
|
||||
Command::DeleteWaypoint(c) => c.apply(state),
|
||||
Command::Metadata(c) => c.apply(state),
|
||||
Command::Style(c) => c.apply(state),
|
||||
Command::NewRoute(c) => c.apply(state),
|
||||
Command::NewTrack(c) => c.apply(state),
|
||||
Command::NewTrackSegment(c) => c.apply(state),
|
||||
Command::Reverse(c) => c.apply(state),
|
||||
Command::RoundTrip(c) => c.apply(state),
|
||||
Command::Route(c) => c.apply(state),
|
||||
Command::Reroute(c) => c.apply(state),
|
||||
Command::NewWaypoint(c) => c.apply(state),
|
||||
Command::EditWaypoint(c) => c.apply(state),
|
||||
Command::MoveWaypoint(c) => c.apply(state),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{File, Track, TrackSegment, Waypoint};
|
||||
use crate::{File, Route, Track, TrackSegment, Waypoint};
|
||||
|
||||
// The trackpoints of a segment are shared chunks, so copying elements is cheap.
|
||||
|
||||
@@ -18,10 +18,20 @@ pub fn copy_track(track: &Track) -> Track {
|
||||
}
|
||||
}
|
||||
|
||||
/// A copy of the file and of its tracks and segments, with new ids.
|
||||
/// A copy of the route and of its segment, with new ids.
|
||||
pub fn copy_route(route: &Route) -> Route {
|
||||
Route {
|
||||
id: Default::default(),
|
||||
seg: copy_segment(&route.seg),
|
||||
..route.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// A copy of the file and of its routes, tracks and segments, with new ids.
|
||||
pub fn copy_file(file: &File) -> File {
|
||||
File {
|
||||
id: Default::default(),
|
||||
rte: file.rte.iter().map(copy_route).collect(),
|
||||
trk: file.trk.iter().map(copy_track).collect(),
|
||||
..file.clone()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ pub fn update_segments(
|
||||
let file = files
|
||||
.entry(location.file_id)
|
||||
.or_insert_with(|| (*state.files[&location.file_id]).clone());
|
||||
f(i, &mut file.trk[location.trk].trkseg[location.seg]);
|
||||
f(i, file.segment_mut(location.index));
|
||||
}
|
||||
for (file_id, file) in files {
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
@@ -31,6 +31,8 @@ mod tests {
|
||||
|
||||
use crate::engine::command::fixture::Fixture;
|
||||
|
||||
use crate::SegmentIndex;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn fixture() -> (Fixture, FileId) {
|
||||
@@ -49,13 +51,11 @@ mod tests {
|
||||
let locations = [
|
||||
SegmentLocation {
|
||||
file_id: id,
|
||||
trk: 1,
|
||||
seg: 0,
|
||||
index: SegmentIndex::Track { trk: 1, seg: 0 },
|
||||
},
|
||||
SegmentLocation {
|
||||
file_id: id,
|
||||
trk: 0,
|
||||
seg: 0,
|
||||
index: SegmentIndex::Track { trk: 0, seg: 0 },
|
||||
},
|
||||
];
|
||||
let mut calls = vec![];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{File, FileId, Files, Selection, State, Track, TrackSegment, Waypoint};
|
||||
use crate::{File, FileId, Files, Route, Selection, State, Track, TrackSegment, Waypoint};
|
||||
|
||||
/// What an [`Editor`] hook did to the element it was given.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -21,13 +21,19 @@ impl Edit {
|
||||
|
||||
/// Independent edit of each selected element, without needing the rest of the state.
|
||||
///
|
||||
/// A selected file, track or segment is passed to the hook of its own level. By default a
|
||||
/// file is forwarded to its tracks and a track to its segments, so an editor only overrides
|
||||
/// the level(s) it cares about (metadata: file and track, style: track, reverse: segment).
|
||||
/// Waypoints are only edited when selected, never through their file.
|
||||
/// A selected file, route, track or segment is passed to the hook of its own level. By default
|
||||
/// a file is forwarded to its routes and tracks, and a route or a track to its segments, so an
|
||||
/// editor only overrides the level(s) it cares about (metadata: file, route and track, style:
|
||||
/// route and track, reverse: segment). Waypoints are only edited when selected, never through
|
||||
/// their file.
|
||||
pub trait Editor {
|
||||
fn file(&mut self, file: &mut File) -> Edit {
|
||||
edit_each(&mut file.trk, |trk| self.track(trk))
|
||||
let routes = edit_each(&mut file.rte, |rte| self.route(rte));
|
||||
routes.merge(edit_each(&mut file.trk, |trk| self.track(trk)))
|
||||
}
|
||||
|
||||
fn route(&mut self, route: &mut Route) -> Edit {
|
||||
edit_segment(self, &mut route.seg)
|
||||
}
|
||||
|
||||
fn track(&mut self, track: &mut Track) -> Edit {
|
||||
@@ -95,7 +101,7 @@ fn edit_waypoints<E: Editor + ?Sized>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the editor to every selected file, track or segment.
|
||||
/// Applies the editor to every selected file, route, track or segment.
|
||||
pub fn update_selected<E: Editor>(state: &mut State, editor: &mut E) {
|
||||
match &*state.selection {
|
||||
Selection::File { file_ids } => {
|
||||
@@ -103,6 +109,15 @@ pub fn update_selected<E: Editor>(state: &mut State, editor: &mut E) {
|
||||
update_file(state.files, *id, |file| editor.file(file));
|
||||
}
|
||||
}
|
||||
Selection::Route { file_id, rte_ids } => {
|
||||
update_file(state.files, *file_id, |file| {
|
||||
edit_where(
|
||||
&mut file.rte,
|
||||
|rte| rte_ids.contains(&rte.id),
|
||||
|rte| editor.route(rte),
|
||||
)
|
||||
});
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
update_file(state.files, *file_id, |file| {
|
||||
edit_where(
|
||||
|
||||
@@ -0,0 +1,604 @@
|
||||
//! The commands on routes: they work as on tracks, a route being a track of a single segment.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Apply, Clean, CleanType, Clipboard, ClipboardContent, CommandError, Crop, Delete, Duplicate,
|
||||
Extract, File, FileId, LngLat, LngLatBounds, Merge, MergeType, Metadata, Move, MoveTarget,
|
||||
Paste, Reverse, RouteId, SegmentIndex, Selection, Split, SplitType, Style,
|
||||
engine::command::fixture::Fixture, parse,
|
||||
};
|
||||
|
||||
/// A file with the 2 routes of `with_routes` and the 2 tracks (of 2 segments) of
|
||||
/// `with_tracks_and_segments`, which is the only file and is in the order.
|
||||
fn fixture() -> (Fixture, FileId) {
|
||||
let read = |name: &str| {
|
||||
let data = std::fs::read(format!("data/{name}.gpx")).unwrap();
|
||||
parse(&data, &mut Default::default()).unwrap()
|
||||
};
|
||||
let mut file = read("with_tracks_and_segments");
|
||||
file.rte = read("with_routes").rte;
|
||||
assert_eq!((file.rte.len(), file.trk.len()), (2, 2));
|
||||
let mut fx = Fixture::default();
|
||||
let id = file.id;
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.order.0.push(id);
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
fn file(fx: &Fixture, id: FileId) -> &File {
|
||||
&fx.files[&id]
|
||||
}
|
||||
|
||||
fn routes(fx: &Fixture, id: FileId, indices: &[usize]) -> Selection {
|
||||
Selection::Route {
|
||||
file_id: id,
|
||||
rte_ids: indices.iter().map(|i| file(fx, id).rte[*i].id).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn whole(id: FileId) -> Selection {
|
||||
Selection::File {
|
||||
file_ids: [id].into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn lengths(file: &File) -> Vec<usize> {
|
||||
file.segments().map(|segment| segment.len()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_segments_of_a_file_are_the_ones_of_its_routes_then_its_tracks() {
|
||||
let (fx, id) = fixture();
|
||||
let file = file(&fx, id);
|
||||
let locations = whole(id).segment_locations(&fx.files, &fx.order.0);
|
||||
let indices: Vec<SegmentIndex> = locations.iter().map(|l| l.index).collect();
|
||||
assert_eq!(
|
||||
indices,
|
||||
[
|
||||
SegmentIndex::Route(0),
|
||||
SegmentIndex::Route(1),
|
||||
SegmentIndex::Track { trk: 0, seg: 0 },
|
||||
SegmentIndex::Track { trk: 0, seg: 1 },
|
||||
SegmentIndex::Track { trk: 1, seg: 0 },
|
||||
SegmentIndex::Track { trk: 1, seg: 1 },
|
||||
]
|
||||
);
|
||||
// a selection of routes covers their segments only
|
||||
let selected = routes(&fx, id, &[1]).segment_locations(&fx.files, &fx.order.0);
|
||||
assert_eq!(selected.len(), 1);
|
||||
assert_eq!(selected[0].segment(&fx.files).id, file.rte[1].seg.id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_routes_are_selected_like_tracks() {
|
||||
let (fx, id) = fixture();
|
||||
let ids: Vec<RouteId> = file(&fx, id).rte.iter().map(|r| r.id).collect();
|
||||
|
||||
let mut selection = routes(&fx, id, &[0]);
|
||||
selection.extend(routes(&fx, id, &[1]));
|
||||
assert_eq!(selection, routes(&fx, id, &[0, 1]));
|
||||
selection.toggle(routes(&fx, id, &[0]));
|
||||
assert_eq!(selection, routes(&fx, id, &[1]));
|
||||
// routes and tracks are not selected together
|
||||
let track = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [file(&fx, id).trk[0].id].into(),
|
||||
};
|
||||
selection.extend(track.clone());
|
||||
assert_eq!(selection, track);
|
||||
|
||||
assert_eq!(
|
||||
routes(&fx, id, &[0]).all_at_level(&fx.files, &fx.order.0),
|
||||
Some(routes(&fx, id, &[0, 1]))
|
||||
);
|
||||
assert_eq!(
|
||||
routes(&fx, id, &[0]).neighbour(&fx.files, &fx.order.0, true),
|
||||
Some(routes(&fx, id, &[1]))
|
||||
);
|
||||
assert_eq!(
|
||||
routes(&fx, id, &[1]).neighbour(&fx.files, &fx.order.0, true),
|
||||
None
|
||||
);
|
||||
assert_eq!(routes(&fx, id, &[1]).to_files(), whole(id));
|
||||
|
||||
let mut gone = Selection::Route {
|
||||
file_id: id,
|
||||
rte_ids: [ids[0], RouteId::default()].into(),
|
||||
};
|
||||
gone.retain_existing(&fx.files);
|
||||
assert_eq!(gone, routes(&fx, id, &[0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_and_duplicate_routes() {
|
||||
let (mut fx, id) = fixture();
|
||||
let kept = file(&fx, id).rte[1].id;
|
||||
fx.selection = routes(&fx, id, &[0]);
|
||||
Delete { whole_files: false }
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(file(&fx, id).rte.len(), 1);
|
||||
assert_eq!(file(&fx, id).rte[0].id, kept);
|
||||
assert_eq!(file(&fx, id).trk.len(), 2);
|
||||
assert_eq!(fx.selection, whole(id));
|
||||
|
||||
fx.selection = routes(&fx, id, &[0]);
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
let file = file(&fx, id);
|
||||
assert_eq!(file.rte.len(), 2);
|
||||
assert_ne!(file.rte[1].id, kept);
|
||||
assert_ne!(file.rte[1].seg.id, file.rte[0].seg.id);
|
||||
assert_eq!(file.rte[1].info, file.rte[0].info);
|
||||
assert_eq!(
|
||||
fx.selection,
|
||||
Selection::Route {
|
||||
file_id: id,
|
||||
rte_ids: [file.rte[1].id].into()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_metadata_and_style_of_routes() {
|
||||
let (mut fx, id) = fixture();
|
||||
fx.selection = routes(&fx, id, &[1]);
|
||||
Metadata {
|
||||
name: "renamed",
|
||||
desc: "about",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
Style {
|
||||
color: Some("00ff00"),
|
||||
opacity: None,
|
||||
width: Some(3.0),
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = file(&fx, id);
|
||||
let info = &file.rte[1].info;
|
||||
assert_eq!(info.name.as_deref(), Some("renamed"));
|
||||
assert_eq!(info.desc.as_deref(), Some("about"));
|
||||
assert_eq!(info.color.as_deref(), Some("00ff00"));
|
||||
assert_eq!(info.width, Some(3.0));
|
||||
assert_ne!(file.rte[0].info.name.as_deref(), Some("renamed"));
|
||||
|
||||
// the style of a file is the one of its routes and tracks
|
||||
fx.selection = whole(id);
|
||||
Style {
|
||||
color: Some("ff0000"),
|
||||
opacity: None,
|
||||
width: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = &fx.files[&id];
|
||||
assert!(
|
||||
file.rte
|
||||
.iter()
|
||||
.map(|r| &r.info)
|
||||
.chain(file.trk.iter().map(|t| &t.info))
|
||||
.all(|info| info.color.as_deref() == Some("ff0000"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_and_paste_routes() {
|
||||
let (mut fx, id) = fixture();
|
||||
let copied = file(&fx, id).rte[0].clone();
|
||||
fx.clipboard = Clipboard::new(&routes(&fx, id, &[0]), &fx.files, &fx.order.0, false);
|
||||
assert!(matches!(
|
||||
fx.clipboard.as_ref().unwrap().content,
|
||||
ClipboardContent::Routes(_)
|
||||
));
|
||||
|
||||
// after the selected route
|
||||
fx.selection = routes(&fx, id, &[1]);
|
||||
Paste.apply(&mut fx.state()).unwrap();
|
||||
let file = file(&fx, id);
|
||||
assert_eq!(file.rte.len(), 3);
|
||||
assert_ne!(file.rte[2].id, copied.id);
|
||||
assert_eq!(file.rte[2].seg.len(), copied.seg.len());
|
||||
assert!(
|
||||
matches!(&fx.selection, Selection::Route { rte_ids, .. } if rte_ids.contains(&file.rte[2].id))
|
||||
);
|
||||
|
||||
// nothing selected: a new file, named like the route
|
||||
fx.selection = Selection::Empty;
|
||||
Paste.apply(&mut fx.state()).unwrap();
|
||||
assert_eq!(fx.order.0.len(), 2);
|
||||
let new = &fx.files[&fx.order.0[1]];
|
||||
assert_eq!(new.rte.len(), 1);
|
||||
assert!(new.trk.is_empty());
|
||||
assert_eq!(Some(&new.info.name), copied.info.name.as_ref());
|
||||
|
||||
// a track selected: among the routes of its file
|
||||
fx.selection = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [fx.files[&id].trk[0].id].into(),
|
||||
};
|
||||
Paste.apply(&mut fx.state()).unwrap();
|
||||
assert_eq!(fx.files[&id].rte.len(), 4);
|
||||
|
||||
// segments do not go in a route, but become routes next to it
|
||||
let file = &fx.files[&id];
|
||||
let track = &file.trk[0];
|
||||
fx.clipboard = Clipboard::new(
|
||||
&Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: track.id,
|
||||
trkseg_ids: [track.trkseg[1].id].into(),
|
||||
},
|
||||
&fx.files,
|
||||
&fx.order.0,
|
||||
false,
|
||||
);
|
||||
fx.selection = routes(&fx, id, &[0]);
|
||||
Paste.apply(&mut fx.state()).unwrap();
|
||||
let file = &fx.files[&id];
|
||||
assert_eq!(file.rte.len(), 5);
|
||||
assert_eq!(file.rte[1].seg.len(), file.trk[0].trkseg[1].len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cut_tracks_pasted_on_a_route_go_first_among_the_tracks() {
|
||||
let (mut fx, id) = fixture();
|
||||
let moved = file(&fx, id).trk[1].id;
|
||||
fx.clipboard = Clipboard::new(
|
||||
&Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [moved].into(),
|
||||
},
|
||||
&fx.files,
|
||||
&fx.order.0,
|
||||
true,
|
||||
);
|
||||
fx.selection = routes(&fx, id, &[0]);
|
||||
Paste.apply(&mut fx.state()).unwrap();
|
||||
let file = file(&fx, id);
|
||||
assert_eq!(file.trk.len(), 2);
|
||||
assert_eq!(file.trk[0].id, moved);
|
||||
assert_eq!(file.rte.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_routes() {
|
||||
let (mut fx, id) = fixture();
|
||||
let ids: Vec<RouteId> = file(&fx, id).rte.iter().map(|r| r.id).collect();
|
||||
Move {
|
||||
what: routes(&fx, id, &[1]),
|
||||
to: MoveTarget::Routes {
|
||||
file_id: id,
|
||||
index: 0,
|
||||
},
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let order: Vec<RouteId> = file(&fx, id).rte.iter().map(|r| r.id).collect();
|
||||
assert_eq!(order, [ids[1], ids[0]]);
|
||||
|
||||
// to the files: a file of its own
|
||||
Move {
|
||||
what: routes(&fx, id, &[0]),
|
||||
to: MoveTarget::Files { index: 1 },
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(file(&fx, id).rte.len(), 1);
|
||||
let new = &fx.files[&fx.order.0[1]];
|
||||
assert_eq!(new.rte[0].id, ids[1]);
|
||||
|
||||
// a segment moved among the routes becomes one
|
||||
let track = &file(&fx, id).trk[0];
|
||||
let segment = track.trkseg[0].id;
|
||||
Move {
|
||||
what: Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: track.id,
|
||||
trkseg_ids: [segment].into(),
|
||||
},
|
||||
to: MoveTarget::Routes {
|
||||
file_id: id,
|
||||
index: 1,
|
||||
},
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = file(&fx, id);
|
||||
assert_eq!(file.rte[1].seg.id, segment);
|
||||
assert_eq!(file.trk[0].trkseg.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_routes_and_tracks_moved_among_each_other_keep_their_kind() {
|
||||
let (mut fx, id) = fixture();
|
||||
let route = file(&fx, id).rte[0].id;
|
||||
let track = file(&fx, id).trk[1].id;
|
||||
|
||||
// a route among the tracks: after the other routes, which come first
|
||||
Move {
|
||||
what: routes(&fx, id, &[0]),
|
||||
to: MoveTarget::Tracks {
|
||||
file_id: id,
|
||||
index: 1,
|
||||
},
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let moved = file(&fx, id);
|
||||
assert_eq!((moved.rte.len(), moved.trk.len()), (2, 2));
|
||||
assert_eq!(moved.rte[1].id, route);
|
||||
assert_eq!(
|
||||
fx.selection,
|
||||
Selection::Route {
|
||||
file_id: id,
|
||||
rte_ids: [route].into()
|
||||
}
|
||||
);
|
||||
|
||||
// a track among the routes: before the other tracks, which come after the routes
|
||||
Move {
|
||||
what: Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [track].into(),
|
||||
},
|
||||
to: MoveTarget::Routes {
|
||||
file_id: id,
|
||||
index: 0,
|
||||
},
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let moved = file(&fx, id);
|
||||
assert_eq!((moved.rte.len(), moved.trk.len()), (2, 2));
|
||||
assert_eq!(moved.trk[0].id, track);
|
||||
|
||||
// and copied routes pasted on a track stay routes
|
||||
fx.clipboard = Clipboard::new(&routes(&fx, id, &[0]), &fx.files, &fx.order.0, false);
|
||||
fx.selection = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [track].into(),
|
||||
};
|
||||
Paste.apply(&mut fx.state()).unwrap();
|
||||
let pasted = file(&fx, id);
|
||||
assert_eq!((pasted.rte.len(), pasted.trk.len()), (3, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_routes() {
|
||||
let (mut fx, id) = fixture();
|
||||
let before = lengths(file(&fx, id));
|
||||
let first = file(&fx, id).rte[0].id;
|
||||
fx.selection = routes(&fx, id, &[0, 1]);
|
||||
let merge = |type_| Merge {
|
||||
type_,
|
||||
remove_gaps: false,
|
||||
keep_originals: false,
|
||||
};
|
||||
assert_eq!(
|
||||
merge(MergeType::Group).apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
merge(MergeType::Connect).apply(&mut fx.state()).unwrap();
|
||||
let file = file(&fx, id);
|
||||
assert_eq!(file.rte.len(), 1);
|
||||
assert_eq!(file.rte[0].id, first);
|
||||
assert_eq!(file.rte[0].seg.len(), before[0] + before[1]);
|
||||
assert_eq!(file.trk.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_files_with_routes() {
|
||||
let (mut fx, id) = fixture();
|
||||
let other = File {
|
||||
rte: file(&fx, id).rte.iter().map(crate::copy_route).collect(),
|
||||
..File::default()
|
||||
};
|
||||
let other_id = other.id;
|
||||
fx.files.insert(other_id, Rc::new(other));
|
||||
fx.order.0.push(other_id);
|
||||
let total: usize = lengths(file(&fx, id)).iter().sum::<usize>()
|
||||
+ lengths(&fx.files[&other_id]).iter().sum::<usize>();
|
||||
|
||||
let mut grouped = Fixture {
|
||||
files: fx.files.clone(),
|
||||
order: fx.order.clone(),
|
||||
selection: Selection::File {
|
||||
file_ids: [id, other_id].into(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
Merge {
|
||||
type_: MergeType::Group,
|
||||
remove_gaps: false,
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut grouped.state())
|
||||
.unwrap();
|
||||
let merged = &grouped.files[&id];
|
||||
assert_eq!((merged.rte.len(), merged.trk.len()), (4, 2));
|
||||
assert!(!grouped.files.contains_key(&other_id));
|
||||
|
||||
// connected in the first route, which comes first
|
||||
fx.selection = Selection::File {
|
||||
file_ids: [id, other_id].into(),
|
||||
};
|
||||
Merge {
|
||||
type_: MergeType::Connect,
|
||||
remove_gaps: false,
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let merged = &fx.files[&id];
|
||||
assert_eq!((merged.rte.len(), merged.trk.len()), (1, 0));
|
||||
assert_eq!(merged.rte[0].seg.len(), total);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_a_file_with_routes() {
|
||||
let (mut fx, id) = fixture();
|
||||
let names: Vec<_> = file(&fx, id)
|
||||
.rte
|
||||
.iter()
|
||||
.map(|r| r.info.name.clone().unwrap())
|
||||
.collect();
|
||||
fx.selection = whole(id);
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
// one file per route and one per track
|
||||
assert_eq!(fx.order.0.len(), 4);
|
||||
let files: Vec<&File> = fx.order.0.iter().map(|id| &*fx.files[id]).collect();
|
||||
for (file, name) in files.iter().zip(&names) {
|
||||
assert_eq!((file.rte.len(), file.trk.len()), (1, 0));
|
||||
assert_eq!(&file.info.name, name);
|
||||
}
|
||||
for file in &files[2..] {
|
||||
assert_eq!((file.rte.len(), file.trk.len()), (0, 2));
|
||||
}
|
||||
|
||||
// a route has nothing to extract
|
||||
let (mut fx, id) = fixture();
|
||||
fx.selection = routes(&fx, id, &[0]);
|
||||
assert_eq!(
|
||||
Extract {
|
||||
keep_originals: false
|
||||
}
|
||||
.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_a_route() {
|
||||
let (mut fx, id) = fixture();
|
||||
let before = file(&fx, id).rte[1].clone();
|
||||
fx.selection = routes(&fx, id, &[1]);
|
||||
Split {
|
||||
at: 10,
|
||||
split_type: SplitType::Routes,
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
// two routes, which share the trackpoint
|
||||
let split = file(&fx, id);
|
||||
assert_eq!(split.rte.len(), 3);
|
||||
assert_eq!(split.rte[1].id, before.id);
|
||||
assert_ne!(split.rte[2].id, before.id);
|
||||
assert_eq!(split.rte[1].seg.len(), 11);
|
||||
assert_eq!(split.rte[2].seg.len(), before.seg.len() - 10);
|
||||
assert_eq!(split.rte[2].info, before.info);
|
||||
|
||||
// a route is not a track nor a segment, and the other way around
|
||||
let (mut fx, id) = fixture();
|
||||
fx.selection = whole(id);
|
||||
let segment_start = lengths(file(&fx, id))[..2].iter().sum::<usize>() as u32;
|
||||
for (at, split_type) in [
|
||||
(10, SplitType::Tracks),
|
||||
(10, SplitType::Segments),
|
||||
(segment_start + 10, SplitType::Routes),
|
||||
] {
|
||||
assert_eq!(
|
||||
Split {
|
||||
at,
|
||||
split_type,
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
assert_eq!(file(&fx, id).rte.len(), 2);
|
||||
|
||||
// into two files: the following routes and the tracks go to the second one
|
||||
let (mut fx, id) = fixture();
|
||||
let first_len = file(&fx, id).rte[0].seg.len();
|
||||
fx.selection = whole(id);
|
||||
Split {
|
||||
at: 5,
|
||||
split_type: SplitType::Files,
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(fx.order.0.len(), 2);
|
||||
let (a, b) = (&fx.files[&fx.order.0[0]], &fx.files[&fx.order.0[1]]);
|
||||
assert_eq!(a.id, id);
|
||||
assert_eq!((a.rte.len(), a.trk.len()), (1, 0));
|
||||
assert_eq!(a.rte[0].seg.len(), 6);
|
||||
assert_eq!((b.rte.len(), b.trk.len()), (2, 2));
|
||||
assert_eq!(b.rte[0].seg.len(), first_len - 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_crop_removes_the_routes_left_without_trackpoints() {
|
||||
let (mut fx, id) = fixture();
|
||||
let len = lengths(file(&fx, id));
|
||||
fx.selection = whole(id);
|
||||
// the second route only
|
||||
let start = len[0] as u32;
|
||||
Crop {
|
||||
start,
|
||||
end: start + len[1] as u32 - 1,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = file(&fx, id);
|
||||
assert_eq!((file.rte.len(), file.trk.len()), (1, 0));
|
||||
assert_eq!(file.rte[0].seg.len(), len[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clean_removes_the_routes_left_without_trackpoints() {
|
||||
let (mut fx, id) = fixture();
|
||||
fx.selection = routes(&fx, id, &[0]);
|
||||
let everywhere = LngLatBounds {
|
||||
sw: LngLat {
|
||||
lng: -180.0,
|
||||
lat: -90.0,
|
||||
},
|
||||
ne: LngLat {
|
||||
lng: 180.0,
|
||||
lat: 90.0,
|
||||
},
|
||||
};
|
||||
Clean {
|
||||
bounds: everywhere,
|
||||
type_: CleanType::Inside,
|
||||
trkpt: true,
|
||||
wpt: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(file(&fx, id).rte.len(), 1);
|
||||
assert_eq!(fx.selection, whole(id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reverse_a_route_and_the_routes_of_a_file() {
|
||||
let (mut fx, id) = fixture();
|
||||
let before = file(&fx, id).clone();
|
||||
fx.selection = routes(&fx, id, &[0]);
|
||||
Reverse.apply(&mut fx.state()).unwrap();
|
||||
let route = &file(&fx, id).rte[0].seg;
|
||||
let original = &before.rte[0].seg;
|
||||
assert_eq!(
|
||||
route[0].coordinates.lat,
|
||||
original[original.len() - 1].coordinates.lat
|
||||
);
|
||||
assert_eq!(file(&fx, id).rte[1].seg.rev_id, before.rte[1].seg.rev_id);
|
||||
|
||||
let (mut fx, id) = fixture();
|
||||
let before = file(&fx, id).clone();
|
||||
fx.selection = whole(id);
|
||||
Reverse.apply(&mut fx.state()).unwrap();
|
||||
let file = file(&fx, id);
|
||||
// the routes are in the opposite order, and still before the tracks
|
||||
assert_eq!(file.rte[0].id, before.rte[1].id);
|
||||
assert_eq!(file.trk[0].id, before.trk[1].id);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ impl Apply for ChangeLoopStart {
|
||||
let index = self.index as usize;
|
||||
let target = find_target(state, index, index)?.ok_or(CommandError::NothingToDo)?;
|
||||
let mut file = (*state.files[&target.location.file_id]).clone();
|
||||
let segment = &mut file.trk[target.location.trk].trkseg[target.location.seg];
|
||||
let segment = file.segment_mut(target.location.index);
|
||||
let (start, len) = (target.start, segment.len());
|
||||
if start == 0 || start >= len {
|
||||
// it already is the start, or it is not a trackpoint
|
||||
|
||||
@@ -13,8 +13,8 @@ pub enum CleanType {
|
||||
}
|
||||
|
||||
/// Removes the trackpoints and/or waypoints of the selection lying inside (or outside) a
|
||||
/// rectangle. A selected file cleans its tracks and its waypoints, a selected track or segment
|
||||
/// only trackpoints, selected waypoints only waypoints.
|
||||
/// rectangle. A selected file cleans its routes, its tracks and its waypoints, a selected route,
|
||||
/// track or segment only trackpoints, selected waypoints only waypoints.
|
||||
#[derive(Debug)]
|
||||
pub struct Clean {
|
||||
pub bounds: LngLatBounds,
|
||||
@@ -85,6 +85,11 @@ struct Cleaner<'a> {
|
||||
impl Editor for Cleaner<'_> {
|
||||
fn file(&mut self, file: &mut File) -> Edit {
|
||||
let mut edit = Edit::Unchanged;
|
||||
for rte in &mut file.rte {
|
||||
if self.route(rte) == Edit::Changed {
|
||||
edit = Edit::Changed;
|
||||
}
|
||||
}
|
||||
for trk in &mut file.trk {
|
||||
if self.track(trk) == Edit::Changed {
|
||||
edit = Edit::Changed;
|
||||
@@ -124,24 +129,21 @@ impl Editor for Cleaner<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes what the clean left empty: the emptied segments and the tracks that lost all their
|
||||
/// segments. A file is never removed, even if nothing is left in it: it stays, empty. The
|
||||
/// Removes what the clean left empty: the emptied segments, the routes of these segments and the
|
||||
/// tracks that lost all their segments. A file is never removed, even if nothing is left in it: it stays, empty. The
|
||||
/// selection falls back to the closest element still existing.
|
||||
fn prune(state: &mut State, emptied: &HashSet<TrackSegmentId>, wpt_files: &HashSet<FileId>) {
|
||||
let ids: Vec<FileId> = state
|
||||
.files
|
||||
.iter()
|
||||
.filter(|(id, file)| {
|
||||
wpt_files.contains(id)
|
||||
|| file
|
||||
.trk
|
||||
.iter()
|
||||
.any(|trk| trk.trkseg.iter().any(|seg| emptied.contains(&seg.id)))
|
||||
wpt_files.contains(id) || file.segments().any(|seg| emptied.contains(&seg.id))
|
||||
})
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
for id in ids {
|
||||
let mut file = (*state.files[&id]).clone();
|
||||
file.rte.retain(|rte| !emptied.contains(&rte.seg.id));
|
||||
file.trk.retain_mut(|trk| {
|
||||
let len = trk.trkseg.len();
|
||||
trk.trkseg.retain(|seg| !emptied.contains(&seg.id));
|
||||
@@ -167,6 +169,27 @@ fn fixed_selection(state: &State) -> Selection {
|
||||
Selection::File { file_ids }
|
||||
}
|
||||
}
|
||||
Selection::Route { file_id, rte_ids } => {
|
||||
let Some(file) = files.get(file_id) else {
|
||||
return Selection::Empty;
|
||||
};
|
||||
let rte_ids: HashSet<_> = file
|
||||
.rte
|
||||
.iter()
|
||||
.map(|rte| rte.id)
|
||||
.filter(|id| rte_ids.contains(id))
|
||||
.collect();
|
||||
if rte_ids.is_empty() {
|
||||
Selection::File {
|
||||
file_ids: [*file_id].into(),
|
||||
}
|
||||
} else {
|
||||
Selection::Route {
|
||||
file_id: *file_id,
|
||||
rte_ids,
|
||||
}
|
||||
}
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let Some(file) = files.get(file_id) else {
|
||||
return Selection::Empty;
|
||||
|
||||
@@ -11,9 +11,9 @@ use crate::{
|
||||
/// Keeps the trackpoints `start` to `end` (both included) of the selection, which are counted
|
||||
/// over its segments one after the other (as in the `RoutingBuffer`), and removes the others.
|
||||
///
|
||||
/// The segments that have no trackpoint left are removed. So are the tracks that have no segment
|
||||
/// left, if tracks or files are selected (not if only segments are), and the files that have no
|
||||
/// trackpoint left, if files are selected. A range that goes past the last trackpoint stops at it.
|
||||
/// The segments that have no trackpoint left are removed, and so are the routes of these segments.
|
||||
/// So are the tracks that have no segment left, if tracks or files are selected (not if only
|
||||
/// segments are), and the files that have no trackpoint left, if files are selected. A range that goes past the last trackpoint stops at it.
|
||||
#[derive(Debug)]
|
||||
pub struct Crop {
|
||||
pub start: u32,
|
||||
@@ -43,8 +43,7 @@ impl Apply for Crop {
|
||||
.segment_locations(state.files, &state.order.0)
|
||||
.into_iter()
|
||||
.map(|location| {
|
||||
let len =
|
||||
state.files[&location.file_id].trk[location.trk].trkseg[location.seg].len();
|
||||
let len = location.segment(state.files).len();
|
||||
let keep = (len > 0 && start < offset + len && end >= offset)
|
||||
.then(|| (start.saturating_sub(offset), (end - offset).min(len - 1)));
|
||||
offset += len;
|
||||
@@ -73,14 +72,14 @@ impl Apply for Crop {
|
||||
let mut removed: HashSet<TrackSegmentId> = HashSet::new();
|
||||
let mut had_points: HashSet<FileId> = HashSet::new();
|
||||
for plan in &plans {
|
||||
let SegmentLocation { file_id, trk, seg } = plan.location;
|
||||
let SegmentLocation { file_id, index } = plan.location;
|
||||
let file = files
|
||||
.entry(file_id)
|
||||
.or_insert_with(|| (*state.files[&file_id]).clone());
|
||||
if plan.len > 0 {
|
||||
had_points.insert(file_id);
|
||||
}
|
||||
let segment = &mut file.trk[trk].trkseg[seg];
|
||||
let segment = file.segment_mut(index);
|
||||
match plan.keep {
|
||||
None => {
|
||||
removed.insert(segment.id);
|
||||
@@ -91,6 +90,8 @@ impl Apply for Crop {
|
||||
|
||||
let mut gone: Vec<FileId> = vec![];
|
||||
for (file_id, file) in files.iter_mut() {
|
||||
// a route has its segment, or is not there
|
||||
file.rte.retain(|route| !removed.contains(&route.seg.id));
|
||||
// the tracks that lost segments
|
||||
let touched: Vec<bool> = file
|
||||
.trk
|
||||
@@ -108,7 +109,7 @@ impl Apply for Crop {
|
||||
file.trk
|
||||
.retain(|track| !(touched.next().unwrap() && track.trkseg.is_empty()));
|
||||
}
|
||||
if prune_files && file.trk.is_empty() && had_points.contains(file_id) {
|
||||
if prune_files && file.has_no_path() && had_points.contains(file_id) {
|
||||
gone.push(*file_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,7 @@ impl Apply for Elevation<'_> {
|
||||
.segment_locations(state.files, &state.order.0);
|
||||
let count: usize = locations
|
||||
.iter()
|
||||
.map(|location| {
|
||||
state.files[&location.file_id].trk[location.trk].trkseg[location.seg].len()
|
||||
})
|
||||
.map(|location| location.segment(state.files).len())
|
||||
.sum();
|
||||
if count == 0 {
|
||||
return Err(CommandError::NothingToDo);
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, File, FileId, LngLat, LngLatBounds, Selection, State, Track, TrackId,
|
||||
TrackSegment, Waypoint, Waypoints, copy_file, copy_waypoint, crossarc_lnglat, distance,
|
||||
Apply, CommandError, File, FileId, LngLat, LngLatBounds, Route, Selection, State, Track,
|
||||
TrackId, TrackSegment, Waypoint, Waypoints, copy_file, copy_waypoint, crossarc_lnglat,
|
||||
distance,
|
||||
};
|
||||
|
||||
/// Splits the selected elements that are made of several segments into elements that have one
|
||||
/// each:
|
||||
///
|
||||
/// - files: a file with several tracks becomes one file per track, in which the track is split
|
||||
/// into one track per segment. A file with a single track becomes one file per segment. The new
|
||||
/// files are in the place of the file, which goes, and are selected. They are named after the
|
||||
/// track, or the file, and numbered. The waypoints go to the files that have the segments
|
||||
/// - files: a file with several routes and tracks becomes one file per route and per track, in
|
||||
/// which the track is split into one track per segment. A file with a single track becomes one
|
||||
/// file per segment. The new files are in the place of the file, which goes, and are selected.
|
||||
/// They are named after the route or track, or the file, and numbered. The waypoints go to the files that have the segments
|
||||
/// closest to them (to the first file if the file has no trackpoint).
|
||||
/// - tracks: each selected track is replaced by one track per segment, which are selected. They
|
||||
/// are named after the track and numbered.
|
||||
///
|
||||
/// What has a single segment is left as it is. There is nothing to do if nothing was changed.
|
||||
/// What has a single segment is left as it is, which routes always are. There is nothing to do if nothing was changed.
|
||||
///
|
||||
/// With `keep_originals`, the files that are extracted stay where they are, and the new files come
|
||||
/// right after each of them, and only the new files are selected. Extracting
|
||||
@@ -46,8 +47,7 @@ fn extract_files(
|
||||
let mut extracted: Vec<(FileId, Vec<File>)> = vec![];
|
||||
for id in state.order.0.iter().filter(|id| file_ids.contains(id)) {
|
||||
let file = &state.files[id];
|
||||
let segments: usize = file.trk.iter().map(|track| track.trkseg.len()).sum();
|
||||
if segments > 1 {
|
||||
if file.segments().count() > 1 {
|
||||
let mut files = extract_file(file);
|
||||
if keep_originals {
|
||||
// the tracks and segments are the ones of the file, which stays: they need new ids
|
||||
@@ -95,60 +95,65 @@ fn extract_files(
|
||||
|
||||
/// The files that a file with several segments becomes.
|
||||
fn extract_file(file: &File) -> Vec<File> {
|
||||
// for each new file: its tracks and its name, and the segments that go with it
|
||||
// for each new file: its routes, its tracks and its name, and the segments that go with it
|
||||
struct Part {
|
||||
routes: Vec<Route>,
|
||||
tracks: Vec<Track>,
|
||||
name: String,
|
||||
}
|
||||
let (parts, owner): (Vec<Part>, Vec<usize>) = if file.trk.len() > 1 {
|
||||
// one file per track, the segments of a track follow it
|
||||
let parts = file
|
||||
.trk
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, track)| Part {
|
||||
tracks: split_track(track),
|
||||
name: track
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("{} ({})", file.info.name, i + 1)),
|
||||
})
|
||||
.collect();
|
||||
let owner = file
|
||||
.trk
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(i, track)| track.trkseg.iter().map(move |_| i))
|
||||
.collect();
|
||||
(parts, owner)
|
||||
} else {
|
||||
// one file per segment
|
||||
let track = &file.trk[0];
|
||||
let base = track
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| file.info.name.clone());
|
||||
let parts = track
|
||||
.trkseg
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, segment)| Part {
|
||||
tracks: vec![Track {
|
||||
id: if i == 0 { track.id } else { Default::default() },
|
||||
trkseg: vec![segment.clone()],
|
||||
..track.clone()
|
||||
}],
|
||||
name: format!("{base} ({})", i + 1),
|
||||
})
|
||||
.collect();
|
||||
let owner = (0..track.trkseg.len()).collect();
|
||||
(parts, owner)
|
||||
let name = |name: &Option<String>, i: usize| {
|
||||
name.clone()
|
||||
.unwrap_or_else(|| format!("{} ({})", file.info.name, i + 1))
|
||||
};
|
||||
let (parts, owner): (Vec<Part>, Vec<usize>) =
|
||||
if file.rte.len() + file.trk.len() > 1 {
|
||||
// one file per route and per track, the segments of a track follow it
|
||||
let routes = file.rte.iter().enumerate().map(|(i, route)| Part {
|
||||
routes: vec![route.clone()],
|
||||
tracks: vec![],
|
||||
name: name(&route.info.name, i),
|
||||
});
|
||||
let tracks = file.trk.iter().enumerate().map(|(i, track)| Part {
|
||||
routes: vec![],
|
||||
tracks: split_track(track),
|
||||
name: name(&track.info.name, file.rte.len() + i),
|
||||
});
|
||||
let parts = routes.chain(tracks).collect();
|
||||
let owner =
|
||||
(0..file.rte.len())
|
||||
.chain(file.trk.iter().enumerate().flat_map(|(i, track)| {
|
||||
track.trkseg.iter().map(move |_| file.rte.len() + i)
|
||||
}))
|
||||
.collect();
|
||||
(parts, owner)
|
||||
} else {
|
||||
// one file per segment
|
||||
let track = &file.trk[0];
|
||||
let base = track
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| file.info.name.clone());
|
||||
let parts = track
|
||||
.trkseg
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, segment)| Part {
|
||||
routes: vec![],
|
||||
tracks: vec![Track {
|
||||
id: if i == 0 { track.id } else { Default::default() },
|
||||
trkseg: vec![segment.clone()],
|
||||
..track.clone()
|
||||
}],
|
||||
name: format!("{base} ({})", i + 1),
|
||||
})
|
||||
.collect();
|
||||
let owner = (0..track.trkseg.len()).collect();
|
||||
(parts, owner)
|
||||
};
|
||||
|
||||
// the waypoints go with the segments that are the closest to them
|
||||
let segments: Vec<&TrackSegment> = file.trk.iter().flat_map(|track| &track.trkseg).collect();
|
||||
let segments: Vec<&TrackSegment> = file.segments().collect();
|
||||
let mut waypoints: Vec<Vec<Waypoint>> = vec![vec![]; parts.len()];
|
||||
for waypoint in file.wpt.iter() {
|
||||
let mut owners: Vec<usize> = closest_segments(&segments, waypoint.coordinates)
|
||||
@@ -176,6 +181,7 @@ fn extract_file(file: &File) -> Vec<File> {
|
||||
.map(|(part, waypoints)| {
|
||||
let mut extracted = File {
|
||||
id: Default::default(),
|
||||
rte: part.routes,
|
||||
trk: part.tracks,
|
||||
wpt: Waypoints::default(),
|
||||
..file.clone()
|
||||
|
||||
@@ -39,7 +39,7 @@ impl Apply for InsertAnchor {
|
||||
.selection
|
||||
.segment_locations(state.files, &state.order.0)
|
||||
{
|
||||
let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg];
|
||||
let segment = location.segment(state.files);
|
||||
let mut points = segment.iter();
|
||||
let Some(mut previous) = points.next() else {
|
||||
continue;
|
||||
@@ -81,7 +81,7 @@ impl Apply for InsertAnchor {
|
||||
let closest = closest.ok_or(CommandError::NothingToDo)?;
|
||||
|
||||
let mut file = (*state.files[&closest.location.file_id]).clone();
|
||||
let segment = &mut file.trk[closest.location.trk].trkseg[closest.location.seg];
|
||||
let segment = file.segment_mut(closest.location.index);
|
||||
let before = closest.before;
|
||||
// on a trackpoint already, if it is closer than `SNAP_DISTANCE`
|
||||
let snapped = if segment.len() == 1 {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, FileId, GlobalStatistics, Selection, State, Statistics, Track, TrackId,
|
||||
TrackSegment, TrackSegmentId, Waypoint, Waypoints, copy_track, replace_trackpoints,
|
||||
Apply, CommandError, FileId, GlobalStatistics, Route, RouteId, Selection, State, Statistics,
|
||||
Track, TrackId, TrackSegment, TrackSegmentId, Waypoint, Waypoints, copy_route, copy_track,
|
||||
replace_trackpoints,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
|
||||
@@ -16,13 +17,16 @@ pub enum MergeType {
|
||||
|
||||
/// Merges the selected elements into the first of them:
|
||||
///
|
||||
/// - files: the other files are deleted. The tracks of all of them are in the first file, as
|
||||
/// they are with `Group`, or all connected in the first track with `Connect`. The waypoints of
|
||||
/// - files: the other files are deleted. The routes and tracks of all of them are in the first
|
||||
/// file, as they are with `Group`, or all connected in the first of them (a route or a track,
|
||||
/// the routes of a file coming before its tracks) with `Connect`. The waypoints of
|
||||
/// all of them are in the first file too, except the ones that are the same. With
|
||||
/// `keep_originals`, no file is deleted nor changed: the result is a new file (with the info of
|
||||
/// the first one and new ids), which comes right after the last of the files and is selected.
|
||||
/// - tracks of a file: the first track gets their segments, as they are with `Group`, or all
|
||||
/// connected in one segment with `Connect`. The other tracks are removed.
|
||||
/// - routes of a file: the first route gets their trackpoints, all connected with `Connect`, and
|
||||
/// the other routes are removed. A route has a single segment, so there is nothing to `Group`.
|
||||
/// - segments of a track: they are all connected in a segment at the position of the first one
|
||||
/// with `Connect`. With `Group` they are put one after the other, at that position.
|
||||
///
|
||||
@@ -44,6 +48,7 @@ impl Apply for Merge {
|
||||
let selection = state.selection.clone();
|
||||
match &selection {
|
||||
Selection::File { file_ids } => self.merge_files(state, file_ids),
|
||||
Selection::Route { file_id, rte_ids } => self.merge_routes(state, *file_id, rte_ids),
|
||||
Selection::Track { file_id, trk_ids } => self.merge_tracks(state, *file_id, trk_ids),
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
@@ -77,21 +82,26 @@ impl Merge {
|
||||
return Err(CommandError::NothingToDo);
|
||||
};
|
||||
|
||||
let mut routes: Vec<Route> = vec![];
|
||||
let mut tracks: Vec<Track> = vec![];
|
||||
let mut segments: Vec<TrackSegment> = vec![];
|
||||
let mut waypoints: Vec<Waypoint> = vec![];
|
||||
for id in &ordered {
|
||||
let file = &state.files[id];
|
||||
routes.extend(file.rte.iter().cloned());
|
||||
tracks.extend(file.trk.iter().cloned());
|
||||
segments.extend(file.segments().cloned());
|
||||
for waypoint in file.wpt.iter() {
|
||||
if !waypoints.iter().any(|kept| kept.same_content(waypoint)) {
|
||||
waypoints.push(waypoint.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let segments: Vec<TrackSegment> = tracks
|
||||
.iter()
|
||||
.flat_map(|track| track.trkseg.iter().cloned())
|
||||
.collect();
|
||||
// the first route or track, which holds the connected segment
|
||||
let first_is_route = ordered.iter().find_map(|id| {
|
||||
let file = &state.files[id];
|
||||
(!file.has_no_path()).then(|| !file.rte.is_empty())
|
||||
});
|
||||
|
||||
match self.type_ {
|
||||
MergeType::Group if ordered.len() == 1 => return Err(CommandError::NothingToDo),
|
||||
@@ -99,24 +109,35 @@ impl Merge {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
MergeType::Connect if !segments.is_empty() => {
|
||||
// a single track, which is the first one, with the connected segment
|
||||
// a single route or track, which is the first one, with the connected segment
|
||||
let merged = connect(&segments, segments[0].id, self.remove_gaps);
|
||||
let first = Track {
|
||||
trkseg: vec![merged],
|
||||
..tracks[0].clone()
|
||||
};
|
||||
tracks = vec![first];
|
||||
if first_is_route == Some(true) {
|
||||
let first = Route {
|
||||
seg: merged,
|
||||
..routes[0].clone()
|
||||
};
|
||||
(routes, tracks) = (vec![first], vec![]);
|
||||
} else {
|
||||
let first = Track {
|
||||
trkseg: vec![merged],
|
||||
..tracks[0].clone()
|
||||
};
|
||||
(routes, tracks) = (vec![], vec![first]);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut target = (*state.files[&target_id]).clone();
|
||||
target.rte = routes;
|
||||
target.trk = tracks;
|
||||
target.wpt = Waypoints::default();
|
||||
target.wpt.insert_at(0, waypoints);
|
||||
if self.keep_originals {
|
||||
// a new file, whose tracks and segments do not share their ids with the originals
|
||||
// a new file, whose routes, tracks and segments do not share their ids with the
|
||||
// originals
|
||||
target.id = Default::default();
|
||||
target.rte = target.rte.iter().map(copy_route).collect();
|
||||
target.trk = target.trk.iter().map(copy_track).collect();
|
||||
let at = state
|
||||
.order
|
||||
@@ -139,6 +160,33 @@ impl Merge {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn merge_routes(
|
||||
&self,
|
||||
state: &mut State,
|
||||
file_id: FileId,
|
||||
rte_ids: &HashSet<RouteId>,
|
||||
) -> Result<(), CommandError> {
|
||||
let file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let selected: Vec<usize> = (0..file.rte.len())
|
||||
.filter(|i| rte_ids.contains(&file.rte[*i].id))
|
||||
.collect();
|
||||
if selected.len() < 2 || self.type_ == MergeType::Group {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
let first = selected[0];
|
||||
let segments: Vec<TrackSegment> =
|
||||
selected.iter().map(|i| file.rte[*i].seg.clone()).collect();
|
||||
let mut file = (**file).clone();
|
||||
file.rte[first].seg = connect(&segments, segments[0].id, self.remove_gaps);
|
||||
let mut index = 0;
|
||||
file.rte.retain(|_| {
|
||||
index += 1;
|
||||
!selected[1..].contains(&(index - 1))
|
||||
});
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn merge_tracks(
|
||||
&self,
|
||||
state: &mut State,
|
||||
|
||||
@@ -58,7 +58,8 @@ impl Apply for NewWaypoint<'_> {
|
||||
.filter(|id| file_ids.contains(id))
|
||||
.copied()
|
||||
.collect(),
|
||||
Selection::Track { file_id, .. }
|
||||
Selection::Route { file_id, .. }
|
||||
| Selection::Track { file_id, .. }
|
||||
| Selection::TrackSegment { file_id, .. }
|
||||
| Selection::Waypoints { file_id }
|
||||
| Selection::Waypoint { file_id, .. } => vec![*file_id],
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Apply for Reduce {
|
||||
let mut changed = vec![];
|
||||
let mut kept_points = vec![];
|
||||
for location in &locations {
|
||||
let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg];
|
||||
let segment = location.segment(state.files);
|
||||
let kept = reduce_indices(segment, self.tolerance);
|
||||
if kept.len() == segment.len() {
|
||||
continue;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, File, FileId, Selection, State, TrackId, TrackSegmentId, end_time,
|
||||
reverse_segment, reverse_segments, reverse_tracks, start_time, track_end_time,
|
||||
track_start_time,
|
||||
Apply, CommandError, File, FileId, RouteId, Selection, State, TrackId, TrackSegmentId,
|
||||
end_time, reverse_routes, reverse_segment, reverse_segments, reverse_tracks, start_time,
|
||||
track_end_time, track_start_time,
|
||||
};
|
||||
|
||||
/// Reverses the direction of the selected elements: the whole of each selected file (its tracks,
|
||||
/// the segments of its tracks and their trackpoints are in the opposite order), of each selected
|
||||
/// track (its segments and their trackpoints), or each selected segment on its own (its
|
||||
/// Reverses the direction of the selected elements: the whole of each selected file (its routes,
|
||||
/// its tracks, the segments of its tracks and their trackpoints are in the opposite order; the
|
||||
/// routes and the tracks are two wholes, as the routes stay before the tracks), of each selected
|
||||
/// track (its segments and their trackpoints), or each selected route or segment on its own (its
|
||||
/// trackpoints). The timestamps are mirrored, see [`reverse_segment`].
|
||||
#[derive(Debug)]
|
||||
pub struct Reverse;
|
||||
@@ -20,9 +21,7 @@ impl Apply for Reverse {
|
||||
.segment_locations(state.files, &state.order.0);
|
||||
let points: usize = locations
|
||||
.iter()
|
||||
.map(|location| {
|
||||
state.files[&location.file_id].trk[location.trk].trkseg[location.seg].len()
|
||||
})
|
||||
.map(|location| location.segment(state.files).len())
|
||||
.sum();
|
||||
if points <= 1 {
|
||||
return Err(CommandError::NothingToDo);
|
||||
@@ -40,12 +39,18 @@ impl Apply for Reverse {
|
||||
Selection::File { file_ids } => {
|
||||
for file_id in file_ids {
|
||||
edit(file_id, &|file| {
|
||||
let end = file.rte.last().and_then(|rte| end_time(&rte.seg));
|
||||
let start = file.rte.first().and_then(|rte| start_time(&rte.seg));
|
||||
reverse_routes(&mut file.rte, end, start);
|
||||
let end = file.trk.last().and_then(track_end_time);
|
||||
let start = file.trk.first().and_then(track_start_time);
|
||||
reverse_tracks(&mut file.trk, end, start);
|
||||
});
|
||||
}
|
||||
}
|
||||
Selection::Route { file_id, rte_ids } => edit(file_id, &|file| {
|
||||
reverse_selected_routes(file, rte_ids);
|
||||
}),
|
||||
Selection::Track { file_id, trk_ids } => edit(file_id, &|file| {
|
||||
reverse_selected_tracks(file, trk_ids);
|
||||
}),
|
||||
@@ -66,6 +71,13 @@ impl Apply for Reverse {
|
||||
}
|
||||
}
|
||||
|
||||
fn reverse_selected_routes(file: &mut File, ids: &std::collections::HashSet<RouteId>) {
|
||||
for route in file.rte.iter_mut().filter(|route| ids.contains(&route.id)) {
|
||||
let (end, start) = (end_time(&route.seg), start_time(&route.seg));
|
||||
reverse_segment(&mut route.seg, end, start);
|
||||
}
|
||||
}
|
||||
|
||||
fn reverse_selected_tracks(file: &mut File, ids: &std::collections::HashSet<TrackId>) {
|
||||
for track in file.trk.iter_mut().filter(|track| ids.contains(&track.id)) {
|
||||
let end = track.trkseg.last().and_then(end_time);
|
||||
|
||||
@@ -14,7 +14,7 @@ impl Apply for RoundTrip {
|
||||
.selection
|
||||
.segment_locations(state.files, &state.order.0)
|
||||
.into_iter()
|
||||
.filter(|l| state.files[&l.file_id].trk[l.trk].trkseg[l.seg].len() >= 2)
|
||||
.filter(|l| l.segment(state.files).len() >= 2)
|
||||
.collect();
|
||||
if locations.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Apply, Categories, CommandError, FileId, SegmentLocation, Selection, State, Statistics, Track,
|
||||
TrackSegment, Trackpoint, distance, replace_trackpoints,
|
||||
Apply, Categories, CommandError, FileId, SegmentIndex, SegmentLocation, Selection, State,
|
||||
Statistics, Track, TrackSegment, Trackpoint, distance, replace_trackpoints,
|
||||
};
|
||||
|
||||
/// The values of an OSM attribute (surface, highway...) of the new trackpoints of a [`Route`], as
|
||||
/// The values of an OSM attribute (surface, highway...) of the new trackpoints of a [`Reroute`], as
|
||||
/// the intervals of consecutive trackpoints that share one: `starts[i]` is the index of the first
|
||||
/// trackpoint of the interval `i`, and `values[i]` its value, 0 when unknown, else 1 + the index
|
||||
/// of its name in `names`. No intervals means that the attribute is unknown everywhere.
|
||||
@@ -64,7 +64,7 @@ impl RouteCategory<'_> {
|
||||
/// anchors anyway. The ones right before and after the range, between which the route goes, become
|
||||
/// shown at every zoom level as well.
|
||||
#[derive(Debug)]
|
||||
pub struct Route<'a> {
|
||||
pub struct Reroute<'a> {
|
||||
pub start: u32,
|
||||
pub end: u32,
|
||||
pub lng: &'a [f64],
|
||||
@@ -89,7 +89,7 @@ pub(super) struct Target {
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
impl Apply for Route<'_> {
|
||||
impl Apply for Reroute<'_> {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let (start, end) = (self.start as usize, self.end as usize);
|
||||
let len = self.lng.len();
|
||||
@@ -152,14 +152,17 @@ impl Apply for Route<'_> {
|
||||
}
|
||||
let seg = file.trk[trk].trkseg.len() - 1;
|
||||
Target {
|
||||
location: SegmentLocation { file_id, trk, seg },
|
||||
location: SegmentLocation {
|
||||
file_id,
|
||||
index: SegmentIndex::Track { trk, seg },
|
||||
},
|
||||
start: 0,
|
||||
end: 0,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let segment = &mut file.trk[target.location.trk].trkseg[target.location.seg];
|
||||
let segment = file.segment_mut(target.location.index);
|
||||
let new_len = points.len();
|
||||
let (speed, start_time) = speed_and_start_time(segment, &target, &points);
|
||||
let has_times = !segment.is_empty() && segment[0].time.is_some();
|
||||
@@ -208,7 +211,7 @@ pub(super) fn find_target(
|
||||
let segments: Vec<(SegmentLocation, usize)> = locations
|
||||
.into_iter()
|
||||
.map(|location| {
|
||||
let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg];
|
||||
let segment = location.segment(state.files);
|
||||
(location, segment.len())
|
||||
})
|
||||
.collect();
|
||||
@@ -382,8 +385,8 @@ mod tests {
|
||||
lng: &'a [f64],
|
||||
lat: &'a [f64],
|
||||
ele: &'a [f64],
|
||||
) -> Route<'a> {
|
||||
Route {
|
||||
) -> Reroute<'a> {
|
||||
Reroute {
|
||||
start,
|
||||
end,
|
||||
lng,
|
||||
@@ -459,7 +462,7 @@ mod tests {
|
||||
route(0, 0, &[1.0], &[], &[]).apply(&mut fx.state()),
|
||||
route(0, len + 1, &[], &[], &[]).apply(&mut fx.state()),
|
||||
route(len + 1, len + 1, &[1.0], &[1.0], &[1.0]).apply(&mut fx.state()),
|
||||
Route {
|
||||
Reroute {
|
||||
anchors: &[1],
|
||||
..route(0, 0, &[1.0], &[1.0], &[1.0])
|
||||
}
|
||||
@@ -589,7 +592,7 @@ mod tests {
|
||||
|
||||
// 5 new points replacing the 2 in the middle, two of them are anchors
|
||||
let (lng, lat, ele) = ([1.0; 5], [2.0; 5], [3.0; 5]);
|
||||
Route {
|
||||
Reroute {
|
||||
anchors: &[1, 3],
|
||||
..route(1, 3, &lng, &lat, &ele)
|
||||
}
|
||||
@@ -639,7 +642,7 @@ mod tests {
|
||||
let names = ["asphalt".to_string(), "gravel".to_string()];
|
||||
let (lng, lat, ele) = ([1.0; 4], [2.0; 4], [3.0; 4]);
|
||||
// asphalt, asphalt, unknown, gravel
|
||||
Route {
|
||||
Reroute {
|
||||
surface: RouteCategory {
|
||||
starts: &[0, 2, 3],
|
||||
values: &[1, 0, 2],
|
||||
@@ -683,7 +686,7 @@ mod tests {
|
||||
},
|
||||
];
|
||||
for surface in broken {
|
||||
let result = Route {
|
||||
let result = Reroute {
|
||||
surface,
|
||||
..route(0, 0, &lng, &lat, &ele)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, File, Selection, State, Track, TrackSegment, Waypoints, copy_file,
|
||||
copy_segment, copy_track, copy_waypoint,
|
||||
Apply, CommandError, File, Route, SegmentIndex, Selection, State, Track, TrackSegment,
|
||||
Waypoints, copy_file, copy_route, copy_segment, copy_track, copy_waypoint,
|
||||
};
|
||||
|
||||
use super::route::find_target;
|
||||
@@ -13,24 +13,27 @@ use super::route::find_target;
|
||||
pub enum SplitType {
|
||||
/// The file becomes two files, which both have the waypoints (copies, in the second one).
|
||||
Files,
|
||||
/// The route becomes two routes.
|
||||
Routes,
|
||||
/// The track becomes two tracks.
|
||||
Tracks,
|
||||
/// The segment becomes two segments.
|
||||
/// The segment of a track becomes two segments.
|
||||
Segments,
|
||||
}
|
||||
|
||||
/// Cuts the file, the track or the segment of the trackpoint `at` of the selection (see
|
||||
/// Cuts the file, the route, the track or the segment of the trackpoint `at` of the selection (see
|
||||
/// `RoutingBuffer`) in two, there: the trackpoint is the last one of the first part and the first
|
||||
/// one of the second part.
|
||||
/// one of the second part. Cutting a route needs the trackpoint to be in a route, and cutting a
|
||||
/// track or a segment needs it to be in a track: there is nothing to do otherwise.
|
||||
///
|
||||
/// The first part keeps the ids of what is cut, the second one has new ids, as the elements that
|
||||
/// come after the trackpoint (the following segments of the track, the following tracks of the
|
||||
/// file), which are moved to it. When the file is cut, the new file comes right after it. The
|
||||
/// selection does not change.
|
||||
/// come after the trackpoint (the following segments of the track, the following routes and
|
||||
/// tracks of the file, the routes coming before the tracks), which are moved to it. When the file
|
||||
/// is cut, the new file comes right after it. The selection does not change.
|
||||
///
|
||||
/// With `keep_originals`, a file that is cut (`SplitType::Files`) is left as it is: its two parts
|
||||
/// are two new files, with new ids, which come right after it and are selected. Cutting a track
|
||||
/// or a segment changes the file in place anyway, so the flag has no effect on them.
|
||||
/// are two new files, with new ids, which come right after it and are selected. Cutting a route, a
|
||||
/// track or a segment changes the file in place anyway, so the flag has no effect on them.
|
||||
#[derive(Debug)]
|
||||
pub struct Split {
|
||||
pub at: u32,
|
||||
@@ -43,104 +46,115 @@ impl Apply for Split {
|
||||
let at = self.at as usize;
|
||||
let target = find_target(state, at, at)?.ok_or(CommandError::NothingToDo)?;
|
||||
let location = target.location;
|
||||
let (file_id, trk, seg, index) =
|
||||
(location.file_id, location.trk, location.seg, target.start);
|
||||
let (file_id, index) = (location.file_id, target.start);
|
||||
let in_route = matches!(location.index, SegmentIndex::Route(_));
|
||||
let fits = match self.split_type {
|
||||
SplitType::Files => true,
|
||||
SplitType::Routes => in_route,
|
||||
SplitType::Tracks | SplitType::Segments => !in_route,
|
||||
};
|
||||
if !fits {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
let mut file = (*state.files[&file_id]).clone();
|
||||
if index >= file.trk[trk].trkseg[seg].len() {
|
||||
if index >= file.segment(location.index).len() {
|
||||
return Err(CommandError::InvalidData(
|
||||
"trackpoint out of the segment".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let track = file.trk[trk].clone();
|
||||
let segment = &track.trkseg[seg];
|
||||
let (first, second) = (keep_up_to(segment, index), keep_from(segment, index));
|
||||
// the track is cut: what is before the segment, and its first part, then the second part
|
||||
// and what is after it
|
||||
let split_track = || {
|
||||
let mut first_track = track.clone();
|
||||
first_track.trkseg.truncate(seg);
|
||||
first_track.trkseg.push(first.clone());
|
||||
let mut second_track = Track {
|
||||
id: Default::default(),
|
||||
trkseg: vec![second.clone()],
|
||||
..track.clone()
|
||||
};
|
||||
second_track
|
||||
.trkseg
|
||||
.extend(track.trkseg[seg + 1..].iter().map(copy_segment));
|
||||
(first_track, second_track)
|
||||
// with `SplitType::Files`, what goes to the second file: what comes after the trackpoint
|
||||
let mut second_file = File {
|
||||
id: Default::default(),
|
||||
rte: vec![],
|
||||
trk: vec![],
|
||||
wpt: Waypoints::default(),
|
||||
..file.clone()
|
||||
};
|
||||
|
||||
match self.split_type {
|
||||
SplitType::Segments => {
|
||||
file.trk[trk].trkseg.splice(seg..=seg, [first, second]);
|
||||
}
|
||||
SplitType::Tracks => {
|
||||
let (first_track, second_track) = split_track();
|
||||
file.trk.splice(trk..=trk, [first_track, second_track]);
|
||||
}
|
||||
SplitType::Files if self.keep_originals => {
|
||||
let (first_track, second_track) = split_track();
|
||||
// the parts are new files: nothing is shared by id with the file, nor between them
|
||||
let first_file = copy_file(&File {
|
||||
trk: file.trk[..trk]
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain([first_track])
|
||||
.collect(),
|
||||
..file.clone()
|
||||
});
|
||||
let mut second_file = File {
|
||||
match location.index {
|
||||
SegmentIndex::Route(rte) => {
|
||||
let route = file.rte[rte].clone();
|
||||
let first = Route {
|
||||
seg: keep_up_to(&route.seg, index),
|
||||
..route.clone()
|
||||
};
|
||||
let second = Route {
|
||||
id: Default::default(),
|
||||
trk: vec![second_track],
|
||||
wpt: Waypoints::default(),
|
||||
..file.clone()
|
||||
seg: keep_from(&route.seg, index),
|
||||
..route.clone()
|
||||
};
|
||||
second_file
|
||||
.trk
|
||||
.extend(file.trk[trk + 1..].iter().map(copy_track));
|
||||
second_file
|
||||
.wpt
|
||||
.insert_at(0, file.wpt.iter().map(copy_waypoint).collect());
|
||||
|
||||
let ids = [first_file.id, second_file.id];
|
||||
let position = state.order.0.iter().position(|id| *id == file_id);
|
||||
let at = position.map_or(state.order.0.len(), |p| p + 1);
|
||||
state.order.0.splice(at..at, ids);
|
||||
state.files.insert(first_file.id, Rc::new(first_file));
|
||||
state.files.insert(second_file.id, Rc::new(second_file));
|
||||
*state.selection = Selection::File {
|
||||
file_ids: ids.into(),
|
||||
};
|
||||
return Ok(());
|
||||
if self.split_type == SplitType::Routes {
|
||||
file.rte.splice(rte..=rte, [first, second]);
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
return Ok(());
|
||||
}
|
||||
// the following routes, and the tracks, which come after the routes
|
||||
second_file.rte = [second]
|
||||
.into_iter()
|
||||
.chain(file.rte[rte + 1..].iter().map(copy_route))
|
||||
.collect();
|
||||
second_file.trk = file.trk.iter().map(copy_track).collect();
|
||||
file.rte.truncate(rte);
|
||||
file.rte.push(first);
|
||||
file.trk.clear();
|
||||
}
|
||||
SplitType::Files => {
|
||||
let (first_track, second_track) = split_track();
|
||||
let mut second_file = File {
|
||||
SegmentIndex::Track { trk, seg } => {
|
||||
let track = file.trk[trk].clone();
|
||||
let segment = &track.trkseg[seg];
|
||||
let (first, second) = (keep_up_to(segment, index), keep_from(segment, index));
|
||||
if self.split_type == SplitType::Segments {
|
||||
file.trk[trk].trkseg.splice(seg..=seg, [first, second]);
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
return Ok(());
|
||||
}
|
||||
// the track is cut: what is before the segment, and its first part, then the
|
||||
// second part and what is after it
|
||||
let mut first_track = track.clone();
|
||||
first_track.trkseg.truncate(seg);
|
||||
first_track.trkseg.push(first);
|
||||
let mut second_track = Track {
|
||||
id: Default::default(),
|
||||
trk: vec![second_track],
|
||||
wpt: Waypoints::default(),
|
||||
..file.clone()
|
||||
trkseg: vec![second],
|
||||
..track.clone()
|
||||
};
|
||||
second_file
|
||||
.trk
|
||||
.extend(file.trk[trk + 1..].iter().map(copy_track));
|
||||
second_file
|
||||
.wpt
|
||||
.insert_at(0, file.wpt.iter().map(copy_waypoint).collect());
|
||||
|
||||
second_track
|
||||
.trkseg
|
||||
.extend(track.trkseg[seg + 1..].iter().map(copy_segment));
|
||||
if self.split_type == SplitType::Tracks {
|
||||
file.trk.splice(trk..=trk, [first_track, second_track]);
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
return Ok(());
|
||||
}
|
||||
second_file.trk = [second_track]
|
||||
.into_iter()
|
||||
.chain(file.trk[trk + 1..].iter().map(copy_track))
|
||||
.collect();
|
||||
file.trk.truncate(trk);
|
||||
file.trk.push(first_track);
|
||||
let position = state.order.0.iter().position(|id| *id == file_id);
|
||||
state.order.0.insert(
|
||||
position.map_or(state.order.0.len(), |p| p + 1),
|
||||
second_file.id,
|
||||
);
|
||||
state.files.insert(second_file.id, Rc::new(second_file));
|
||||
}
|
||||
}
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
|
||||
// the file is cut: both parts have the waypoints
|
||||
second_file
|
||||
.wpt
|
||||
.insert_at(0, file.wpt.iter().map(copy_waypoint).collect());
|
||||
let position = state.order.0.iter().position(|id| *id == file_id);
|
||||
let at = position.map_or(state.order.0.len(), |p| p + 1);
|
||||
if self.keep_originals {
|
||||
// the parts are new files: nothing is shared by id with the file, nor between them
|
||||
let first_file = copy_file(&file);
|
||||
let ids = [first_file.id, second_file.id];
|
||||
state.order.0.splice(at..at, ids);
|
||||
state.files.insert(first_file.id, Rc::new(first_file));
|
||||
state.files.insert(second_file.id, Rc::new(second_file));
|
||||
*state.selection = Selection::File {
|
||||
file_ids: ids.into(),
|
||||
};
|
||||
} else {
|
||||
state.order.0.insert(at, second_file.id);
|
||||
state.files.insert(second_file.id, Rc::new(second_file));
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,8 @@ impl Apply for Time {
|
||||
let locations = state
|
||||
.selection
|
||||
.segment_locations(state.files, &state.order.0);
|
||||
let segments: Vec<&TrackSegment> = locations
|
||||
.iter()
|
||||
.map(|l| &state.files[&l.file_id].trk[l.trk].trkseg[l.seg])
|
||||
.collect();
|
||||
let segments: Vec<&TrackSegment> =
|
||||
locations.iter().map(|l| l.segment(state.files)).collect();
|
||||
if segments.iter().all(|segment| segment.is_empty()) {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user