diff --git a/website/src/lib/components/Menu.svelte b/website/src/lib/components/Menu.svelte index 21f3e446..66b8a8b4 100644 --- a/website/src/lib/components/Menu.svelte +++ b/website/src/lib/components/Menu.svelte @@ -73,6 +73,7 @@ import { fileActionManager } from '$lib/logic/file-action-manager'; import { copied, selection } from '$lib/logic/selection'; import { allHidden } from '$lib/logic/hidden'; + import { boundsManager } from '$lib/logic/bounds'; const { distanceUnits, @@ -281,7 +282,7 @@ {/if} selection.selectAll()} disabled={fileStateCollection.size == 0} > @@ -291,9 +292,10 @@ { if ($selection.size > 0) { - // centerMapOnSelection(); + boundsManager.centerMapOnSelection(); } }} + disabled={$selection.size == 0} > {i18n._('menu.center')} @@ -302,7 +304,7 @@ {#if $treeFileView} selection.copySelection()} disabled={$selection.size === 0} > @@ -310,7 +312,7 @@ selection.cutSelection()} disabled={$selection.size === 0} > @@ -375,7 +377,7 @@ /> - + map.toggle3D()}> {i18n._('menu.toggle_3d')} @@ -629,9 +631,9 @@ } e.preventDefault(); } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { - // if ($selection.size > 0) { - // centerMapOnSelection(); - // } + if ($selection.size > 0) { + boundsManager.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 551d1139..672ca713 100644 --- a/website/src/lib/components/file-list/FileListNodeLabel.svelte +++ b/website/src/lib/components/file-list/FileListNodeLabel.svelte @@ -41,6 +41,7 @@ import { map } from '$lib/components/map/map'; import { fileActions, pasteSelection } from '$lib/logic/file-actions'; import { allHidden } from '$lib/logic/hidden'; + import { boundsManager } from '$lib/logic/bounds'; let { node, @@ -287,7 +288,7 @@ {/if} - + boundsManager.centerMapOnSelection()}> {i18n._('menu.center')} diff --git a/website/src/lib/components/map/gpx-layer/gpx-layers.ts b/website/src/lib/components/map/gpx-layer/gpx-layers.ts index 74b5fb74..76604a8f 100644 --- a/website/src/lib/components/map/gpx-layer/gpx-layers.ts +++ b/website/src/lib/components/map/gpx-layer/gpx-layers.ts @@ -14,9 +14,11 @@ export class GPXLayerCollection { return; } this._fileStateCollectionObserver = new GPXFileStateCollectionObserver( - (fileId, fileState) => { - const layer = new GPXLayer(fileId, fileState); - this._layers.set(fileId, layer); + (newFiles) => { + newFiles.forEach((fileState, fileId) => { + const layer = new GPXLayer(fileId, fileState); + this._layers.set(fileId, layer); + }); }, (fileId) => { const layer = this._layers.get(fileId); diff --git a/website/src/lib/components/map/map.ts b/website/src/lib/components/map/map.ts index 10cd6d1f..10be9d25 100644 --- a/website/src/lib/components/map/map.ts +++ b/website/src/lib/components/map/map.ts @@ -221,105 +221,3 @@ export class MapboxGLMap { } export const map = new MapboxGLMap(); - -// const targetMapBounds: { -// bounds: mapboxgl.LngLatBounds; -// ids: string[]; -// total: number; -// } = $state({ -// bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]), -// ids: [], -// total: 0, -// }); - -// $effect(() => { -// if ( -// map.current === null || -// targetMapBounds.ids.length > 0 || -// (targetMapBounds.bounds.getSouth() === 90 && -// targetMapBounds.bounds.getWest() === 180 && -// targetMapBounds.bounds.getNorth() === -90 && -// targetMapBounds.bounds.getEast() === -180) -// ) { -// return; -// } - -// let currentZoom = map.current.getZoom(); -// let currentBounds = map.current.getBounds(); -// if ( -// targetMapBounds.total !== get(fileObservers).size && -// currentBounds && -// currentZoom > 2 // Extend current bounds only if the map is zoomed in -// ) { -// // There are other files on the map -// if ( -// currentBounds.contains(targetMapBounds.bounds.getSouthEast()) && -// currentBounds.contains(targetMapBounds.bounds.getNorthWest()) -// ) { -// return; -// } - -// targetMapBounds.bounds.extend(currentBounds.getSouthWest()); -// targetMapBounds.bounds.extend(currentBounds.getNorthEast()); -// } - -// map.current.fitBounds(targetMapBounds.bounds, { padding: 80, linear: true, easing: () => 1 }); -// }); - -// export function initTargetMapBounds(ids: string[]) { -// targetMapBounds.bounds = new mapboxgl.LngLatBounds([180, 90, -180, -90]); -// targetMapBounds.ids = ids; -// targetMapBounds.total = ids.length; -// } - -// export function updateTargetMapBounds( -// id: string, -// bounds: { southWest: Coordinates; northEast: Coordinates } -// ) { -// if (targetMapBounds.ids.indexOf(id) === -1) { -// return; -// } - -// if ( -// bounds.southWest.lat !== 90 || -// bounds.southWest.lon !== 180 || -// bounds.northEast.lat !== -90 || -// bounds.northEast.lon !== -180 -// ) { -// // Avoid update for empty (new) files -// targetMapBounds.ids = targetMapBounds.ids.filter((x) => x !== id); -// targetMapBounds.bounds.extend(bounds.southWest); -// targetMapBounds.bounds.extend(bounds.northEast); -// } -// } - -// 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, -// }); -// } diff --git a/website/src/lib/components/toolbar/tools/reduce/reduce.ts b/website/src/lib/components/toolbar/tools/reduce/reduce.ts index da4a7158..3f9e4a35 100644 --- a/website/src/lib/components/toolbar/tools/reduce/reduce.ts +++ b/website/src/lib/components/toolbar/tools/reduce/reduce.ts @@ -61,8 +61,13 @@ export class ReducedGPXLayerCollection { this._layers = new Map(); this._simplified = new Map(); this._fileStateCollectionOberver = new GPXFileStateCollectionObserver( - (fileId, fileState) => { - this._layers.set(fileId, new ReducedGPXLayer(fileState, this._updateSimplified)); + (newFiles) => { + newFiles.forEach((fileState, fileId) => { + this._layers.set( + fileId, + new ReducedGPXLayer(fileState, this._updateSimplified) + ); + }); }, (fileId) => { this._layers.get(fileId)?.destroy(); diff --git a/website/src/lib/components/toolbar/tools/routing/Routing.svelte b/website/src/lib/components/toolbar/tools/routing/Routing.svelte index 7fe4707e..e882b12b 100644 --- a/website/src/lib/components/toolbar/tools/routing/Routing.svelte +++ b/website/src/lib/components/toolbar/tools/routing/Routing.svelte @@ -84,11 +84,13 @@ onMount(() => { if ($map && popup && popupElement) { fileStateCollectionObserver = new GPXFileStateCollectionObserver( - (fileId, fileState) => { - routingControls.set( - fileId, - new RoutingControls(fileId, fileState, popup, popupElement) - ); + (newFiles) => { + newFiles.forEach((fileState, fileId) => { + routingControls.set( + fileId, + new RoutingControls(fileId, fileState, popup, popupElement) + ); + }); }, (fileId) => { const controls = routingControls.get(fileId); diff --git a/website/src/lib/logic/bounds.ts b/website/src/lib/logic/bounds.ts new file mode 100644 index 00000000..b6d4f23a --- /dev/null +++ b/website/src/lib/logic/bounds.ts @@ -0,0 +1,185 @@ +import { get } from 'svelte/store'; +import { selection } from '$lib/logic/selection'; +import mapboxgl from 'mapbox-gl'; +import { ListFileItem, ListWaypointItem } from '$lib/components/file-list/file-list'; +import { + fileStateCollection, + GPXFileState, + GPXFileStateCollectionObserver, +} from '$lib/logic/file-state'; +import { gpxStatistics } from '$lib/logic/statistics'; +import { map } from '$lib/components/map/map'; +import type { GPXFileWithStatistics } from './statistics-tree'; +import type { Coordinates } from 'gpx'; +import { page } from '$app/state'; +import { browser } from '$app/environment'; + +// const targetMapBounds: { +// bounds: mapboxgl.LngLatBounds; +// ids: string[]; +// total: number; +// } = $state({ +// bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]), +// ids: [], +// total: 0, +// }); + +// $effect(() => { +// if ( +// map.current === null || +// targetMapBounds.ids.length > 0 || +// (targetMapBounds.bounds.getSouth() === 90 && +// targetMapBounds.bounds.getWest() === 180 && +// targetMapBounds.bounds.getNorth() === -90 && +// targetMapBounds.bounds.getEast() === -180) +// ) { +// return; +// } + +// let currentZoom = map.current.getZoom(); +// let currentBounds = map.current.getBounds(); +// if ( +// targetMapBounds.total !== get(fileObservers).size && +// currentBounds && +// currentZoom > 2 // Extend current bounds only if the map is zoomed in +// ) { +// // There are other files on the map +// if ( +// currentBounds.contains(targetMapBounds.bounds.getSouthEast()) && +// currentBounds.contains(targetMapBounds.bounds.getNorthWest()) +// ) { +// return; +// } + +// targetMapBounds.bounds.extend(currentBounds.getSouthWest()); +// targetMapBounds.bounds.extend(currentBounds.getNorthEast()); +// } + +// map.current.fitBounds(targetMapBounds.bounds, { padding: 80, linear: true, easing: () => 1 }); +// }); + +export class BoundsManager { + private _bounds: mapboxgl.LngLatBounds = new mapboxgl.LngLatBounds(); + private _files: Set = new Set(); + private _fileStateCollectionObserver: GPXFileStateCollectionObserver | null = null; + private _unsubscribes: (() => void)[] = []; + + constructor() { + this._fileStateCollectionObserver = new GPXFileStateCollectionObserver( + (newFiles) => { + if (page.url.hash.length == 0) { + this.fitBoundsOnLoad(Array.from(newFiles.keys())); + } + }, + (fileId) => {}, + () => {} + ); + } + + fitBoundsOnLoad(files: string[]) { + this.reset(); + + this._files = new Set(files); + this._fileStateCollectionObserver = new GPXFileStateCollectionObserver( + (newFiles) => { + newFiles.forEach((fileState, fileId) => { + if (this._files.has(fileId)) { + this._unsubscribes.push( + fileState.subscribe((state) => { + this.addBoundsFromFile(fileId, state); + }) + ); + } + }); + }, + (fileId) => {}, + () => {} + ); + } + + addBoundsFromFile(fileId: string, file: GPXFileWithStatistics | undefined) { + if (!file || !this._files.has(fileId)) return; + + this._files.delete(fileId); + + const bounds = file.statistics.getStatisticsFor(new ListFileItem(fileId)).global.bounds; + if (!this.validBounds(bounds)) return; + + this._bounds.extend(bounds.southWest); + this._bounds.extend(bounds.northEast); + + if (this._files.size === 0) { + this.finalizeFitBounds(); + } + } + + finalizeFitBounds() { + if ( + this._bounds.getSouth() === 90 && + this._bounds.getWest() === 180 && + this._bounds.getNorth() === -90 && + this._bounds.getEast() === -180 + ) { + return; + } + + this._unsubscribes.push( + map.subscribe((map_) => { + if (!map_) return; + map_.fitBounds(this._bounds, { padding: 80, linear: true, easing: () => 1 }); + this.reset(); + }) + ); + } + + reset() { + if (this._fileStateCollectionObserver) { + this._fileStateCollectionObserver.destroy(); + } + this._unsubscribes.forEach((unsubscribe) => unsubscribe()); + this._unsubscribes = []; + this._bounds = new mapboxgl.LngLatBounds([180, 90, -180, -90]); + } + + centerMapOnSelection() { + let selected = get(selection).getSelected(); + let bounds = new mapboxgl.LngLatBounds(); + + if (selected.find((item) => item instanceof ListWaypointItem)) { + selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { + let file = fileStateCollection.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, + }); + } + + validBounds(bounds: { southWest: Coordinates; northEast: Coordinates }) { + return ( + bounds.southWest.lat !== 90 || + bounds.southWest.lon !== 180 || + bounds.northEast.lat !== -90 || + bounds.northEast.lon !== -180 + ); + } +} + +export const boundsManager = new BoundsManager(); diff --git a/website/src/lib/logic/file-action-manager.ts b/website/src/lib/logic/file-action-manager.ts index 3df1d179..9a5d5c44 100644 --- a/website/src/lib/logic/file-action-manager.ts +++ b/website/src/lib/logic/file-action-manager.ts @@ -35,15 +35,17 @@ export class FileActionManager { this._files = new Map(); this._fileSubscriptions = new Map(); this._fileStateCollectionObserver = new GPXFileStateCollectionObserver( - (fileId, fileState) => { - this._fileSubscriptions.set( - fileId, - fileState.subscribe((fileWithStatistics) => { - if (fileWithStatistics) { - this._files.set(fileId, fileWithStatistics.file); - } - }) - ); + (newFiles) => { + newFiles.forEach((fileState, fileId) => { + this._fileSubscriptions.set( + fileId, + fileState.subscribe((fileWithStatistics) => { + if (fileWithStatistics) { + this._files.set(fileId, fileWithStatistics.file); + } + }) + ); + }); }, (fileId) => { let unsubscribe = this._fileSubscriptions.get(fileId); diff --git a/website/src/lib/logic/file-actions.ts b/website/src/lib/logic/file-actions.ts index 7a9bef1a..aebf93ae 100644 --- a/website/src/lib/logic/file-actions.ts +++ b/website/src/lib/logic/file-actions.ts @@ -32,6 +32,7 @@ import { get } from 'svelte/store'; import { settings } from '$lib/logic/settings'; import { getClosestLinePoint, getElevation } from '$lib/utils'; import { gpxStatistics } from '$lib/logic/statistics'; +import { boundsManager } from './bounds'; // Generate unique file ids, different from the ones in the database export function getFileIds(n: number) { @@ -97,9 +98,8 @@ export async function loadFiles(list: FileList | File[]) { } let ids = fileActions.addMultiple(files); - - // initTargetMapBounds(ids); selection.selectFileWhenLoaded(ids[0]); + boundsManager.fitBoundsOnLoad(ids); } export async function loadFile(file: File): Promise { diff --git a/website/src/lib/logic/file-state.ts b/website/src/lib/logic/file-state.ts index e4fbb389..8cc3cc2b 100644 --- a/website/src/lib/logic/file-state.ts +++ b/website/src/lib/logic/file-state.ts @@ -13,23 +13,12 @@ export class GPXFileState { constructor(db: Database, fileId: string) { this._file = writable(undefined); - let first = true; this._subscription = liveQuery(() => db.files.get(fileId)).subscribe((value) => { if (value !== undefined) { let file = new GPXFile(value); updateAnchorPoints(file); - let statistics = new GPXStatisticsTree(file); - if (first) { - // Update the map bounds for new files - // updateTargetMapBounds( - // id, - // statistics.getStatisticsFor(new ListFileItem(id)).global.bounds - // ); - first = false; - } - this._file.set({ file, statistics }); } }); @@ -55,27 +44,12 @@ export class GPXFileState { // Observe the file ids in the database, and maintain a map of file states for the corresponding files export class GPXFileStateCollection { - private _db: Database; private _files: Writable>; constructor(db: Database) { - this._db = db; this._files = writable(new Map()); - } - subscribe(run: Subscriber>, invalidate?: () => void) { - return this._files.subscribe(run, invalidate); - } - - initialize(fitBounds: boolean) { - let initialize = true; - liveQuery(() => this._db.fileids.toArray()).subscribe((dbFileIds) => { - if (initialize) { - // if (fitBounds && dbFileIds.length > 0) { - // initTargetMapBounds(dbFileIds); - // } - initialize = false; - } + liveQuery(() => db.fileids.toArray()).subscribe((dbFileIds) => { const currentFiles = get(this._files); // Find new files to observe let newFiles = dbFileIds @@ -90,7 +64,7 @@ export class GPXFileStateCollection { // Update the map of file states this._files.update(($files) => { newFiles.forEach((id) => { - $files.set(id, new GPXFileState(this._db, id)); + $files.set(id, new GPXFileState(db, id)); }); deletedFiles.forEach((id) => { $files.get(id)?.destroy(); @@ -111,6 +85,10 @@ export class GPXFileStateCollection { }); } + subscribe(run: Subscriber>, invalidate?: () => void) { + return this._files.subscribe(run, invalidate); + } + get size(): number { return get(this._files).size; } @@ -141,21 +119,21 @@ export class GPXFileStateCollection { // Collection of all file states export const fileStateCollection = new GPXFileStateCollection(db); -export type GPXFileStateCallback = (fileId: string, fileState: GPXFileState) => void; +export type GPXFileStateCallback = (files: Map) => void; export class GPXFileStateCollectionObserver { private _fileIds: Set; - private _onFileAdded: GPXFileStateCallback; + private _onFilesAdded: GPXFileStateCallback; private _onFileRemoved: (fileId: string) => void; private _onDestroy: () => void; private _unsubscribe: () => void; constructor( - onFileAdded: GPXFileStateCallback, + onFilesAdded: GPXFileStateCallback, onFileRemoved: (fileId: string) => void, onDestroy: () => void ) { this._fileIds = new Set(); - this._onFileAdded = onFileAdded; + this._onFilesAdded = onFilesAdded; this._onFileRemoved = onFileRemoved; this._onDestroy = onDestroy; @@ -166,12 +144,16 @@ export class GPXFileStateCollectionObserver { this._fileIds.delete(fileId); } }); + let newFiles = new Map(); files.forEach((file: GPXFileState, fileId: string) => { if (!this._fileIds.has(fileId)) { - this._onFileAdded(fileId, file); + newFiles.set(fileId, file); this._fileIds.add(fileId); } }); + if (newFiles.size > 0) { + this._onFilesAdded(newFiles); + } }); } diff --git a/website/src/lib/logic/selection.ts b/website/src/lib/logic/selection.ts index 58d734c0..6babf6c3 100644 --- a/website/src/lib/logic/selection.ts +++ b/website/src/lib/logic/selection.ts @@ -261,15 +261,17 @@ export class SelectedGPXFilesObserver { constructor(onSelectedFileChange: () => void) { this._unsubscribes = new Map(); this._fileStateCollectionObserver = new GPXFileStateCollectionObserver( - (fileId, fileState) => { - this._unsubscribes.set( - fileId, - fileState.subscribe(() => { - if (get(selection).hasAnyChildren(new ListFileItem(fileId))) { - onSelectedFileChange(); - } - }) - ); + (newFiles) => { + newFiles.forEach((fileState, fileId) => { + this._unsubscribes.set( + fileId, + fileState.subscribe(() => { + if (get(selection).hasAnyChildren(new ListFileItem(fileId))) { + onSelectedFileChange(); + } + }) + ); + }); }, (fileId) => { this._unsubscribes.get(fileId)?.(); diff --git a/website/src/routes/[[language]]/app/+page.svelte b/website/src/routes/[[language]]/app/+page.svelte index aacb2f6c..ecab1b88 100644 --- a/website/src/routes/[[language]]/app/+page.svelte +++ b/website/src/routes/[[language]]/app/+page.svelte @@ -11,18 +11,15 @@ // import CoordinatesPopup from '$lib/components/map/CoordinatesPopup.svelte'; import Resizer from '$lib/components/Resizer.svelte'; import { Toaster } from '$lib/components/ui/sonner'; - // import { onMount } from 'svelte'; - // import { page } from '$app/state'; import { languages } from '$lib/languages'; import { getURLForLanguage } from '$lib/utils'; - // import { getURLForGoogleDriveFile } from '$lib/components/embedding/Embedding'; import { i18n } from '$lib/i18n.svelte'; import { settings } from '$lib/logic/settings'; - import { fileStateCollection } from '$lib/logic/file-state'; import { loadFiles } from '$lib/logic/file-actions'; import { onMount } from 'svelte'; import { page } from '$app/state'; import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics'; + import { getURLForGoogleDriveFile } from '$lib/components/embedding/Embedding'; const { treeFileView, @@ -36,9 +33,7 @@ onMount(() => { let files: string[] = JSON.parse(page.url.searchParams.get('files') || '[]'); let ids: string[] = JSON.parse(page.url.searchParams.get('ids') || '[]'); - let urls: string[] = []; //files.concat(ids.map(getURLForGoogleDriveFile)); - - fileStateCollection.initialize(urls.length === 0); + let urls: string[] = files.concat(ids.map(getURLForGoogleDriveFile)); if (urls.length > 0) { let downloads: Promise[] = []; @@ -57,7 +52,7 @@ }); -
+

{i18n._('metadata.home_title')} — {i18n._('metadata.app_title')}

{i18n._('metadata.description')}

{i18n._('toolbar.routing.tooltip')}