rework map bounds logic when opening files from url

This commit is contained in:
vcoppe
2024-09-19 10:35:02 +02:00
parent a27de23fa4
commit aa50f1f2b0
3 changed files with 146 additions and 144 deletions

View File

@@ -180,7 +180,7 @@ function dexieGPXFileStore(id: string): Readable<GPXFileWithStatistics> & { dest
let statistics = new GPXStatisticsTree(gpx); let statistics = new GPXStatisticsTree(gpx);
if (!fileState.has(id)) { // Update the map bounds for new files if (!fileState.has(id)) { // Update the map bounds for new files
updateTargetMapBounds(statistics.getStatisticsFor(new ListFileItem(id)).global.bounds); updateTargetMapBounds(id, statistics.getStatisticsFor(new ListFileItem(id)).global.bounds);
} }
fileState.set(id, gpx); fileState.set(id, gpx);
@@ -287,12 +287,13 @@ export const fileObservers: Writable<Map<string, Readable<GPXFileWithStatistics
const fileState: Map<string, GPXFile> = new Map(); // Used to generate patches const fileState: Map<string, GPXFile> = new Map(); // Used to generate patches
// Observe the file ids in the database, and maintain a map of file observers for the corresponding files // Observe the file ids in the database, and maintain a map of file observers for the corresponding files
export function observeFilesFromDatabase() { export function observeFilesFromDatabase(fitBounds: boolean) {
let initialize = true; let initialize = true;
liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => { liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => {
if (initialize) { if (initialize) {
if (dbFileIds.length > 0) { console.log('fitBounds', fitBounds);
initTargetMapBounds(dbFileIds.length); if (fitBounds && dbFileIds.length > 0) {
initTargetMapBounds(dbFileIds);
} }
initialize = false; initialize = false;
} }
@@ -453,13 +454,14 @@ export const dbUtils = {
}); });
}, },
addMultiple: (files: GPXFile[]) => { addMultiple: (files: GPXFile[]) => {
return applyGlobal((draft) => { let ids = getFileIds(files.length);
let ids = getFileIds(files.length); applyGlobal((draft) => {
files.forEach((file, index) => { files.forEach((file, index) => {
file._data.id = ids[index]; file._data.id = ids[index];
draft.set(file._data.id, freeze(file)); draft.set(file._data.id, freeze(file));
}); });
}); });
return ids;
}, },
applyToFile: (id: string, callback: (file: WritableDraft<GPXFile>) => void) => { applyToFile: (id: string, callback: (file: WritableDraft<GPXFile>) => void) => {
applyToFiles([id], callback); applyToFiles([id], callback);

View File

@@ -84,16 +84,20 @@ gpxStatistics.subscribe(() => {
slicedGPXStatistics.set(undefined); slicedGPXStatistics.set(undefined);
}); });
const targetMapBounds = writable({ const targetMapBounds = writable<{
bounds: mapboxgl.LngLatBounds;
ids: string[];
total: number;
}>({
bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]), bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]),
count: 0, ids: [],
total: -1 total: 0,
}); });
derived([targetMapBounds, map], (x) => x).subscribe(([bounds, $map]) => { derived([targetMapBounds, map], (x) => x).subscribe(([bounds, $map]) => {
if ( if (
$map === null || $map === null ||
bounds.count !== bounds.total || bounds.ids.length > 0 ||
(bounds.bounds.getSouth() === 90 && (bounds.bounds.getSouth() === 90 &&
bounds.bounds.getWest() === 180 && bounds.bounds.getWest() === 180 &&
bounds.bounds.getNorth() === -90 && bounds.bounds.getNorth() === -90 &&
@@ -102,10 +106,13 @@ derived([targetMapBounds, map], (x) => x).subscribe(([bounds, $map]) => {
return; return;
} }
let currentZoom = $map.getZoom();
let currentBounds = $map.getBounds(); let currentBounds = $map.getBounds();
if (bounds.count !== get(fileObservers).size && currentBounds) { if (bounds.total !== get(fileObservers).size &&
currentBounds &&
currentZoom > 2 // Extend current bounds only if the map is zoomed in
) {
// There are other files on the map // There are other files on the map
if ( if (
currentBounds.contains(bounds.bounds.getSouthEast()) && currentBounds.contains(bounds.bounds.getSouthEast()) &&
currentBounds.contains(bounds.bounds.getNorthWest()) currentBounds.contains(bounds.bounds.getNorthWest())
@@ -120,33 +127,32 @@ derived([targetMapBounds, map], (x) => x).subscribe(([bounds, $map]) => {
$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) { export function initTargetMapBounds(ids: string[]) {
targetMapBounds.set({ targetMapBounds.set({
bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]), bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]),
count: 0, ids,
total: total total: ids.length,
}); });
} }
export function updateTargetMapBounds(bounds: { southWest: Coordinates; northEast: Coordinates }) { export function updateTargetMapBounds(id: string, bounds: { southWest: Coordinates; northEast: Coordinates }) {
if ( if (get(targetMapBounds).ids.indexOf(id) === -1) {
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;
});
return; return;
} }
targetMapBounds.update((target) => { targetMapBounds.update((target) => {
target.bounds.extend(bounds.southWest); target.ids = target.ids.filter((x) => x !== id);
target.bounds.extend(bounds.northEast); if (
target.count += 1; bounds.southWest.lat !== 90 ||
bounds.southWest.lon !== 180 ||
bounds.northEast.lat !== -90 ||
bounds.northEast.lon !== -180
) {
// Avoid update for empty (new) files
target.bounds.extend(bounds.southWest);
target.bounds.extend(bounds.northEast);
}
return target; return target;
}); });
} }
@@ -246,7 +252,7 @@ export function triggerFileInput() {
} }
export async function loadFiles(list: FileList | File[]) { export async function loadFiles(list: FileList | File[]) {
let files = []; let files: GPXFile[] = [];
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) {
@@ -254,11 +260,10 @@ export async function loadFiles(list: FileList | File[]) {
} }
} }
initTargetMapBounds(list.length); let ids = dbUtils.addMultiple(files);
dbUtils.addMultiple(files); initTargetMapBounds(ids);
selectFileWhenLoaded(ids[0]);
selectFileWhenLoaded(files[0]._data.id);
} }
export async function loadFile(file: File): Promise<GPXFile | null> { export async function loadFile(file: File): Promise<GPXFile | null> {

View File

@@ -1,127 +1,122 @@
<script lang="ts"> <script lang="ts">
import GPXLayers from '$lib/components/gpx-layer/GPXLayers.svelte'; import GPXLayers from '$lib/components/gpx-layer/GPXLayers.svelte';
import ElevationProfile from '$lib/components/ElevationProfile.svelte'; import ElevationProfile from '$lib/components/ElevationProfile.svelte';
import FileList from '$lib/components/file-list/FileList.svelte'; import FileList from '$lib/components/file-list/FileList.svelte';
import GPXStatistics from '$lib/components/GPXStatistics.svelte'; import GPXStatistics from '$lib/components/GPXStatistics.svelte';
import Map from '$lib/components/Map.svelte'; import Map from '$lib/components/Map.svelte';
import Menu from '$lib/components/Menu.svelte'; import Menu from '$lib/components/Menu.svelte';
import Toolbar from '$lib/components/toolbar/Toolbar.svelte'; import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
import StreetViewControl from '$lib/components/street-view-control/StreetViewControl.svelte'; import StreetViewControl from '$lib/components/street-view-control/StreetViewControl.svelte';
import LayerControl from '$lib/components/layer-control/LayerControl.svelte'; import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
import Resizer from '$lib/components/Resizer.svelte'; import Resizer from '$lib/components/Resizer.svelte';
import { Toaster } from '$lib/components/ui/sonner'; import { Toaster } from '$lib/components/ui/sonner';
import { observeFilesFromDatabase, settings } from '$lib/db'; import { observeFilesFromDatabase, settings } from '$lib/db';
import { gpxStatistics, loadFiles, slicedGPXStatistics } from '$lib/stores'; import { gpxStatistics, loadFiles, slicedGPXStatistics } from '$lib/stores';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { languages } from '$lib/languages'; import { languages } from '$lib/languages';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { getURLForGoogleDriveFile } from '$lib/components/embedding/Embedding'; import { getURLForGoogleDriveFile } from '$lib/components/embedding/Embedding';
const { const {
verticalFileView, verticalFileView,
elevationProfile, elevationProfile,
bottomPanelSize, bottomPanelSize,
rightPanelSize, rightPanelSize,
additionalDatasets, additionalDatasets,
elevationFill elevationFill
} = settings; } = settings;
onMount(() => { onMount(() => {
observeFilesFromDatabase(); 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));
let files: string[] = JSON.parse($page.url.searchParams.get('files') || '[]'); observeFilesFromDatabase(urls.length === 0);
let ids: string[] = JSON.parse($page.url.searchParams.get('ids') || '[]');
let urls: string[] = files.concat(ids.map(getURLForGoogleDriveFile));
if (urls.length > 0) { if (urls.length > 0) {
let downloads: Promise<File | null>[] = []; let downloads: Promise<File | null>[] = [];
urls.forEach((url) => { urls.forEach((url) => {
downloads.push( downloads.push(
fetch(url) fetch(url)
.then((response) => response.blob()) .then((response) => response.blob())
.then((blob) => new File([blob], url.split('/').pop())) .then((blob) => new File([blob], url.split('/').pop()))
); );
}); });
Promise.all(downloads).then((files) => { Promise.all(downloads).then((files) => {
files = files.filter((file) => file !== null); files = files.filter((file) => file !== null);
loadFiles(files); loadFiles(files);
}); });
} }
}); });
</script> </script>
<div class="fixed flex flex-row w-screen h-screen supports-dvh:h-dvh"> <div class="fixed flex flex-row w-screen h-screen supports-dvh:h-dvh">
<div class="flex flex-col grow h-full min-w-0"> <div class="flex flex-col grow h-full min-w-0">
<div class="grow relative"> <div class="grow relative">
<Menu /> <Menu />
<div <div
class="absolute top-0 bottom-0 left-0 z-40 flex flex-col justify-center pointer-events-none" class="absolute top-0 bottom-0 left-0 z-40 flex flex-col justify-center pointer-events-none"
> >
<Toolbar /> <Toolbar />
</div> </div>
<Map class="h-full {$verticalFileView ? '' : 'horizontal'}" /> <Map class="h-full {$verticalFileView ? '' : 'horizontal'}" />
<StreetViewControl /> <StreetViewControl />
<LayerControl /> <LayerControl />
<GPXLayers /> <GPXLayers />
<Toaster richColors /> <Toaster richColors />
{#if !$verticalFileView} {#if !$verticalFileView}
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30"> <div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
<FileList orientation="horizontal" /> <FileList orientation="horizontal" />
</div> </div>
{/if} {/if}
</div> </div>
{#if $elevationProfile} {#if $elevationProfile}
<Resizer <Resizer orientation="row" bind:after={$bottomPanelSize} minAfter={100} maxAfter={300} />
orientation="row" {/if}
bind:after={$bottomPanelSize} <div
minAfter={100} class="{$elevationProfile ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"
maxAfter={300} style={$elevationProfile ? `height: ${$bottomPanelSize}px` : ''}
/> >
{/if} <GPXStatistics
<div {gpxStatistics}
class="{$elevationProfile ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4" {slicedGPXStatistics}
style={$elevationProfile ? `height: ${$bottomPanelSize}px` : ''} panelSize={$bottomPanelSize}
> orientation={$elevationProfile ? 'vertical' : 'horizontal'}
<GPXStatistics />
{gpxStatistics} {#if $elevationProfile}
{slicedGPXStatistics} <ElevationProfile
panelSize={$bottomPanelSize} {gpxStatistics}
orientation={$elevationProfile ? 'vertical' : 'horizontal'} {slicedGPXStatistics}
/> bind:additionalDatasets={$additionalDatasets}
{#if $elevationProfile} bind:elevationFill={$elevationFill}
<ElevationProfile panelSize={$bottomPanelSize}
{gpxStatistics} class="py-2"
{slicedGPXStatistics} />
bind:additionalDatasets={$additionalDatasets} {/if}
bind:elevationFill={$elevationFill} </div>
panelSize={$bottomPanelSize} </div>
class="py-2" {#if $verticalFileView}
/> <Resizer orientation="col" bind:after={$rightPanelSize} minAfter={100} maxAfter={400} />
{/if} <FileList orientation="vertical" recursive={true} style="width: {$rightPanelSize}px" />
</div> {/if}
</div>
{#if $verticalFileView}
<Resizer orientation="col" bind:after={$rightPanelSize} minAfter={100} maxAfter={400} />
<FileList orientation="vertical" recursive={true} style="width: {$rightPanelSize}px" />
{/if}
</div> </div>
<!-- hidden links for svelte crawling --> <!-- hidden links for svelte crawling -->
<div class="hidden"> <div class="hidden">
{#each Object.entries(languages) as [lang, label]} {#each Object.entries(languages) as [lang, label]}
<a href={getURLForLanguage(lang, '/embed')}> <a href={getURLForLanguage(lang, '/embed')}>
{label} {label}
</a> </a>
{/each} {/each}
</div> </div>
<style lang="postcss"> <style lang="postcss">
div :global(.toaster.group) { div :global(.toaster.group) {
@apply absolute; @apply absolute;
@apply right-2; @apply right-2;
--offset: 50px !important; --offset: 50px !important;
} }
</style> </style>