diff --git a/gpx-rs/src/engine/command/edit/convert.rs b/gpx-rs/src/engine/command/edit/convert.rs index ffedb75e2..a954ff77f 100644 --- a/gpx-rs/src/engine/command/edit/convert.rs +++ b/gpx-rs/src/engine/command/edit/convert.rs @@ -9,10 +9,12 @@ use crate::{Apply, CommandError, FileId, Route, Selection, State, Track}; /// after the routes; /// - a track becomes one route per segment, as a route has a single segment, and goes away. A /// selected segment becomes a route and leaves its track, which stays even if it has no segment -/// left. The routes have the information of their track, and go after the other routes. +/// left. The routes have the information of their track, and go after the other routes; +/// - a file with routes only has all of them converted to tracks, and a file with tracks only +/// has all of them converted to routes. The files with both, or neither, stay as they are. /// -/// The segments keep their ids, the new routes and tracks are selected. There is nothing to do -/// for the other selections, or for tracks without segments. +/// The segments keep their ids, the new routes and tracks are selected (the files stay selected). +/// There is nothing to do for the other selections, or for tracks without segments. #[derive(Debug)] pub struct Convert; @@ -31,14 +33,7 @@ impl Apply for Convert { .into_iter() .partition::, _>(|rte| rte_ids.contains(&rte.id)); file.rte = kept; - let tracks: Vec = converted - .into_iter() - .map(|rte| Track { - id: Default::default(), - info: rte.info, - trkseg: vec![rte.seg], - }) - .collect(); + let tracks = to_tracks(converted); let trk_ids: HashSet<_> = tracks.iter().map(|trk| trk.id).collect(); file.trk.splice(0..0, tracks); Selection::Track { @@ -57,14 +52,7 @@ impl Apply for Convert { .into_iter() .partition::, _>(|trk| trk_ids.contains(&trk.id)); file.trk = kept; - let routes = converted.into_iter().flat_map(|trk| { - trk.trkseg.into_iter().map(move |seg| Route { - id: Default::default(), - info: trk.info.clone(), - seg, - }) - }); - add_routes(&mut file.rte, *file_id, routes) + add_routes(&mut file.rte, *file_id, to_routes(converted)) } Selection::TrackSegment { file_id, @@ -94,8 +82,32 @@ impl Apply for Convert { }); add_routes(&mut file.rte, *file_id, routes) } + Selection::File { file_ids } => { + let mut converted = false; + for file_id in file_ids { + let Some(file) = state.files.get_mut(file_id) else { + continue; + }; + if !file.rte.is_empty() && file.trk.is_empty() { + let file = Rc::make_mut(file); + file.trk = to_tracks(std::mem::take(&mut file.rte)); + } else if file.rte.is_empty() + && file.trk.iter().any(|trk| !trk.trkseg.is_empty()) + { + let file = Rc::make_mut(file); + file.rte = to_routes(std::mem::take(&mut file.trk)).collect(); + } else { + continue; + } + converted = true; + } + return if converted { + Ok(()) + } else { + Err(CommandError::NothingToDo) + }; + } Selection::Empty - | Selection::File { .. } | Selection::Waypoints { .. } | Selection::Waypoint { .. } => return Err(CommandError::NothingToDo), }; @@ -113,6 +125,29 @@ impl Apply for Convert { } } +/// The tracks of the routes, each with the segment of its route. +fn to_tracks(routes: Vec) -> Vec { + routes + .into_iter() + .map(|rte| Track { + id: Default::default(), + info: rte.info, + trkseg: vec![rte.seg], + }) + .collect() +} + +/// The routes of the tracks, one per segment, each with the information of its track. +fn to_routes(tracks: Vec) -> impl Iterator { + tracks.into_iter().flat_map(|trk| { + trk.trkseg.into_iter().map(move |seg| Route { + id: Default::default(), + info: trk.info.clone(), + seg, + }) + }) +} + /// Adds the routes after the other routes, and gives the selection of them. fn add_routes( rte: &mut Vec, diff --git a/gpx-rs/src/engine/command/routes_tests.rs b/gpx-rs/src/engine/command/routes_tests.rs index 0d6bf5dd9..b36a19737 100644 --- a/gpx-rs/src/engine/command/routes_tests.rs +++ b/gpx-rs/src/engine/command/routes_tests.rs @@ -682,3 +682,43 @@ fn test_convert_segments_to_routes_keeps_their_track() { ); } } + +#[test] +fn test_convert_a_file_with_routes_or_tracks_only() { + let (mut fx, id) = fixture(); + let before = file(&fx, id).clone(); + let only_tracks = File { + trk: before.trk.clone(), + ..File::default() + }; + let only_tracks_id = only_tracks.id; + fx.files.insert(only_tracks_id, Rc::new(only_tracks)); + fx.order.0.push(only_tracks_id); + + // the file with both stays as it is, the other gets one route per segment + fx.selection = Selection::File { + file_ids: [id, only_tracks_id].into(), + }; + Convert.apply(&mut fx.state()).unwrap(); + assert_eq!(lengths(file(&fx, id)), lengths(&before)); + let routes = file(&fx, only_tracks_id); + assert_eq!((routes.rte.len(), routes.trk.len()), (4, 0)); + assert_eq!(routes.rte[0].seg.id, before.trk[0].trkseg[0].id); + assert_eq!(routes.rte[0].info, before.trk[0].info); + + // and back: one track per route + fx.selection = Selection::File { + file_ids: [only_tracks_id].into(), + }; + Convert.apply(&mut fx.state()).unwrap(); + let tracks = file(&fx, only_tracks_id); + assert_eq!((tracks.rte.len(), tracks.trk.len()), (0, 4)); + assert_eq!(tracks.trk[0].trkseg[0].id, before.trk[0].trkseg[0].id); + + // nothing to do for a file with both + fx.selection = whole(id); + assert_eq!( + Convert.apply(&mut fx.state()), + Err(CommandError::NothingToDo) + ); +} diff --git a/website/src/lib/components/Menu.svelte b/website/src/lib/components/Menu.svelte index 00ede01e1..b9a4e1a56 100644 --- a/website/src/lib/components/Menu.svelte +++ b/website/src/lib/components/Menu.svelte @@ -78,7 +78,7 @@ } from '$lib/actions/file-actions'; import UnsavedDialog from '$lib/components/file-list/UnsavedDialog.svelte'; import { engine } from '$lib/engine'; - import { allHidden } from '$lib/engine/all-hidden'; + import { allHidden, selectedFilesConversion } from '$lib/engine/selection-stores'; import { selectedFileIds, selectionSize } from '$lib/engine/selection-helpers'; import { boundsManager } from '$lib/state/bounds'; import { onMount } from 'svelte'; @@ -334,6 +334,19 @@ {/if} {/if} + {#if $selectedFilesConversion} + {#if !$treeFileView} + + {/if} + engine.convert()}> + + {i18n._( + $selectedFilesConversion === 'route' + ? 'menu.convert_to_routes' + : 'menu.convert_to_tracks' + )} + + {/if} engine.selectAll()} disabled={noFiles}> diff --git a/website/src/lib/components/file-list/FileListNodeLabel.svelte b/website/src/lib/components/file-list/FileListNodeLabel.svelte index 692107144..f33e23044 100644 --- a/website/src/lib/components/file-list/FileListNodeLabel.svelte +++ b/website/src/lib/components/file-list/FileListNodeLabel.svelte @@ -29,7 +29,7 @@ import { editStyle } from '$lib/components/file-list/style/utils.svelte'; import { getSymbolKey, symbols } from '$lib/data/symbols'; import { engine, type FileState } from '$lib/engine'; - import { allHidden } from '$lib/engine/all-hidden'; + import { allHidden, selectedFilesConversion } from '$lib/engine/selection-stores'; import { normalizeColor } from '$lib/engine/file-colors'; import { isHidden } from '$lib/engine/file-visibility'; import { @@ -271,7 +271,6 @@ {i18n._('menu.new_route')} - {:else if node.type === 'track'} {/if} {/if} + {#if node.type === 'file'} + {#if $selectedFilesConversion} + engine.convert()}> + + {i18n._( + $selectedFilesConversion === 'route' + ? 'menu.convert_to_routes' + : 'menu.convert_to_tracks' + )} + + {/if} + {#if orientation === 'vertical' || $selectedFilesConversion} + + {/if} + {/if} engine.setSelectionHidden(!$allHidden)}> {#if $allHidden} diff --git a/website/src/lib/components/map/gpx-layer/distance-markers.ts b/website/src/lib/components/map/gpx-layer/distance-markers.ts index 75f72a32e..619e7574f 100644 --- a/website/src/lib/components/map/gpx-layer/distance-markers.ts +++ b/website/src/lib/components/map/gpx-layer/distance-markers.ts @@ -3,7 +3,7 @@ import { engine } from '$lib/engine'; import { getConvertedDistanceToKilometers } from '$lib/units/units'; import { get } from 'svelte/store'; import { map } from '$lib/components/map/map'; -import { allHidden } from '$lib/engine/all-hidden'; +import { allHidden } from '$lib/engine/selection-stores'; import type { GeoJSONSource } from 'maplibre-gl'; import { ANCHOR_LAYER_KEY } from '$lib/components/map/style'; diff --git a/website/src/lib/components/map/gpx-layer/start-end-markers.ts b/website/src/lib/components/map/gpx-layer/start-end-markers.ts index 7b16914ce..7700daf37 100644 --- a/website/src/lib/components/map/gpx-layer/start-end-markers.ts +++ b/website/src/lib/components/map/gpx-layer/start-end-markers.ts @@ -2,7 +2,7 @@ import { currentTool, Tool } from '$lib/components/toolbar/tools'; import type { GeoJSONSource } from 'maplibre-gl'; import { get } from 'svelte/store'; import { map } from '$lib/components/map/map'; -import { allHidden } from '$lib/engine/all-hidden'; +import { allHidden } from '$lib/engine/selection-stores'; import { ANCHOR_LAYER_KEY } from '$lib/components/map/style'; import { loadSVGIcon } from '$lib/utils'; import { hoveredPoint, slicedStatistics } from '$lib/state/selection-statistics'; diff --git a/website/src/lib/docs/en/files-and-stats.mdx b/website/src/lib/docs/en/files-and-stats.mdx index 00f85c821..a3f2be573 100644 --- a/website/src/lib/docs/en/files-and-stats.mdx +++ b/website/src/lib/docs/en/files-and-stats.mdx @@ -51,7 +51,7 @@ By right-clicking on a file tab, you can access the same actions as in the [edit As mentioned in the [view options section](./menu/view), you can switch to a tree layout for the files list. This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map. -In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](./gpx) contained inside the files through collapsible sections. +In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](./gpx) contained inside the files through collapsible sections. You can also apply [edit actions](./menu/edit) and [tools](./toolbar) to internal file items. Furthermore, you can drag and drop the inner items to reorder them, or move them in the hierarchy or even to another file. diff --git a/website/src/lib/docs/en/gpx.mdx b/website/src/lib/docs/en/gpx.mdx index 490265ed6..89707ab92 100644 --- a/website/src/lib/docs/en/gpx.mdx +++ b/website/src/lib/docs/en/gpx.mdx @@ -3,13 +3,14 @@ title: GPX file format --- # { title } The GPX file format is an open standard for exchanging GPS data between applications and GPS devices. It essentially consists of a series of GPS points encoding one or multiple GPS traces, and, optionally, some points of interest. +These traces can be stored either as tracks or as routes. GPX files may also contain metadata, of which the **name** and **description** fields are the most useful for users. @@ -27,6 +28,11 @@ Some devices also store additional information such as heart rate, cadence, temp In most cases, GPX files contain a single track with a single segment. However, the hierarchy described above allows for more advanced use cases, such as planning multi-day trips with several variants for each day. +### Routes + +A **route** is a single continuous path described by a sequence of **route points**, which are the places to go through, like the anchor points of the [routing tool](./toolbar/routing). +Like a track, it can have a **name**, a **description**, and **appearance properties**. + ### Points of interest **Points of interest** (technically called *waypoints*) represent locations of interest to show either on a GPS device or on a digital map. diff --git a/website/src/lib/docs/en/menu/edit.mdx b/website/src/lib/docs/en/menu/edit.mdx index f736d7b28..cc82f3766 100644 --- a/website/src/lib/docs/en/menu/edit.mdx +++ b/website/src/lib/docs/en/menu/edit.mdx @@ -3,14 +3,14 @@ title: Edit actions --- # { title } Unlike the file actions, the edit actions can potentially modify the content of the currently selected files. -Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, and points of interest](../gpx). +Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, routes, and points of interest](../gpx). Therefore, we will refer to the elements that can be modified by these actions as *file items*. Note that except for the undo and redo actions, the edit actions are also accessible through the context menu (right-click) of the file items. @@ -38,6 +38,17 @@ Additionally, the selection must be a single file. +### New route + +Create a new route in the selected file. + + + +This action is only available when the tree layout of the files list is enabled. +Additionally, the selection must be a single file. + + + ### New segment Create a new segment in the selected track. @@ -49,6 +60,19 @@ Additionally, the selection must be a single track. +### Convert to route/track + +Convert the selected routes to tracks, or the selected tracks or segments to routes. +Since a route has a single segment, a track with multiple segments is converted into one route per segment. + +When the selected files contain only routes, or only tracks, you can also convert all of them at once. + + + +Except for whole files, this action is only available when the tree layout of the files list is enabled. + + + ### Select all Add all file items in the current hierarchy level to the selection. diff --git a/website/src/lib/docs/en/menu/file.mdx b/website/src/lib/docs/en/menu/file.mdx index 04fae8ecb..8d5529b51 100644 --- a/website/src/lib/docs/en/menu/file.mdx +++ b/website/src/lib/docs/en/menu/file.mdx @@ -15,6 +15,10 @@ The file actions menu contains a set of pretty self-explanatory file operations. Create a new empty file. +### New route file + +Create a new file containing an empty [route](../gpx). + ### Open... Open files from your computer. diff --git a/website/src/lib/docs/en/menu/view.mdx b/website/src/lib/docs/en/menu/view.mdx index a28bc622d..bd7240c9d 100644 --- a/website/src/lib/docs/en/menu/view.mdx +++ b/website/src/lib/docs/en/menu/view.mdx @@ -19,7 +19,7 @@ Hide the elevation profile to make room for the map, or show it to inspect the c Toggle the tree layout for the [file list](../files-and-stats). This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map. -In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](../gpx) contained inside the files through collapsible sections. +In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](../gpx) contained inside the files through collapsible sections. ### Switch to previous basemap diff --git a/website/src/lib/docs/en/toolbar/extract.mdx b/website/src/lib/docs/en/toolbar/extract.mdx index f5c837159..abdcd85dd 100644 --- a/website/src/lib/docs/en/toolbar/extract.mdx +++ b/website/src/lib/docs/en/toolbar/extract.mdx @@ -16,7 +16,7 @@ This tool allows you to extract [tracks (or segments)](../gpx) from files (or tr -Applying the tool to a file containing multiple tracks will create a new file for each of the tracks it contains. +Applying the tool to a file containing multiple [tracks or routes](../gpx) will create a new file for each of the tracks and routes it contains. Similarly, applying the tool to a track containing multiple segments will create (in the same file) a new track for each of the segments it contains. diff --git a/website/src/lib/docs/en/toolbar/merge.mdx b/website/src/lib/docs/en/toolbar/merge.mdx index 272f2dd3c..172822de4 100644 --- a/website/src/lib/docs/en/toolbar/merge.mdx +++ b/website/src/lib/docs/en/toolbar/merge.mdx @@ -10,11 +10,11 @@ title: Merge # { title } -To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, or segments](../gpx). +To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, segments or routes](../gpx). - If your goal is to create a single continuous trace from your selection, use the **Connect the traces** option and validate. -- The second option can be used to create or manage files with multiple [tracks or segments](../gpx). -Merging files (or tracks) will result in a single file (or track) containing all tracks (or segments) from the selection. +- The second option can be used to create or manage files with multiple [tracks, segments or routes](../gpx). +Merging files (or tracks) will result in a single file (or track) containing all tracks and routes (or segments) from the selection. diff --git a/website/src/lib/docs/en/toolbar/scissors.mdx b/website/src/lib/docs/en/toolbar/scissors.mdx index b54aa7775..d37d4b39f 100644 --- a/website/src/lib/docs/en/toolbar/scissors.mdx +++ b/website/src/lib/docs/en/toolbar/scissors.mdx @@ -27,6 +27,6 @@ To split the selected trace into two parts, click on one of the split markers di To split at a specific point of your choice, hover over the trace on the map. Scissors will appear at the cursor position, showing that you can split the trace at that point. -You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks or segments](../gpx). +You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks, segments or routes](../gpx). diff --git a/website/src/lib/engine/all-hidden.ts b/website/src/lib/engine/all-hidden.ts deleted file mode 100644 index 2a0ecb373..000000000 --- a/website/src/lib/engine/all-hidden.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { readable } from 'svelte/store'; -import { engine, FileStateCollectionObserver, type FileState, type Selection } from '$lib/engine'; -import { isSelectionHidden } from '$lib/engine/selection-helpers'; - -/** - * Whether all the selected elements are hidden. It follows the selection and, through the - * observer of the files, the state of every file (hiding, editing). - */ -export const allHidden = readable(false, (set) => { - let selection: Selection = { type: 'empty' }; - const states = new Map(); - const unsubscribes = new Map void>(); - - const update = () => set(isSelectionHidden(selection, states)); - - const unsubscribeSelection = engine.selection.subscribe(($selection) => { - selection = $selection; - update(); - }); - const observer = new FileStateCollectionObserver( - (files) => { - files.forEach((store, fileId) => { - unsubscribes.set( - fileId, - store.subscribe(($file) => { - states.set(fileId, $file); - update(); - }) - ); - }); - }, - (fileId) => { - unsubscribes.get(fileId)?.(); - unsubscribes.delete(fileId); - states.delete(fileId); - update(); - }, - () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - unsubscribes.clear(); - states.clear(); - } - ); - - return () => { - unsubscribeSelection(); - observer.destroy(); - }; -}); diff --git a/website/src/lib/engine/engine.ts b/website/src/lib/engine/engine.ts index 356a7fb95..d127ee560 100644 --- a/website/src/lib/engine/engine.ts +++ b/website/src/lib/engine/engine.ts @@ -374,7 +374,8 @@ class Engine { /** * Converts the selected routes to tracks, or the selected tracks or segments to routes (one - * per segment), which are selected. Resolves to `false` when there is nothing to convert. + * per segment), which are selected. Selected files with routes only (or tracks only) have all + * of them converted. Resolves to `false` when there is nothing to convert. */ convert() { return this.run((b) => b.convert()); diff --git a/website/src/lib/engine/selection-helpers.test.ts b/website/src/lib/engine/selection-helpers.test.ts index f5e8a1e72..7ca665ee7 100644 --- a/website/src/lib/engine/selection-helpers.test.ts +++ b/website/src/lib/engine/selection-helpers.test.ts @@ -5,6 +5,7 @@ import type { VisibilityState } from '$lib/engine/file-visibility'; import { coversSegments, elementId, + fileConversion, hasSelectionWithin, isCovered, isInClipboard, @@ -274,4 +275,41 @@ describe('selection helpers', () => { assert.equal(isSelectionHidden(files('F1', 'F2'), states('F1')), false); }); }); + + describe('fileConversion', () => { + const segment = (id: string) => ({ id, rev: id, length: 2 }); + const structure = (routes: string[], tracks: string[]): { structure: FileStructure } => ({ + structure: { + id: 'F', + name: 'file', + waypointsRev: 'rev', + waypoints: [], + routes: routes.map((id) => ({ id, segment: segment(`${id}-S`) })), + tracks: tracks.map((id) => ({ id, segments: [segment(`${id}-S`)] })), + }, + }); + const states = new Map([ + ['routes', structure(['R1', 'R2'], [])], + ['tracks', structure([], ['T1'])], + ['both', structure(['R1'], ['T1'])], + ['none', structure([], [])], + ]); + + it('converts files with tracks only to routes, and routes only to tracks', () => { + assert.equal(fileConversion(files('tracks'), states), 'route'); + assert.equal(fileConversion(files('routes'), states), 'track'); + }); + + it('does not convert files with both, neither, or a mix of the two', () => { + assert.equal(fileConversion(files('both'), states), undefined); + assert.equal(fileConversion(files('none'), states), undefined); + assert.equal(fileConversion(files('routes', 'tracks'), states), undefined); + assert.equal(fileConversion(files('unknown'), states), undefined); + }); + + it('only converts files', () => { + assert.equal(fileConversion(empty, states), undefined); + assert.equal(fileConversion(tracks('T1'), states), undefined); + }); + }); }); diff --git a/website/src/lib/engine/selection-helpers.ts b/website/src/lib/engine/selection-helpers.ts index ac728963d..285e262ba 100644 --- a/website/src/lib/engine/selection-helpers.ts +++ b/website/src/lib/engine/selection-helpers.ts @@ -247,3 +247,34 @@ export function isSelectionHidden( }) ); } + +/** + * What the selected files convert to: `'route'` when they all have tracks only (with segments), + * `'track'` when they all have routes only, `undefined` otherwise (other selections, files with + * both or neither, or a mix of the two). `states` holds the files by id. + */ +export function fileConversion( + selection: Selection, + states: ReadonlyMap +): 'route' | 'track' | undefined { + if (selection.type !== 'file' || selection.fileIds.length === 0) { + return undefined; + } + const targets = new Set( + selection.fileIds.map((fileId) => { + const structure = states.get(fileId)?.structure; + if (structure === undefined) { + return undefined; + } else if (structure.routes.length > 0 && structure.tracks.length === 0) { + return 'track' as const; + } else if ( + structure.routes.length === 0 && + structure.tracks.some((track) => track.segments.length > 0) + ) { + return 'route' as const; + } + return undefined; + }) + ); + return targets.size === 1 ? targets.values().next().value : undefined; +} diff --git a/website/src/lib/engine/selection-stores.ts b/website/src/lib/engine/selection-stores.ts new file mode 100644 index 000000000..3c6b319b2 --- /dev/null +++ b/website/src/lib/engine/selection-stores.ts @@ -0,0 +1,63 @@ +import { readable, type Readable } from 'svelte/store'; +import { engine, FileStateCollectionObserver, type FileState, type Selection } from '$lib/engine'; +import { fileConversion, isSelectionHidden } from '$lib/engine/selection-helpers'; + +/** + * A value computed from the selection and the states of the files. It follows the selection and, + * through the observer of the files, the state of every file (hiding, editing). + */ +function fromSelectionAndFiles( + initial: T, + compute: (selection: Selection, states: ReadonlyMap) => T +): Readable { + return readable(initial, (set) => { + let selection: Selection = { type: 'empty' }; + const states = new Map(); + const unsubscribes = new Map void>(); + + const update = () => set(compute(selection, states)); + + const unsubscribeSelection = engine.selection.subscribe(($selection) => { + selection = $selection; + update(); + }); + const observer = new FileStateCollectionObserver( + (files) => { + files.forEach((store, fileId) => { + unsubscribes.set( + fileId, + store.subscribe(($file) => { + states.set(fileId, $file); + update(); + }) + ); + }); + }, + (fileId) => { + unsubscribes.get(fileId)?.(); + unsubscribes.delete(fileId); + states.delete(fileId); + update(); + }, + () => { + unsubscribes.forEach((unsubscribe) => unsubscribe()); + unsubscribes.clear(); + states.clear(); + } + ); + + return () => { + unsubscribeSelection(); + observer.destroy(); + }; + }); +} + +/** Whether all the selected elements are hidden. */ +export const allHidden = fromSelectionAndFiles(false, isSelectionHidden); + +/** What the selected files convert to, see `fileConversion`. */ +export const selectedFilesConversion = fromSelectionAndFiles<'route' | 'track' | undefined>( + undefined, + fileConversion +); diff --git a/website/src/locales/en.json b/website/src/locales/en.json index df937cace..0e6849f10 100644 --- a/website/src/locales/en.json +++ b/website/src/locales/en.json @@ -14,6 +14,8 @@ "new_route": "New route", "convert_to_route": "Convert to route", "convert_to_track": "Convert to track", + "convert_to_routes": "Convert to route file", + "convert_to_tracks": "Convert to track file", "new_segment": "New segment", "open": "Open...", "open_folder": "Open folder...",