* Change icon to Maximize

* Use default animation
* Rename feature to "Fly to"
* Move logic to stores so that it can be reused in the main menu (and eventually a keyboard shortcut)
This commit is contained in:
mbof 2024-08-23 07:18:36 -07:00
parent 6559fdd125
commit c6919f518a
4 changed files with 843 additions and 783 deletions

View file

@ -41,7 +41,8 @@
FileStack, FileStack,
FileX, FileX,
BookOpenText, BookOpenText,
ChartArea ChartArea,
Maximize
} from 'lucide-svelte'; } from 'lucide-svelte';
import { import {
@ -54,7 +55,9 @@
editMetadata, editMetadata,
editStyle, editStyle,
exportState, exportState,
ExportState ExportState,
flyToBounds,
gpxStatistics
} from '$lib/stores'; } from '$lib/stores';
import { import {
copied, copied,
@ -222,6 +225,13 @@
<PaintBucket size="16" class="mr-1" /> <PaintBucket size="16" class="mr-1" />
{$_('menu.style.button')} {$_('menu.style.button')}
</Menubar.Item> </Menubar.Item>
<Menubar.Item
disabled={$selection.size === 0}
on:click={() => flyToBounds($gpxStatistics.global.bounds, $map)}
>
<Maximize size="16" class="mr-1" />
{$_('menu.fly_to_selection')}
</Menubar.Item>
<Menubar.Item <Menubar.Item
on:click={() => { on:click={() => {
if ($allHidden) { if ($allHidden) {

View file

@ -15,7 +15,7 @@
EyeOff, EyeOff,
ClipboardCopy, ClipboardCopy,
ClipboardPaste, ClipboardPaste,
Maximize2, Maximize,
Scissors, Scissors,
FileStack, FileStack,
FileX FileX
@ -45,9 +45,10 @@
editMetadata, editMetadata,
editStyle, editStyle,
embedding, embedding,
flyToBounds,
gpxLayers, gpxLayers,
map, gpxStatistics,
updateTargetMapBounds map
} from '$lib/stores'; } from '$lib/stores';
import { import {
GPXTreeElement, GPXTreeElement,
@ -226,30 +227,14 @@
{$_('menu.style.button')} {$_('menu.style.button')}
</ContextMenu.Item> </ContextMenu.Item>
{/if} {/if}
{#if node instanceof GPXTreeElement} <ContextMenu.Item
<ContextMenu.Item on:click={() => {
on:click={() => { flyToBounds($gpxStatistics.global.bounds, $map);
const targetBounds = node.getStatistics().global.bounds; }}
const mapBoxBounds = new mapboxgl.LngLatBounds([ >
[targetBounds.northEast.lon, targetBounds.northEast.lat], <Maximize size="16" class="mr-1" />
[targetBounds.southWest.lon, targetBounds.southWest.lat] {$_('menu.fly_to')}
]); </ContextMenu.Item>
$map?.fitBounds(mapBoxBounds, {
padding: 80,
linear: true,
duration: 1000,
easing: (t) => {
return t < 0.5
? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2
: (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2;
}
});
}}
>
<Maximize2 size="16" class="mr-1" />
{$_('menu.fit_map')}
</ContextMenu.Item>
{/if}
<ContextMenu.Item <ContextMenu.Item
on:click={() => { on:click={() => {
if ($allHidden) { if ($allHidden) {

View file

@ -6,8 +6,21 @@ import { tick } from 'svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import type { GPXLayer } from '$lib/components/gpx-layer/GPXLayer'; import type { GPXLayer } from '$lib/components/gpx-layer/GPXLayer';
import { dbUtils, fileObservers, getFile, getStatistics, settings } from './db'; import { dbUtils, fileObservers, getFile, getStatistics, settings } from './db';
import { addSelectItem, applyToOrderedSelectedItemsFromFile, selectFile, selectItem, selection } from '$lib/components/file-list/Selection'; import {
import { ListFileItem, ListItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem } from '$lib/components/file-list/FileList'; addSelectItem,
applyToOrderedSelectedItemsFromFile,
selectFile,
selectItem,
selection
} from '$lib/components/file-list/Selection';
import {
ListFileItem,
ListItem,
ListTrackItem,
ListTrackSegmentItem,
ListWaypointItem,
ListWaypointsItem
} from '$lib/components/file-list/FileList';
import type { RoutingControls } from '$lib/components/toolbar/tools/routing/RoutingControls'; import type { RoutingControls } from '$lib/components/toolbar/tools/routing/RoutingControls';
import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte'; import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
@ -18,369 +31,420 @@ export const embedding = writable(false);
export const selectFiles = writable<{ [key: string]: (fileId?: string) => void }>({}); export const selectFiles = writable<{ [key: string]: (fileId?: string) => void }>({});
export const gpxStatistics: Writable<GPXStatistics> = writable(new GPXStatistics()); export const gpxStatistics: Writable<GPXStatistics> = writable(new GPXStatistics());
export const slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined> = writable(undefined); export const slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined> =
writable(undefined);
export function updateGPXData() { export function updateGPXData() {
let statistics = new GPXStatistics(); let statistics = new GPXStatistics();
applyToOrderedSelectedItemsFromFile((fileId, level, items) => { applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let stats = getStatistics(fileId); let stats = getStatistics(fileId);
if (stats) { if (stats) {
let first = true; let first = true;
items.forEach((item) => { items.forEach((item) => {
if (!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) || first) { if (!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) || first) {
statistics.mergeWith(stats.getStatisticsFor(item)); statistics.mergeWith(stats.getStatisticsFor(item));
first = false; first = false;
} }
}); });
} }
}, false); }, false);
gpxStatistics.set(statistics); gpxStatistics.set(statistics);
} }
let unsubscribes: Map<string, () => void> = new Map(); let unsubscribes: Map<string, () => void> = new Map();
selection.subscribe(($selection) => { // Maintain up-to-date statistics for the current selection selection.subscribe(($selection) => {
updateGPXData(); // Maintain up-to-date statistics for the current selection
updateGPXData();
while (unsubscribes.size > 0) { while (unsubscribes.size > 0) {
let [fileId, unsubscribe] = unsubscribes.entries().next().value; let [fileId, unsubscribe] = unsubscribes.entries().next().value;
unsubscribe(); unsubscribe();
unsubscribes.delete(fileId); unsubscribes.delete(fileId);
} }
$selection.forEach((item) => { $selection.forEach((item) => {
let fileId = item.getFileId(); let fileId = item.getFileId();
if (!unsubscribes.has(fileId)) { if (!unsubscribes.has(fileId)) {
let fileObserver = get(fileObservers).get(fileId); let fileObserver = get(fileObservers).get(fileId);
if (fileObserver) { if (fileObserver) {
let first = true; let first = true;
unsubscribes.set(fileId, fileObserver.subscribe(() => { unsubscribes.set(
if (first) first = false; fileId,
else updateGPXData(); fileObserver.subscribe(() => {
})); if (first) first = false;
} else updateGPXData();
} })
}); );
}
}
});
}); });
gpxStatistics.subscribe(() => { gpxStatistics.subscribe(() => {
slicedGPXStatistics.set(undefined); slicedGPXStatistics.set(undefined);
}); });
const targetMapBounds = writable({ const targetMapBounds = writable({
bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]), bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]),
count: 0, count: 0,
total: -1 total: -1
}); });
derived([targetMapBounds, map], x => x).subscribe(([bounds, $map]) => { derived([targetMapBounds, map], (x) => x).subscribe(([bounds, $map]) => {
if ($map === null || bounds.count !== bounds.total || (bounds.bounds.getSouth() === 90 && bounds.bounds.getWest() === 180 && bounds.bounds.getNorth() === -90 && bounds.bounds.getEast() === -180)) { if (
return; $map === null ||
} bounds.count !== bounds.total ||
(bounds.bounds.getSouth() === 90 &&
bounds.bounds.getWest() === 180 &&
bounds.bounds.getNorth() === -90 &&
bounds.bounds.getEast() === -180)
) {
return;
}
let currentBounds = $map.getBounds(); let currentBounds = $map.getBounds();
if (bounds.count !== get(fileObservers).size && currentBounds) { if (bounds.count !== get(fileObservers).size && currentBounds) {
// There are other files on the map // There are other files on the map
if (currentBounds.contains(bounds.bounds.getSouthEast()) && currentBounds.contains(bounds.bounds.getNorthWest())) { if (
return; currentBounds.contains(bounds.bounds.getSouthEast()) &&
} currentBounds.contains(bounds.bounds.getNorthWest())
) {
return;
}
bounds.bounds.extend(currentBounds.getSouthWest()); bounds.bounds.extend(currentBounds.getSouthWest());
bounds.bounds.extend(currentBounds.getNorthEast()); bounds.bounds.extend(currentBounds.getNorthEast());
} }
$map.fitBounds(bounds.bounds, { $map.fitBounds(bounds.bounds, { padding: 80, linear: true, easing: () => 1 });
padding: 80,
linear: true,
easing: () => 1
});
}); });
export function initTargetMapBounds(total: number) { export function initTargetMapBounds(total: number) {
targetMapBounds.set({ targetMapBounds.set({
bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]), bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]),
count: 0, count: 0,
total: total total: total
}); });
} }
export function updateTargetMapBounds(bounds: { export function updateTargetMapBounds(bounds: { southWest: Coordinates; northEast: Coordinates }) {
southWest: Coordinates, if (
northEast: Coordinates bounds.southWest.lat == 90 &&
}) { bounds.southWest.lon == 180 &&
if (bounds.southWest.lat == 90 && bounds.southWest.lon == 180 && bounds.northEast.lat == -90 && bounds.northEast.lon == -180) { // Avoid update for empty (new) files bounds.northEast.lat == -90 &&
targetMapBounds.update((target) => { bounds.northEast.lon == -180
target.count += 1; ) {
return target; // Avoid update for empty (new) files
}); targetMapBounds.update((target) => {
return; target.count += 1;
} return target;
});
return;
}
targetMapBounds.update((target) => { targetMapBounds.update((target) => {
target.bounds.extend(bounds.southWest); target.bounds.extend(bounds.southWest);
target.bounds.extend(bounds.northEast); target.bounds.extend(bounds.northEast);
target.count += 1; target.count += 1;
return target; return target;
}); });
}
export function flyToBounds(
bounds: { southWest: Coordinates; northEast: Coordinates },
map: mapboxgl.Map | null
) {
const mapBoxBounds = new mapboxgl.LngLatBounds([
[bounds.northEast.lon, bounds.northEast.lat],
[bounds.southWest.lon, bounds.southWest.lat]
]);
map?.fitBounds(mapBoxBounds, {
padding: 80
});
} }
export const gpxLayers: Map<string, GPXLayer> = new Map(); export const gpxLayers: Map<string, GPXLayer> = new Map();
export const routingControls: Map<string, RoutingControls> = new Map(); export const routingControls: Map<string, RoutingControls> = new Map();
export enum Tool { export enum Tool {
ROUTING, ROUTING,
WAYPOINT, WAYPOINT,
SCISSORS, SCISSORS,
TIME, TIME,
MERGE, MERGE,
EXTRACT, EXTRACT,
REDUCE, REDUCE,
CLEAN CLEAN
} }
export const currentTool = writable<Tool | null>(null); export const currentTool = writable<Tool | null>(null);
export const splitAs = writable(SplitType.FILES); export const splitAs = writable(SplitType.FILES);
export const streetViewEnabled = writable(false); export const streetViewEnabled = writable(false);
export function newGPXFile() { export function newGPXFile() {
const newFileName = get(_)("menu.new_file"); const newFileName = get(_)('menu.new_file');
let file = new GPXFile(); let file = new GPXFile();
let maxNewFileNumber = 0; let maxNewFileNumber = 0;
get(fileObservers).forEach((f) => { get(fileObservers).forEach((f) => {
let file = get(f)?.file; let file = get(f)?.file;
if (file && file.metadata.name && file.metadata.name.startsWith(newFileName)) { if (file && file.metadata.name && file.metadata.name.startsWith(newFileName)) {
let number = parseInt(file.metadata.name.split(' ').pop() ?? '0'); let number = parseInt(file.metadata.name.split(' ').pop() ?? '0');
if (!isNaN(number) && number > maxNewFileNumber) { if (!isNaN(number) && number > maxNewFileNumber) {
maxNewFileNumber = number; maxNewFileNumber = number;
} }
} }
}); });
file.metadata.name = `${newFileName} ${maxNewFileNumber + 1}`; file.metadata.name = `${newFileName} ${maxNewFileNumber + 1}`;
return file; return file;
} }
export function createFile() { export function createFile() {
let file = newGPXFile(); let file = newGPXFile();
dbUtils.add(file); dbUtils.add(file);
selectFileWhenLoaded(file._data.id); selectFileWhenLoaded(file._data.id);
currentTool.set(Tool.ROUTING); currentTool.set(Tool.ROUTING);
} }
export function triggerFileInput() { export function triggerFileInput() {
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'file'; input.type = 'file';
input.accept = '.gpx'; input.accept = '.gpx';
input.multiple = true; input.multiple = true;
input.className = 'hidden'; input.className = 'hidden';
input.onchange = () => { input.onchange = () => {
if (input.files) { if (input.files) {
loadFiles(input.files); loadFiles(input.files);
} }
}; };
input.click(); input.click();
} }
export async function loadFiles(list: FileList | File[]) { export async function loadFiles(list: FileList | File[]) {
let files = []; let files = [];
for (let i = 0; i < list.length; i++) { for (let i = 0; i < list.length; i++) {
let file = await loadFile(list[i]); let file = await loadFile(list[i]);
if (file) { if (file) {
files.push(file); files.push(file);
} }
} }
initTargetMapBounds(list.length); initTargetMapBounds(list.length);
dbUtils.addMultiple(files); dbUtils.addMultiple(files);
selectFileWhenLoaded(files[0]._data.id); selectFileWhenLoaded(files[0]._data.id);
} }
export async function loadFile(file: File): Promise<GPXFile | null> { export async function loadFile(file: File): Promise<GPXFile | null> {
let result = await new Promise<GPXFile | null>((resolve) => { let result = await new Promise<GPXFile | null>((resolve) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
let data = reader.result?.toString() ?? null; let data = reader.result?.toString() ?? null;
if (data) { if (data) {
let gpx = parseGPX(data); let gpx = parseGPX(data);
if (gpx.metadata === undefined) { if (gpx.metadata === undefined) {
gpx.metadata = { name: file.name.split('.').slice(0, -1).join('.') }; gpx.metadata = { name: file.name.split('.').slice(0, -1).join('.') };
} else if (gpx.metadata.name === undefined) { } else if (gpx.metadata.name === undefined) {
gpx.metadata['name'] = file.name.split('.').slice(0, -1).join('.'); gpx.metadata['name'] = file.name.split('.').slice(0, -1).join('.');
} }
resolve(gpx); resolve(gpx);
} else { } else {
resolve(null); resolve(null);
} }
}; };
reader.readAsText(file); reader.readAsText(file);
}); });
return result; return result;
} }
export function selectFileWhenLoaded(fileId: string) { export function selectFileWhenLoaded(fileId: string) {
const unsubscribe = fileObservers.subscribe((files) => { const unsubscribe = fileObservers.subscribe((files) => {
if (files.has(fileId)) { if (files.has(fileId)) {
tick().then(() => { tick().then(() => {
selectFile(fileId); selectFile(fileId);
}); });
unsubscribe(); unsubscribe();
} }
}); });
} }
export function updateSelectionFromKey(down: boolean, shift: boolean) { export function updateSelectionFromKey(down: boolean, shift: boolean) {
let selected = get(selection).getSelected(); let selected = get(selection).getSelected();
if (selected.length === 0) { if (selected.length === 0) {
return; return;
} }
let next: ListItem | undefined = undefined; let next: ListItem | undefined = undefined;
if (selected[0] instanceof ListFileItem) { if (selected[0] instanceof ListFileItem) {
let order = get(fileOrder); let order = get(fileOrder);
let limitIndex: number | undefined = undefined; let limitIndex: number | undefined = undefined;
selected.forEach((item) => { selected.forEach((item) => {
let index = order.indexOf(item.getFileId()); let index = order.indexOf(item.getFileId());
if (limitIndex === undefined || (down && index > limitIndex) || (!down && index < limitIndex)) { if (
limitIndex = index; limitIndex === undefined ||
} (down && index > limitIndex) ||
}); (!down && index < limitIndex)
) {
limitIndex = index;
}
});
if (limitIndex !== undefined) { if (limitIndex !== undefined) {
let nextIndex = down ? limitIndex + 1 : limitIndex - 1; let nextIndex = down ? limitIndex + 1 : limitIndex - 1;
while (true) { while (true) {
if (nextIndex < 0) { if (nextIndex < 0) {
nextIndex = order.length - 1; nextIndex = order.length - 1;
} else if (nextIndex >= order.length) { } else if (nextIndex >= order.length) {
nextIndex = 0; nextIndex = 0;
} }
if (nextIndex === limitIndex) { if (nextIndex === limitIndex) {
break; break;
} }
next = new ListFileItem(order[nextIndex]); next = new ListFileItem(order[nextIndex]);
if (!get(selection).has(next)) { if (!get(selection).has(next)) {
break; break;
} }
nextIndex += down ? 1 : -1; nextIndex += down ? 1 : -1;
} }
} }
} else if (selected[0] instanceof ListTrackItem && selected[selected.length - 1] instanceof ListTrackItem) { } else if (
let fileId = selected[0].getFileId(); selected[0] instanceof ListTrackItem &&
let file = getFile(fileId); selected[selected.length - 1] instanceof ListTrackItem
if (file) { ) {
let numberOfTracks = file.trk.length; let fileId = selected[0].getFileId();
let trackIndex = down ? selected[selected.length - 1].getTrackIndex() : selected[0].getTrackIndex(); let file = getFile(fileId);
if (down && trackIndex < numberOfTracks - 1) { if (file) {
next = new ListTrackItem(fileId, trackIndex + 1); let numberOfTracks = file.trk.length;
} else if (!down && trackIndex > 0) { let trackIndex = down
next = new ListTrackItem(fileId, trackIndex - 1); ? selected[selected.length - 1].getTrackIndex()
} : selected[0].getTrackIndex();
} if (down && trackIndex < numberOfTracks - 1) {
} else if (selected[0] instanceof ListTrackSegmentItem && selected[selected.length - 1] instanceof ListTrackSegmentItem) { next = new ListTrackItem(fileId, trackIndex + 1);
let fileId = selected[0].getFileId(); } else if (!down && trackIndex > 0) {
let file = getFile(fileId); next = new ListTrackItem(fileId, trackIndex - 1);
if (file) { }
let trackIndex = selected[0].getTrackIndex(); }
let numberOfSegments = file.trk[trackIndex].trkseg.length; } else if (
let segmentIndex = down ? selected[selected.length - 1].getSegmentIndex() : selected[0].getSegmentIndex(); selected[0] instanceof ListTrackSegmentItem &&
if (down && segmentIndex < numberOfSegments - 1) { selected[selected.length - 1] instanceof ListTrackSegmentItem
next = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex + 1); ) {
} else if (!down && segmentIndex > 0) { let fileId = selected[0].getFileId();
next = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex - 1); let file = getFile(fileId);
} if (file) {
} let trackIndex = selected[0].getTrackIndex();
} else if (selected[0] instanceof ListWaypointItem && selected[selected.length - 1] instanceof ListWaypointItem) { let numberOfSegments = file.trk[trackIndex].trkseg.length;
let fileId = selected[0].getFileId(); let segmentIndex = down
let file = getFile(fileId); ? selected[selected.length - 1].getSegmentIndex()
if (file) { : selected[0].getSegmentIndex();
let numberOfWaypoints = file.wpt.length; if (down && segmentIndex < numberOfSegments - 1) {
let waypointIndex = down ? selected[selected.length - 1].getWaypointIndex() : selected[0].getWaypointIndex(); next = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex + 1);
if (down && waypointIndex < numberOfWaypoints - 1) { } else if (!down && segmentIndex > 0) {
next = new ListWaypointItem(fileId, waypointIndex + 1); next = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex - 1);
} else if (!down && waypointIndex > 0) { }
next = new ListWaypointItem(fileId, waypointIndex - 1); }
} } else if (
} selected[0] instanceof ListWaypointItem &&
} selected[selected.length - 1] instanceof ListWaypointItem
) {
let fileId = selected[0].getFileId();
let file = getFile(fileId);
if (file) {
let numberOfWaypoints = file.wpt.length;
let waypointIndex = down
? selected[selected.length - 1].getWaypointIndex()
: selected[0].getWaypointIndex();
if (down && waypointIndex < numberOfWaypoints - 1) {
next = new ListWaypointItem(fileId, waypointIndex + 1);
} else if (!down && waypointIndex > 0) {
next = new ListWaypointItem(fileId, waypointIndex - 1);
}
}
}
if (next && (!get(selection).has(next) || !shift)) { if (next && (!get(selection).has(next) || !shift)) {
if (shift) { if (shift) {
addSelectItem(next); addSelectItem(next);
} else { } else {
selectItem(next); selectItem(next);
} }
} }
} }
async function exportFiles(fileIds: string[], exclude: string[]) { async function exportFiles(fileIds: string[], exclude: string[]) {
for (let fileId of fileIds) { for (let fileId of fileIds) {
let file = getFile(fileId); let file = getFile(fileId);
if (file) { if (file) {
exportFile(file, exclude); exportFile(file, exclude);
await new Promise(resolve => setTimeout(resolve, 200)); await new Promise((resolve) => setTimeout(resolve, 200));
} }
} }
} }
export function exportSelectedFiles(exclude: string[]) { export function exportSelectedFiles(exclude: string[]) {
let fileIds: string[] = []; let fileIds: string[] = [];
applyToOrderedSelectedItemsFromFile(async (fileId, level, items) => { applyToOrderedSelectedItemsFromFile(async (fileId, level, items) => {
fileIds.push(fileId); fileIds.push(fileId);
}); });
exportFiles(fileIds, exclude); exportFiles(fileIds, exclude);
} }
export function exportAllFiles(exclude: string[]) { export function exportAllFiles(exclude: string[]) {
exportFiles(get(fileOrder), exclude); exportFiles(get(fileOrder), exclude);
} }
export function exportFile(file: GPXFile, exclude: string[]) { export function exportFile(file: GPXFile, exclude: string[]) {
let blob = new Blob([buildGPX(file, exclude)], { type: 'application/gpx+xml' }); let blob = new Blob([buildGPX(file, exclude)], { type: 'application/gpx+xml' });
let url = URL.createObjectURL(blob); let url = URL.createObjectURL(blob);
let a = document.createElement('a'); let a = document.createElement('a');
a.href = url; a.href = url;
a.download = file.metadata.name + '.gpx'; a.download = file.metadata.name + '.gpx';
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
export const allHidden = writable(false); export const allHidden = writable(false);
export function updateAllHidden() { export function updateAllHidden() {
let hidden = true; let hidden = true;
applyToOrderedSelectedItemsFromFile((fileId, level, items) => { applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let file = getFile(fileId); let file = getFile(fileId);
if (file) { if (file) {
for (let item of items) { for (let item of items) {
if (!hidden) { if (!hidden) {
return; return;
} }
if (item instanceof ListFileItem) { if (item instanceof ListFileItem) {
hidden = hidden && (file._data.hidden === true); hidden = hidden && file._data.hidden === true;
} else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) { } else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
hidden = hidden && (file.trk[item.getTrackIndex()]._data.hidden === true); hidden = hidden && file.trk[item.getTrackIndex()]._data.hidden === true;
} else if (item instanceof ListTrackSegmentItem && item.getTrackIndex() < file.trk.length && item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length) { } else if (
hidden = hidden && (file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()]._data.hidden === true); item instanceof ListTrackSegmentItem &&
} else if (item instanceof ListWaypointsItem) { item.getTrackIndex() < file.trk.length &&
hidden = hidden && (file._data.hiddenWpt === true); item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length
} else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) { ) {
hidden = hidden && (file.wpt[item.getWaypointIndex()]._data.hidden === true); hidden =
} hidden &&
} file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()]._data.hidden === true;
} } else if (item instanceof ListWaypointsItem) {
}); hidden = hidden && file._data.hiddenWpt === true;
allHidden.set(hidden); } else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) {
hidden = hidden && file.wpt[item.getWaypointIndex()]._data.hidden === true;
}
}
}
});
allHidden.set(hidden);
} }
selection.subscribe(updateAllHidden); selection.subscribe(updateAllHidden);
@ -388,8 +452,8 @@ export const editMetadata = writable(false);
export const editStyle = writable(false); export const editStyle = writable(false);
export enum ExportState { export enum ExportState {
NONE, NONE,
SELECTION, SELECTION,
ALL ALL
} }
export const exportState = writable<ExportState>(ExportState.NONE); export const exportState = writable<ExportState>(ExportState.NONE);

View file

@ -1,476 +1,477 @@
{ {
"metadata": { "metadata": {
"home_title": "home", "home_title": "home",
"app_title": "the online GPX file editor", "app_title": "the online GPX file editor",
"embed_title": "the online GPX file editor", "embed_title": "the online GPX file editor",
"help_title": "help", "help_title": "help",
"404_title": "page not found", "404_title": "page not found",
"description": "View, edit, and create GPX files online with advanced route planning capabilities and file processing tools, beautiful maps and detailed data visualizations." "description": "View, edit, and create GPX files online with advanced route planning capabilities and file processing tools, beautiful maps and detailed data visualizations."
}, },
"menu": { "menu": {
"new": "New", "new": "New",
"new_file": "New file", "new_file": "New file",
"new_track": "New track", "new_track": "New track",
"new_segment": "New segment", "new_segment": "New segment",
"open": "Open...", "open": "Open...",
"duplicate": "Duplicate", "duplicate": "Duplicate",
"close": "Close", "close": "Close",
"close_all": "Close all", "close_all": "Close all",
"copy": "Copy", "copy": "Copy",
"paste": "Paste", "paste": "Paste",
"cut": "Cut", "cut": "Cut",
"export": "Export...", "export": "Export...",
"export_all": "Export all...", "export_all": "Export all...",
"export_options": "Export options", "export_options": "Export options",
"support_message": "The tool is free to use, but not free to run. Please consider supporting the website if you use it frequently. Thank you!", "support_message": "The tool is free to use, but not free to run. Please consider supporting the website if you use it frequently. Thank you!",
"support_button": "Help keep the website free", "support_button": "Help keep the website free",
"download_file": "Download file", "download_file": "Download file",
"download_files": "Download files", "download_files": "Download files",
"edit": "Edit", "edit": "Edit",
"undo": "Undo", "undo": "Undo",
"redo": "Redo", "redo": "Redo",
"delete": "Delete", "delete": "Delete",
"select_all": "Select all", "select_all": "Select all",
"view": "View", "view": "View",
"elevation_profile": "Elevation profile", "elevation_profile": "Elevation profile",
"vertical_file_view": "Vertical file list", "vertical_file_view": "Vertical file list",
"switch_basemap": "Switch to previous basemap", "switch_basemap": "Switch to previous basemap",
"toggle_overlays": "Toggle overlays", "toggle_overlays": "Toggle overlays",
"toggle_3d": "Toggle 3D", "toggle_3d": "Toggle 3D",
"settings": "Settings", "settings": "Settings",
"distance_units": "Distance units", "distance_units": "Distance units",
"metric": "Metric", "metric": "Metric",
"imperial": "Imperial", "imperial": "Imperial",
"velocity_units": "Velocity units", "velocity_units": "Velocity units",
"temperature_units": "Temperature units", "temperature_units": "Temperature units",
"celsius": "Celsius", "celsius": "Celsius",
"fahrenheit": "Fahrenheit", "fahrenheit": "Fahrenheit",
"language": "Language", "language": "Language",
"mode": "Theme", "mode": "Theme",
"system": "System", "system": "System",
"light": "Light", "light": "Light",
"dark": "Dark", "dark": "Dark",
"street_view_source": "Street view source", "street_view_source": "Street view source",
"mapillary": "Mapillary", "mapillary": "Mapillary",
"google": "Google", "google": "Google",
"layers": "Map layers...", "layers": "Map layers...",
"distance_markers": "Distance markers", "distance_markers": "Distance markers",
"direction_markers": "Direction arrows", "direction_markers": "Direction arrows",
"help": "Help", "help": "Help",
"more": "More...", "more": "More...",
"donate": "Donate", "donate": "Donate",
"ctrl": "Ctrl", "ctrl": "Ctrl",
"click": "Click", "click": "Click",
"drag": "Drag", "drag": "Drag",
"metadata": { "metadata": {
"button": "Info...", "button": "Info...",
"name": "Name", "name": "Name",
"description": "Description", "description": "Description",
"save": "Save" "save": "Save"
}, },
"style": { "style": {
"button": "Appearance...", "button": "Appearance...",
"color": "Color", "color": "Color",
"opacity": "Opacity", "opacity": "Opacity",
"width": "Width" "width": "Width"
}, },
"hide": "Hide", "hide": "Hide",
"unhide": "Unhide", "unhide": "Unhide",
"open_in": "Open in", "open_in": "Open in",
"fit_map": "Fit on map" "fly_to": "Fly to",
}, "fly_to_selection": "Fly to selection"
"toolbar": { },
"routing": { "toolbar": {
"tooltip": "Plan or edit a route", "routing": {
"activity": "Activity", "tooltip": "Plan or edit a route",
"use_routing": "Routing", "activity": "Activity",
"use_routing_tooltip": "Connect anchor points via road network, or in a straight line if disabled", "use_routing": "Routing",
"allow_private": "Allow private roads", "use_routing_tooltip": "Connect anchor points via road network, or in a straight line if disabled",
"reverse": { "allow_private": "Allow private roads",
"button": "Reverse", "reverse": {
"tooltip": "Reverse the direction of the route" "button": "Reverse",
}, "tooltip": "Reverse the direction of the route"
"route_back_to_start": { },
"button": "Back to start", "route_back_to_start": {
"tooltip": "Connect the last point of the route with the starting point" "button": "Back to start",
}, "tooltip": "Connect the last point of the route with the starting point"
"round_trip": { },
"button": "Round trip", "round_trip": {
"tooltip": "Return to the starting point by the same route" "button": "Round trip",
}, "tooltip": "Return to the starting point by the same route"
"start_loop_here": "Start loop here", },
"help_no_file": "Select a trace to use the routing tool, or click on the map to start creating a new route.", "start_loop_here": "Start loop here",
"help": "Click on the map to add a new anchor point, or drag existing ones to change the route.", "help_no_file": "Select a trace to use the routing tool, or click on the map to start creating a new route.",
"activities": { "help": "Click on the map to add a new anchor point, or drag existing ones to change the route.",
"bike": "Bike", "activities": {
"racing_bike": "Road bike", "bike": "Bike",
"gravel_bike": "Gravel bike", "racing_bike": "Road bike",
"mountain_bike": "Mountain bike", "gravel_bike": "Gravel bike",
"foot": "Run/hike", "mountain_bike": "Mountain bike",
"motorcycle": "Motorcycle", "foot": "Run/hike",
"water": "Water", "motorcycle": "Motorcycle",
"railway": "Railway" "water": "Water",
}, "railway": "Railway"
"surface": { },
"unknown": "Unknown", "surface": {
"paved": "Paved", "unknown": "Unknown",
"unpaved": "Unpaved", "paved": "Paved",
"asphalt": "Asphalt", "unpaved": "Unpaved",
"concrete": "Concrete", "asphalt": "Asphalt",
"chipseal": "Chipseal", "concrete": "Concrete",
"cobblestone": "Cobblestone", "chipseal": "Chipseal",
"unhewn_cobblestone": "Unhewn cobblestone", "cobblestone": "Cobblestone",
"paving_stones": "Paving stones", "unhewn_cobblestone": "Unhewn cobblestone",
"stepping_stones": "Stepping stones", "paving_stones": "Paving stones",
"sett": "Sett", "stepping_stones": "Stepping stones",
"metal": "Metal", "sett": "Sett",
"wood": "Wood", "metal": "Metal",
"compacted": "Compacted gravel", "wood": "Wood",
"fine_gravel": "Fine gravel", "compacted": "Compacted gravel",
"gravel": "Gravel", "fine_gravel": "Fine gravel",
"pebblestone": "Pebblestone", "gravel": "Gravel",
"rock": "Rock", "pebblestone": "Pebblestone",
"dirt": "Dirt", "rock": "Rock",
"ground": "Ground", "dirt": "Dirt",
"earth": "Earth", "ground": "Ground",
"snow": "Snow", "earth": "Earth",
"ice": "Ice", "snow": "Snow",
"salt": "Salt", "ice": "Ice",
"mud": "Mud", "salt": "Salt",
"sand": "Sand", "mud": "Mud",
"woodchips": "Woodchips", "sand": "Sand",
"grass": "Grass", "woodchips": "Woodchips",
"grass_paver": "Grass paver" "grass": "Grass",
}, "grass_paver": "Grass paver"
"error": { },
"from": "The start point is too far from the nearest road", "error": {
"via": "The via point is too far from the nearest road", "from": "The start point is too far from the nearest road",
"to": "The end point is too far from the nearest road", "via": "The via point is too far from the nearest road",
"timeout": "Route calculation took too long, try adding points closer together" "to": "The end point is too far from the nearest road",
} "timeout": "Route calculation took too long, try adding points closer together"
}, }
"scissors": { },
"tooltip": "Crop or split", "scissors": {
"crop": "Crop", "tooltip": "Crop or split",
"split_as": "Split the trace into", "crop": "Crop",
"help_invalid_selection": "Select a trace to crop or split.", "split_as": "Split the trace into",
"help": "Use the slider to crop the trace, or split it by clicking on one of the split markers or on the trace itself." "help_invalid_selection": "Select a trace to crop or split.",
}, "help": "Use the slider to crop the trace, or split it by clicking on one of the split markers or on the trace itself."
"time": { },
"tooltip": "Manage time data", "time": {
"start": "Start", "tooltip": "Manage time data",
"end": "End", "start": "Start",
"total_time": "Moving time", "end": "End",
"pick_date": "Pick a date", "total_time": "Moving time",
"artificial": "Create realistic time data", "pick_date": "Pick a date",
"update": "Update time data", "artificial": "Create realistic time data",
"help": "Use the form to set new time data.", "update": "Update time data",
"help_invalid_selection": "Select a single trace to manage its time data." "help": "Use the form to set new time data.",
}, "help_invalid_selection": "Select a single trace to manage its time data."
"merge": { },
"merge_traces": "Connect the traces", "merge": {
"merge_contents": "Merge the contents and keep the traces disconnected", "merge_traces": "Connect the traces",
"merge_selection": "Merge selection", "merge_contents": "Merge the contents and keep the traces disconnected",
"tooltip": "Merge items together", "merge_selection": "Merge selection",
"help_merge_traces": "Connecting the selected traces will create a single continuous trace.", "tooltip": "Merge items together",
"help_cannot_merge_traces": "Your selection must contain several traces to connect them.", "help_merge_traces": "Connecting the selected traces will create a single continuous trace.",
"help_merge_contents": "Merging the contents of the selected items will group all the contents inside the first item.", "help_cannot_merge_traces": "Your selection must contain several traces to connect them.",
"help_cannot_merge_contents": "Your selection must contain several items to merge their contents." "help_merge_contents": "Merging the contents of the selected items will group all the contents inside the first item.",
}, "help_cannot_merge_contents": "Your selection must contain several items to merge their contents."
"extract": { },
"tooltip": "Extract contents to separate items", "extract": {
"button": "Extract", "tooltip": "Extract contents to separate items",
"help": "Extracting the contents of the selected items will create a separate item for each of their contents.", "button": "Extract",
"help_invalid_selection": "Your selection must contain items with multiple traces to extract them." "help": "Extracting the contents of the selected items will create a separate item for each of their contents.",
}, "help_invalid_selection": "Your selection must contain items with multiple traces to extract them."
"waypoint": { },
"tooltip": "Create and edit points of interest", "waypoint": {
"icon": "Icon", "tooltip": "Create and edit points of interest",
"link": "Link", "icon": "Icon",
"longitude": "Longitude", "link": "Link",
"latitude": "Latitude", "longitude": "Longitude",
"create": "Create point of interest", "latitude": "Latitude",
"add": "Add point of interest to file", "create": "Create point of interest",
"help": "Fill in the form to create a new point of interest, or click on an existing one to edit it. Click on the map to fill the coordinates, or drag points of interest to move them.", "add": "Add point of interest to file",
"help_no_selection": "Select a file to create or edit points of interest." "help": "Fill in the form to create a new point of interest, or click on an existing one to edit it. Click on the map to fill the coordinates, or drag points of interest to move them.",
}, "help_no_selection": "Select a file to create or edit points of interest."
"reduce": { },
"tooltip": "Reduce the number of GPS points", "reduce": {
"tolerance": "Tolerance", "tooltip": "Reduce the number of GPS points",
"number_of_points": "Number of GPS points", "tolerance": "Tolerance",
"button": "Minify", "number_of_points": "Number of GPS points",
"help": "Use the slider to choose the number of GPS points to keep.", "button": "Minify",
"help_no_selection": "Select a trace to reduce the number of its GPS points." "help": "Use the slider to choose the number of GPS points to keep.",
}, "help_no_selection": "Select a trace to reduce the number of its GPS points."
"clean": { },
"tooltip": "Clean GPS points and points of interest with a rectangle selection", "clean": {
"delete_trackpoints": "Delete GPS points", "tooltip": "Clean GPS points and points of interest with a rectangle selection",
"delete_waypoints": "Delete points of interest", "delete_trackpoints": "Delete GPS points",
"delete_inside": "Delete inside selection", "delete_waypoints": "Delete points of interest",
"delete_outside": "Delete outside selection", "delete_inside": "Delete inside selection",
"button": "Delete", "delete_outside": "Delete outside selection",
"help": "Select a rectangle area on the map to remove GPS points and points of interest.", "button": "Delete",
"help_no_selection": "Select a trace to clean GPS points and points of interest." "help": "Select a rectangle area on the map to remove GPS points and points of interest.",
} "help_no_selection": "Select a trace to clean GPS points and points of interest."
}, }
"layers": { },
"settings": "Layer settings", "layers": {
"settings_help": "Select the map layers you want to show in the interface, add custom ones, and adjust their settings.", "settings": "Layer settings",
"selection": "Layer selection", "settings_help": "Select the map layers you want to show in the interface, add custom ones, and adjust their settings.",
"custom_layers": { "selection": "Layer selection",
"title": "Custom layers", "custom_layers": {
"new": "New custom layer", "title": "Custom layers",
"edit": "Edit custom layer", "new": "New custom layer",
"urls": "URL(s)", "edit": "Edit custom layer",
"url_placeholder": "WMTS, WMS or Mapbox style JSON", "urls": "URL(s)",
"max_zoom": "Max zoom", "url_placeholder": "WMTS, WMS or Mapbox style JSON",
"layer_type": "Layer type", "max_zoom": "Max zoom",
"basemap": "Basemap", "layer_type": "Layer type",
"overlay": "Overlay", "basemap": "Basemap",
"create": "Create layer", "overlay": "Overlay",
"update": "Update layer" "create": "Create layer",
}, "update": "Update layer"
"opacity": "Overlay opacity", },
"label": { "opacity": "Overlay opacity",
"basemaps": "Basemaps", "label": {
"overlays": "Overlays", "basemaps": "Basemaps",
"custom": "Custom", "overlays": "Overlays",
"world": "World", "custom": "Custom",
"countries": "Countries", "world": "World",
"belgium": "Belgium", "countries": "Countries",
"bulgaria": "Bulgaria", "belgium": "Belgium",
"finland": "Finland", "bulgaria": "Bulgaria",
"france": "France", "finland": "Finland",
"new_zealand": "New Zealand", "france": "France",
"norway": "Norway", "new_zealand": "New Zealand",
"spain": "Spain", "norway": "Norway",
"sweden": "Sweden", "spain": "Spain",
"switzerland": "Switzerland", "sweden": "Sweden",
"united_kingdom": "United Kingdom", "switzerland": "Switzerland",
"united_states": "United States", "united_kingdom": "United Kingdom",
"mapboxOutdoors": "Mapbox Outdoors", "united_states": "United States",
"mapboxSatellite": "Mapbox Satellite", "mapboxOutdoors": "Mapbox Outdoors",
"openStreetMap": "OpenStreetMap", "mapboxSatellite": "Mapbox Satellite",
"openTopoMap": "OpenTopoMap", "openStreetMap": "OpenStreetMap",
"openHikingMap": "OpenHikingMap", "openTopoMap": "OpenTopoMap",
"cyclOSM": "CyclOSM", "openHikingMap": "OpenHikingMap",
"linz": "LINZ Topo", "cyclOSM": "CyclOSM",
"linzTopo": "LINZ Topo50", "linz": "LINZ Topo",
"swisstopoRaster": "swisstopo Raster", "linzTopo": "LINZ Topo50",
"swisstopoVector": "swisstopo Vector", "swisstopoRaster": "swisstopo Raster",
"swisstopoSatellite": "swisstopo Satellite", "swisstopoVector": "swisstopo Vector",
"ignBe": "IGN Topo", "swisstopoSatellite": "swisstopo Satellite",
"ignFrPlan": "IGN Plan", "ignBe": "IGN Topo",
"ignFrTopo": "IGN Topo", "ignFrPlan": "IGN Plan",
"ignFrScan25": "IGN SCAN25", "ignFrTopo": "IGN Topo",
"ignFrSatellite": "IGN Satellite", "ignFrScan25": "IGN SCAN25",
"ignEs": "IGN", "ignFrSatellite": "IGN Satellite",
"ordnanceSurvey": "Ordnance Survey", "ignEs": "IGN",
"norwayTopo": "Topografisk Norgeskart 4", "ordnanceSurvey": "Ordnance Survey",
"swedenTopo": "Lantmäteriet Topo", "norwayTopo": "Topografisk Norgeskart 4",
"finlandTopo": "Lantmäteriverket Terrängkarta", "swedenTopo": "Lantmäteriet Topo",
"bgMountains": "BGMountains", "finlandTopo": "Lantmäteriverket Terrängkarta",
"usgs": "USGS", "bgMountains": "BGMountains",
"cyclOSMlite": "CyclOSM Lite", "usgs": "USGS",
"swisstopoSlope": "swisstopo Slope", "cyclOSMlite": "CyclOSM Lite",
"swisstopoHiking": "swisstopo Hiking", "swisstopoSlope": "swisstopo Slope",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHiking": "swisstopo Hiking",
"swisstopoCycling": "swisstopo Cycling", "swisstopoHikingClosures": "swisstopo Hiking Closures",
"swisstopoCyclingClosures": "swisstopo Cycling Closures", "swisstopoCycling": "swisstopo Cycling",
"swisstopoMountainBike": "swisstopo MTB", "swisstopoCyclingClosures": "swisstopo Cycling Closures",
"swisstopoMountainBikeClosures": "swisstopo MTB Closures", "swisstopoMountainBike": "swisstopo MTB",
"swisstopoSkiTouring": "swisstopo Ski Touring", "swisstopoMountainBikeClosures": "swisstopo MTB Closures",
"ignFrCadastre": "IGN Cadastre", "swisstopoSkiTouring": "swisstopo Ski Touring",
"ignSlope": "IGN Slope", "ignFrCadastre": "IGN Cadastre",
"ignSkiTouring": "IGN Ski Touring", "ignSlope": "IGN Slope",
"waymarked_trails": "Waymarked Trails", "ignSkiTouring": "IGN Ski Touring",
"waymarkedTrailsHiking": "Hiking", "waymarked_trails": "Waymarked Trails",
"waymarkedTrailsCycling": "Cycling", "waymarkedTrailsHiking": "Hiking",
"waymarkedTrailsMTB": "MTB", "waymarkedTrailsCycling": "Cycling",
"waymarkedTrailsSkating": "Skating", "waymarkedTrailsMTB": "MTB",
"waymarkedTrailsHorseRiding": "Horse Riding", "waymarkedTrailsSkating": "Skating",
"waymarkedTrailsWinter": "Winter", "waymarkedTrailsHorseRiding": "Horse Riding",
"points_of_interest": "Points of interest", "waymarkedTrailsWinter": "Winter",
"food": "Food", "points_of_interest": "Points of interest",
"bakery": "Bakery", "food": "Food",
"food-store": "Food Store", "bakery": "Bakery",
"eat-and-drink": "Eat and Drink", "food-store": "Food Store",
"amenities": "Amenities", "eat-and-drink": "Eat and Drink",
"toilets": "Toilets", "amenities": "Amenities",
"water": "Water", "toilets": "Toilets",
"shower": "Shower", "water": "Water",
"shelter": "Shelter", "shower": "Shower",
"motorized": "Cars and Motorcycles", "shelter": "Shelter",
"fuel-station": "Fuel Station", "motorized": "Cars and Motorcycles",
"parking": "Parking", "fuel-station": "Fuel Station",
"garage": "Garage", "parking": "Parking",
"barrier": "Barrier", "garage": "Garage",
"tourism": "Tourism", "barrier": "Barrier",
"attraction": "Attraction", "tourism": "Tourism",
"viewpoint": "Viewpoint", "attraction": "Attraction",
"hotel": "Hotel", "viewpoint": "Viewpoint",
"campsite": "Campsite", "hotel": "Hotel",
"hut": "Hut", "campsite": "Campsite",
"picnic": "Picnic Area", "hut": "Hut",
"summit": "Summit", "picnic": "Picnic Area",
"pass": "Pass", "summit": "Summit",
"climbing": "Climbing", "pass": "Pass",
"bicycle": "Bicycle", "climbing": "Climbing",
"bicycle-parking": "Bicycle Parking", "bicycle": "Bicycle",
"bicycle-rental": "Bicycle Rental", "bicycle-parking": "Bicycle Parking",
"bicycle-shop": "Bicycle Shop", "bicycle-rental": "Bicycle Rental",
"public-transport": "Public Transport", "bicycle-shop": "Bicycle Shop",
"railway-station": "Railway Station", "public-transport": "Public Transport",
"tram-stop": "Tram Stop", "railway-station": "Railway Station",
"bus-stop": "Bus Stop", "tram-stop": "Tram Stop",
"ferry": "Ferry" "bus-stop": "Bus Stop",
}, "ferry": "Ferry"
"color": { },
"blue": "Blue", "color": {
"bluered": "Blue Red", "blue": "Blue",
"gray": "Gray", "bluered": "Blue Red",
"hot": "Hot", "gray": "Gray",
"purple": "Purple", "hot": "Hot",
"orange": "Orange" "purple": "Purple",
} "orange": "Orange"
}, }
"chart": { },
"show_slope": "Show slope data", "chart": {
"show_surface": "Show surface data", "show_slope": "Show slope data",
"show_speed": "Show speed data", "show_surface": "Show surface data",
"show_pace": "Show pace data", "show_speed": "Show speed data",
"show_heartrate": "Show heart rate data", "show_pace": "Show pace data",
"show_cadence": "Show cadence data", "show_heartrate": "Show heart rate data",
"show_temperature": "Show temperature data", "show_cadence": "Show cadence data",
"show_power": "Show power data" "show_temperature": "Show temperature data",
}, "show_power": "Show power data"
"quantities": { },
"distance": "Distance", "quantities": {
"elevation": "Elevation", "distance": "Distance",
"temperature": "Temperature", "elevation": "Elevation",
"speed": "Speed", "temperature": "Temperature",
"pace": "Pace", "speed": "Speed",
"heartrate": "Heart rate", "pace": "Pace",
"cadence": "Cadence", "heartrate": "Heart rate",
"power": "Power", "cadence": "Cadence",
"slope": "Slope", "power": "Power",
"surface": "Surface", "slope": "Slope",
"time": "Time", "surface": "Surface",
"moving": "Moving", "time": "Time",
"total": "Total" "moving": "Moving",
}, "total": "Total"
"units": { },
"meters": "m", "units": {
"feet": "ft", "meters": "m",
"kilometers": "km", "feet": "ft",
"miles": "mi", "kilometers": "km",
"celsius": "°C", "miles": "mi",
"fahrenheit": "°F", "celsius": "°C",
"kilometers_per_hour": "km/h", "fahrenheit": "°F",
"miles_per_hour": "mph", "kilometers_per_hour": "km/h",
"minutes_per_kilometer": "min/km", "miles_per_hour": "mph",
"minutes_per_mile": "min/mi", "minutes_per_kilometer": "min/km",
"heartrate": "bpm", "minutes_per_mile": "min/mi",
"cadence": "rpm", "heartrate": "bpm",
"power": "W" "cadence": "rpm",
}, "power": "W"
"gpx": { },
"file": "File", "gpx": {
"files": "Files", "file": "File",
"track": "Track", "files": "Files",
"tracks": "Tracks", "track": "Track",
"segment": "Segment", "tracks": "Tracks",
"segments": "Segments", "segment": "Segment",
"waypoint": "Point of interest", "segments": "Segments",
"waypoints": "Points of interest", "waypoint": "Point of interest",
"symbol": { "waypoints": "Points of interest",
"alert": "Alert", "symbol": {
"anchor": "Anchor", "alert": "Alert",
"bank": "Bank", "anchor": "Anchor",
"beach": "Beach", "bank": "Bank",
"bike_trail": "Bike Trail", "beach": "Beach",
"binoculars": "Binoculars", "bike_trail": "Bike Trail",
"bridge": "Bridge", "binoculars": "Binoculars",
"building": "Building", "bridge": "Bridge",
"campground": "Campsite", "building": "Building",
"car": "Car", "campground": "Campsite",
"car_repair": "Garage", "car": "Car",
"convenience_store": "Convenience Store", "car_repair": "Garage",
"crossing": "Crossing", "convenience_store": "Convenience Store",
"department_store": "Department Store", "crossing": "Crossing",
"drinking_water": "Water", "department_store": "Department Store",
"exit": "Exit", "drinking_water": "Water",
"lodge": "Hut", "exit": "Exit",
"lodging": "Accommodation", "lodge": "Hut",
"forest": "Forest", "lodging": "Accommodation",
"gas_station": "Fuel Station", "forest": "Forest",
"ground_transportation": "Ground Transportation", "gas_station": "Fuel Station",
"hotel": "Hotel", "ground_transportation": "Ground Transportation",
"house": "House", "hotel": "Hotel",
"information": "Information", "house": "House",
"park": "Park", "information": "Information",
"parking_area": "Parking", "park": "Park",
"pharmacy": "Pharmacy", "parking_area": "Parking",
"picnic_area": "Picnic Area", "pharmacy": "Pharmacy",
"restaurant": "Restaurant", "picnic_area": "Picnic Area",
"restricted_area": "Restricted Area", "restaurant": "Restaurant",
"restroom": "Toilets", "restricted_area": "Restricted Area",
"road": "Road", "restroom": "Toilets",
"scenic_area": "Scenic Area", "road": "Road",
"shelter": "Shelter", "scenic_area": "Scenic Area",
"shopping_center": "Shopping Center", "shelter": "Shelter",
"shower": "Shower", "shopping_center": "Shopping Center",
"summit": "Summit", "shower": "Shower",
"telephone": "Telephone", "summit": "Summit",
"tunnel": "Tunnel", "telephone": "Telephone",
"water_source": "Water Source" "tunnel": "Tunnel",
} "water_source": "Water Source"
}, }
"homepage": { },
"website": "Website", "homepage": {
"home": "Home", "website": "Website",
"app": "App", "home": "Home",
"contact": "Contact", "app": "App",
"x": "X", "contact": "Contact",
"facebook": "Facebook", "x": "X",
"github": "GitHub", "facebook": "Facebook",
"crowdin": "Crowdin", "github": "GitHub",
"email": "Email", "crowdin": "Crowdin",
"contribute": "Contribute", "email": "Email",
"supported_by": "supported by", "contribute": "Contribute",
"support_button": "Support gpx.studio on Ko-fi", "supported_by": "supported by",
"route_planning": "Route planning", "support_button": "Support gpx.studio on Ko-fi",
"route_planning_description": "An intuitive interface to create itineraries tailored to each sport, based on OpenStreetMap data.", "route_planning": "Route planning",
"file_processing": "Advanced file processing", "route_planning_description": "An intuitive interface to create itineraries tailored to each sport, based on OpenStreetMap data.",
"file_processing_description": "A suite of tools for performing all common file processing tasks, and which can be applied to multiple files at once.", "file_processing": "Advanced file processing",
"maps": "Global and local maps", "file_processing_description": "A suite of tools for performing all common file processing tasks, and which can be applied to multiple files at once.",
"maps_description": "A large collection of basemaps, overlays and points of interest to help you craft your next outdoor adventure, or visualize your latest achievement.", "maps": "Global and local maps",
"data_visualization": "Data visualization", "maps_description": "A large collection of basemaps, overlays and points of interest to help you craft your next outdoor adventure, or visualize your latest achievement.",
"data_visualization_description": "An interactive elevation profile with detailed statistics to analyze recorded activities and future objectives.", "data_visualization": "Data visualization",
"identity": "Free, ad-free and open source", "data_visualization_description": "An interactive elevation profile with detailed statistics to analyze recorded activities and future objectives.",
"identity_description": "The website is free to use, without ads, and the source code is publicly available on GitHub. This is only possible thanks to the incredible support of the community." "identity": "Free, ad-free and open source",
}, "identity_description": "The website is free to use, without ads, and the source code is publicly available on GitHub. This is only possible thanks to the incredible support of the community."
"embedding": { },
"title": "Create your own map", "embedding": {
"mapbox_token": "Mapbox access token", "title": "Create your own map",
"file_urls": "File URLs (separated by commas)", "mapbox_token": "Mapbox access token",
"drive_ids": "Google Drive file IDs (separated by commas)", "file_urls": "File URLs (separated by commas)",
"basemap": "Basemap", "drive_ids": "Google Drive file IDs (separated by commas)",
"height": "Height", "basemap": "Basemap",
"fill_by": "Fill by", "height": "Height",
"none": "None", "fill_by": "Fill by",
"show_controls": "Show controls", "none": "None",
"manual_camera": "Manual camera", "show_controls": "Show controls",
"manual_camera_description": "You can move the map below to adjust the camera position.", "manual_camera": "Manual camera",
"latitude": "Latitude", "manual_camera_description": "You can move the map below to adjust the camera position.",
"longitude": "Longitude", "latitude": "Latitude",
"zoom": "Zoom", "longitude": "Longitude",
"pitch": "Pitch", "zoom": "Zoom",
"bearing": "Bearing", "pitch": "Pitch",
"preview": "Preview", "bearing": "Bearing",
"code": "Integration code" "preview": "Preview",
}, "code": "Integration code"
"webgl2_required": "WebGL 2 is required to display the map.", },
"enable_webgl2": "Learn how to enable WebGL 2 in your browser", "webgl2_required": "WebGL 2 is required to display the map.",
"page_not_found": "page not found" "enable_webgl2": "Learn how to enable WebGL 2 in your browser",
} "page_not_found": "page not found"
}