diff --git a/website/src/lib/components/Menu.svelte b/website/src/lib/components/Menu.svelte index ca67c0a3..ed13e1ec 100644 --- a/website/src/lib/components/Menu.svelte +++ b/website/src/lib/components/Menu.svelte @@ -41,7 +41,8 @@ FileStack, FileX, BookOpenText, - ChartArea + ChartArea, + Maximize } from 'lucide-svelte'; import { @@ -54,7 +55,8 @@ editMetadata, editStyle, exportState, - ExportState + ExportState, + centerMapOnSelection } from '$lib/stores'; import { copied, @@ -247,6 +249,17 @@ {$_('menu.select_all')} + { + if ($selection.size > 0) { + centerMapOnSelection(); + } + }} + > + + {$_('menu.center')} + + {#if $verticalFileView} @@ -536,6 +549,10 @@ dbUtils.setHiddenToSelection(true); } e.preventDefault(); + } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { + if ($selection.size > 0) { + centerMapOnSelection(); + } } else if (e.key === 'F1') { switchBasemaps(); e.preventDefault(); diff --git a/website/src/lib/components/file-list/FileListNodeLabel.svelte b/website/src/lib/components/file-list/FileListNodeLabel.svelte index 1efe3c31..f6880557 100644 --- a/website/src/lib/components/file-list/FileListNodeLabel.svelte +++ b/website/src/lib/components/file-list/FileListNodeLabel.svelte @@ -15,6 +15,7 @@ EyeOff, ClipboardCopy, ClipboardPaste, + Maximize, Scissors, FileStack, FileX @@ -39,7 +40,15 @@ } from './Selection'; import { getContext } from 'svelte'; import { get } from 'svelte/store'; - import { allHidden, editMetadata, editStyle, embedding, gpxLayers, map } from '$lib/stores'; + import { + allHidden, + editMetadata, + editStyle, + embedding, + centerMapOnSelection, + gpxLayers, + map + } from '$lib/stores'; import { GPXTreeElement, Track, @@ -275,8 +284,13 @@ {$_('menu.select_all')} - {/if} + + + {$_('menu.center')} + + + {$_('menu.duplicate')} diff --git a/website/src/lib/stores.ts b/website/src/lib/stores.ts index 397ae336..65643db1 100644 --- a/website/src/lib/stores.ts +++ b/website/src/lib/stores.ts @@ -6,8 +6,21 @@ import { tick } from 'svelte'; import { _ } from 'svelte-i18n'; import type { GPXLayer } from '$lib/components/gpx-layer/GPXLayer'; import { dbUtils, fileObservers, getFile, getStatistics, settings } from './db'; -import { 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 { + 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 { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte'; @@ -18,7 +31,8 @@ export const embedding = writable(false); export const selectFiles = writable<{ [key: string]: (fileId?: string) => void }>({}); export const gpxStatistics: Writable = 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() { let statistics = new GPXStatistics(); @@ -38,7 +52,8 @@ export function updateGPXData() { } let unsubscribes: Map void> = new Map(); -selection.subscribe(($selection) => { // Maintain up-to-date statistics for the current selection +selection.subscribe(($selection) => { + // Maintain up-to-date statistics for the current selection updateGPXData(); while (unsubscribes.size > 0) { @@ -53,10 +68,13 @@ selection.subscribe(($selection) => { // Maintain up-to-date statistics for the let fileObserver = get(fileObservers).get(fileId); if (fileObserver) { let first = true; - unsubscribes.set(fileId, fileObserver.subscribe(() => { - if (first) first = false; - else updateGPXData(); - })); + unsubscribes.set( + fileId, + fileObserver.subscribe(() => { + if (first) first = false; + else updateGPXData(); + }) + ); } } }); @@ -72,8 +90,15 @@ const targetMapBounds = writable({ total: -1 }); -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)) { +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) + ) { return; } @@ -81,7 +106,10 @@ derived([targetMapBounds, map], x => x).subscribe(([bounds, $map]) => { if (bounds.count !== get(fileObservers).size && currentBounds) { // There are other files on the map - if (currentBounds.contains(bounds.bounds.getSouthEast()) && currentBounds.contains(bounds.bounds.getNorthWest())) { + if ( + currentBounds.contains(bounds.bounds.getSouthEast()) && + currentBounds.contains(bounds.bounds.getNorthWest()) + ) { return; } @@ -89,14 +117,9 @@ derived([targetMapBounds, map], x => x).subscribe(([bounds, $map]) => { bounds.bounds.extend(currentBounds.getNorthEast()); } - $map.fitBounds(bounds.bounds, { - padding: 80, - linear: true, - easing: () => 1 - }); + $map.fitBounds(bounds.bounds, { padding: 80, linear: true, easing: () => 1 }); }); - export function initTargetMapBounds(total: number) { targetMapBounds.set({ bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]), @@ -105,11 +128,14 @@ export function initTargetMapBounds(total: number) { }); } -export function updateTargetMapBounds(bounds: { - southWest: Coordinates, - northEast: Coordinates -}) { - if (bounds.southWest.lat == 90 && bounds.southWest.lon == 180 && bounds.northEast.lat == -90 && bounds.northEast.lon == -180) { // Avoid update for empty (new) files +export function updateTargetMapBounds(bounds: { southWest: Coordinates; northEast: Coordinates }) { + if ( + bounds.southWest.lat == 90 && + bounds.southWest.lon == 180 && + bounds.northEast.lat == -90 && + bounds.northEast.lon == -180 + ) { + // Avoid update for empty (new) files targetMapBounds.update((target) => { target.count += 1; return target; @@ -125,6 +151,38 @@ export function updateTargetMapBounds(bounds: { }); } +export function centerMapOnSelection( +) { + let selected = get(selection).getSelected(); + let bounds = new mapboxgl.LngLatBounds(); + + if (selected.find((item) => item instanceof ListWaypointItem)) { + applyToOrderedSelectedItemsFromFile((fileId, level, items) => { + let file = getFile(fileId); + if (file) { + items.forEach((item) => { + if (item instanceof ListWaypointItem) { + let waypoint = file.wpt[item.getWaypointIndex()]; + if (waypoint) { + bounds.extend([waypoint.getLongitude(), waypoint.getLatitude()]); + } + } + }); + } + }); + } else { + let selectionBounds = get(gpxStatistics).global.bounds; + bounds.setNorthEast(selectionBounds.northEast); + bounds.setSouthWest(selectionBounds.southWest); + } + + get(map)?.fitBounds(bounds, { + padding: 80, + easing: () => 1, + maxZoom: 15 + }); +} + export const gpxLayers: Map = new Map(); export const routingControls: Map = new Map(); @@ -143,7 +201,7 @@ export const splitAs = writable(SplitType.FILES); export const streetViewEnabled = writable(false); export function newGPXFile() { - const newFileName = get(_)("menu.new_file"); + const newFileName = get(_)('menu.new_file'); let file = new GPXFile(); @@ -247,7 +305,11 @@ export function updateSelectionFromKey(down: boolean, shift: boolean) { let limitIndex: number | undefined = undefined; selected.forEach((item) => { let index = order.indexOf(item.getFileId()); - if (limitIndex === undefined || (down && index > limitIndex) || (!down && index < limitIndex)) { + if ( + limitIndex === undefined || + (down && index > limitIndex) || + (!down && index < limitIndex) + ) { limitIndex = index; } }); @@ -274,37 +336,52 @@ export function updateSelectionFromKey(down: boolean, shift: boolean) { nextIndex += down ? 1 : -1; } } - } else if (selected[0] instanceof ListTrackItem && selected[selected.length - 1] instanceof ListTrackItem) { + } else if ( + selected[0] instanceof ListTrackItem && + selected[selected.length - 1] instanceof ListTrackItem + ) { let fileId = selected[0].getFileId(); let file = getFile(fileId); if (file) { let numberOfTracks = file.trk.length; - let trackIndex = down ? selected[selected.length - 1].getTrackIndex() : selected[0].getTrackIndex(); + let trackIndex = down + ? selected[selected.length - 1].getTrackIndex() + : selected[0].getTrackIndex(); if (down && trackIndex < numberOfTracks - 1) { next = new ListTrackItem(fileId, trackIndex + 1); } else if (!down && trackIndex > 0) { next = new ListTrackItem(fileId, trackIndex - 1); } } - } else if (selected[0] instanceof ListTrackSegmentItem && selected[selected.length - 1] instanceof ListTrackSegmentItem) { + } else if ( + selected[0] instanceof ListTrackSegmentItem && + selected[selected.length - 1] instanceof ListTrackSegmentItem + ) { let fileId = selected[0].getFileId(); let file = getFile(fileId); if (file) { let trackIndex = selected[0].getTrackIndex(); let numberOfSegments = file.trk[trackIndex].trkseg.length; - let segmentIndex = down ? selected[selected.length - 1].getSegmentIndex() : selected[0].getSegmentIndex(); + let segmentIndex = down + ? selected[selected.length - 1].getSegmentIndex() + : selected[0].getSegmentIndex(); if (down && segmentIndex < numberOfSegments - 1) { next = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex + 1); } else if (!down && segmentIndex > 0) { next = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex - 1); } } - } else if (selected[0] instanceof ListWaypointItem && selected[selected.length - 1] instanceof ListWaypointItem) { + } 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(); + 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) { @@ -327,7 +404,7 @@ async function exportFiles(fileIds: string[], exclude: string[]) { let file = getFile(fileId); if (file) { exportFile(file, exclude); - await new Promise(resolve => setTimeout(resolve, 200)); + await new Promise((resolve) => setTimeout(resolve, 200)); } } } @@ -367,15 +444,21 @@ export function updateAllHidden() { } 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) { - 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) { - hidden = hidden && (file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()]._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 + ) { + hidden = + hidden && + file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()]._data.hidden === true; } else if (item instanceof ListWaypointsItem) { - hidden = hidden && (file._data.hiddenWpt === true); + hidden = hidden && file._data.hiddenWpt === true; } else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) { - hidden = hidden && (file.wpt[item.getWaypointIndex()]._data.hidden === true); + hidden = hidden && file.wpt[item.getWaypointIndex()]._data.hidden === true; } } } diff --git a/website/src/locales/en.json b/website/src/locales/en.json index 4c9b687e..3ee8d24c 100644 --- a/website/src/locales/en.json +++ b/website/src/locales/en.json @@ -77,6 +77,7 @@ }, "hide": "Hide", "unhide": "Unhide", + "center": "Center", "open_in": "Open in" }, "toolbar": {