mirror of
				https://github.com/gpxstudio/gpx.studio.git
				synced 2025-11-04 05:21:09 +00:00 
			
		
		
		
	simplify specifying drive ids
This commit is contained in:
		@@ -1,264 +1,272 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import GPXLayers from '$lib/components/gpx-layer/GPXLayers.svelte';
 | 
			
		||||
	import ElevationProfile from '$lib/components/ElevationProfile.svelte';
 | 
			
		||||
	import FileList from '$lib/components/file-list/FileList.svelte';
 | 
			
		||||
	import GPXStatistics from '$lib/components/GPXStatistics.svelte';
 | 
			
		||||
	import Map from '$lib/components/Map.svelte';
 | 
			
		||||
	import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
 | 
			
		||||
	import OpenIn from '$lib/components/embedding/OpenIn.svelte';
 | 
			
		||||
	import {
 | 
			
		||||
		gpxStatistics,
 | 
			
		||||
		slicedGPXStatistics,
 | 
			
		||||
		embedding,
 | 
			
		||||
		loadFile,
 | 
			
		||||
		map,
 | 
			
		||||
		updateGPXData
 | 
			
		||||
	} from '$lib/stores';
 | 
			
		||||
	import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
	import { fileObservers, settings, GPXStatisticsTree } from '$lib/db';
 | 
			
		||||
	import { readable } from 'svelte/store';
 | 
			
		||||
	import type { GPXFile } from 'gpx';
 | 
			
		||||
	import { selection } from '$lib/components/file-list/Selection';
 | 
			
		||||
	import { ListFileItem } from '$lib/components/file-list/FileList';
 | 
			
		||||
	import { allowedEmbeddingBasemaps, type EmbeddingOptions } from './Embedding';
 | 
			
		||||
	import { mode, setMode } from 'mode-watcher';
 | 
			
		||||
    import GPXLayers from '$lib/components/gpx-layer/GPXLayers.svelte';
 | 
			
		||||
    import ElevationProfile from '$lib/components/ElevationProfile.svelte';
 | 
			
		||||
    import FileList from '$lib/components/file-list/FileList.svelte';
 | 
			
		||||
    import GPXStatistics from '$lib/components/GPXStatistics.svelte';
 | 
			
		||||
    import Map from '$lib/components/Map.svelte';
 | 
			
		||||
    import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
 | 
			
		||||
    import OpenIn from '$lib/components/embedding/OpenIn.svelte';
 | 
			
		||||
    import {
 | 
			
		||||
        gpxStatistics,
 | 
			
		||||
        slicedGPXStatistics,
 | 
			
		||||
        embedding,
 | 
			
		||||
        loadFile,
 | 
			
		||||
        map,
 | 
			
		||||
        updateGPXData
 | 
			
		||||
    } from '$lib/stores';
 | 
			
		||||
    import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
    import { fileObservers, settings, GPXStatisticsTree } from '$lib/db';
 | 
			
		||||
    import { readable } from 'svelte/store';
 | 
			
		||||
    import type { GPXFile } from 'gpx';
 | 
			
		||||
    import { selection } from '$lib/components/file-list/Selection';
 | 
			
		||||
    import { ListFileItem } from '$lib/components/file-list/FileList';
 | 
			
		||||
    import {
 | 
			
		||||
        allowedEmbeddingBasemaps,
 | 
			
		||||
        getFilesFromEmbeddingOptions,
 | 
			
		||||
        type EmbeddingOptions
 | 
			
		||||
    } from './Embedding';
 | 
			
		||||
    import { mode, setMode } from 'mode-watcher';
 | 
			
		||||
 | 
			
		||||
	$embedding = true;
 | 
			
		||||
    $embedding = true;
 | 
			
		||||
 | 
			
		||||
	const {
 | 
			
		||||
		currentBasemap,
 | 
			
		||||
		distanceUnits,
 | 
			
		||||
		velocityUnits,
 | 
			
		||||
		temperatureUnits,
 | 
			
		||||
		fileOrder,
 | 
			
		||||
		distanceMarkers,
 | 
			
		||||
		directionMarkers
 | 
			
		||||
	} = settings;
 | 
			
		||||
    const {
 | 
			
		||||
        currentBasemap,
 | 
			
		||||
        distanceUnits,
 | 
			
		||||
        velocityUnits,
 | 
			
		||||
        temperatureUnits,
 | 
			
		||||
        fileOrder,
 | 
			
		||||
        distanceMarkers,
 | 
			
		||||
        directionMarkers
 | 
			
		||||
    } = settings;
 | 
			
		||||
 | 
			
		||||
	export let useHash = true;
 | 
			
		||||
	export let options: EmbeddingOptions;
 | 
			
		||||
	export let hash: string;
 | 
			
		||||
    export let useHash = true;
 | 
			
		||||
    export let options: EmbeddingOptions;
 | 
			
		||||
    export let hash: string;
 | 
			
		||||
 | 
			
		||||
	let prevSettings = {
 | 
			
		||||
		distanceMarkers: false,
 | 
			
		||||
		directionMarkers: false,
 | 
			
		||||
		distanceUnits: 'metric',
 | 
			
		||||
		velocityUnits: 'speed',
 | 
			
		||||
		temperatureUnits: 'celsius',
 | 
			
		||||
		theme: 'system'
 | 
			
		||||
	};
 | 
			
		||||
    let prevSettings = {
 | 
			
		||||
        distanceMarkers: false,
 | 
			
		||||
        directionMarkers: false,
 | 
			
		||||
        distanceUnits: 'metric',
 | 
			
		||||
        velocityUnits: 'speed',
 | 
			
		||||
        temperatureUnits: 'celsius',
 | 
			
		||||
        theme: 'system'
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
	function applyOptions() {
 | 
			
		||||
		fileObservers.update(($fileObservers) => {
 | 
			
		||||
			$fileObservers.clear();
 | 
			
		||||
			return $fileObservers;
 | 
			
		||||
		});
 | 
			
		||||
    function applyOptions() {
 | 
			
		||||
        fileObservers.update(($fileObservers) => {
 | 
			
		||||
            $fileObservers.clear();
 | 
			
		||||
            return $fileObservers;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
		let downloads: Promise<GPXFile | null>[] = [];
 | 
			
		||||
		options.files.forEach((url) => {
 | 
			
		||||
			downloads.push(
 | 
			
		||||
				fetch(url)
 | 
			
		||||
					.then((response) => response.blob())
 | 
			
		||||
					.then((blob) => new File([blob], url.split('/').pop() ?? url))
 | 
			
		||||
					.then(loadFile)
 | 
			
		||||
			);
 | 
			
		||||
		});
 | 
			
		||||
        let downloads: Promise<GPXFile | null>[] = [];
 | 
			
		||||
        getFilesFromEmbeddingOptions(options).forEach((url) => {
 | 
			
		||||
            downloads.push(
 | 
			
		||||
                fetch(url)
 | 
			
		||||
                    .then((response) => response.blob())
 | 
			
		||||
                    .then((blob) => new File([blob], url.split('/').pop() ?? url))
 | 
			
		||||
                    .then(loadFile)
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
		Promise.all(downloads).then((files) => {
 | 
			
		||||
			let ids: string[] = [];
 | 
			
		||||
			let bounds = {
 | 
			
		||||
				southWest: {
 | 
			
		||||
					lat: 90,
 | 
			
		||||
					lon: 180
 | 
			
		||||
				},
 | 
			
		||||
				northEast: {
 | 
			
		||||
					lat: -90,
 | 
			
		||||
					lon: -180
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
        Promise.all(downloads).then((files) => {
 | 
			
		||||
            let ids: string[] = [];
 | 
			
		||||
            let bounds = {
 | 
			
		||||
                southWest: {
 | 
			
		||||
                    lat: 90,
 | 
			
		||||
                    lon: 180
 | 
			
		||||
                },
 | 
			
		||||
                northEast: {
 | 
			
		||||
                    lat: -90,
 | 
			
		||||
                    lon: -180
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
			fileObservers.update(($fileObservers) => {
 | 
			
		||||
				files.forEach((file, index) => {
 | 
			
		||||
					if (file === null) {
 | 
			
		||||
						return;
 | 
			
		||||
					}
 | 
			
		||||
            fileObservers.update(($fileObservers) => {
 | 
			
		||||
                files.forEach((file, index) => {
 | 
			
		||||
                    if (file === null) {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
					let id = `gpx-${index}-embed`;
 | 
			
		||||
					file._data.id = id;
 | 
			
		||||
					let statistics = new GPXStatisticsTree(file);
 | 
			
		||||
                    let id = `gpx-${index}-embed`;
 | 
			
		||||
                    file._data.id = id;
 | 
			
		||||
                    let statistics = new GPXStatisticsTree(file);
 | 
			
		||||
 | 
			
		||||
					$fileObservers.set(
 | 
			
		||||
						id,
 | 
			
		||||
						readable({
 | 
			
		||||
							file,
 | 
			
		||||
							statistics
 | 
			
		||||
						})
 | 
			
		||||
					);
 | 
			
		||||
                    $fileObservers.set(
 | 
			
		||||
                        id,
 | 
			
		||||
                        readable({
 | 
			
		||||
                            file,
 | 
			
		||||
                            statistics
 | 
			
		||||
                        })
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
					ids.push(id);
 | 
			
		||||
					let fileBounds = statistics.getStatisticsFor(new ListFileItem(id)).global.bounds;
 | 
			
		||||
                    ids.push(id);
 | 
			
		||||
                    let fileBounds = statistics.getStatisticsFor(new ListFileItem(id)).global
 | 
			
		||||
                        .bounds;
 | 
			
		||||
 | 
			
		||||
					bounds.southWest.lat = Math.min(bounds.southWest.lat, fileBounds.southWest.lat);
 | 
			
		||||
					bounds.southWest.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon);
 | 
			
		||||
					bounds.northEast.lat = Math.max(bounds.northEast.lat, fileBounds.northEast.lat);
 | 
			
		||||
					bounds.northEast.lon = Math.max(bounds.northEast.lon, fileBounds.northEast.lon);
 | 
			
		||||
				});
 | 
			
		||||
                    bounds.southWest.lat = Math.min(bounds.southWest.lat, fileBounds.southWest.lat);
 | 
			
		||||
                    bounds.southWest.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon);
 | 
			
		||||
                    bounds.northEast.lat = Math.max(bounds.northEast.lat, fileBounds.northEast.lat);
 | 
			
		||||
                    bounds.northEast.lon = Math.max(bounds.northEast.lon, fileBounds.northEast.lon);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
				return $fileObservers;
 | 
			
		||||
			});
 | 
			
		||||
                return $fileObservers;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
			$fileOrder = [...$fileOrder.filter((id) => !id.includes('embed')), ...ids];
 | 
			
		||||
            $fileOrder = [...$fileOrder.filter((id) => !id.includes('embed')), ...ids];
 | 
			
		||||
 | 
			
		||||
			selection.update(($selection) => {
 | 
			
		||||
				$selection.clear();
 | 
			
		||||
				ids.forEach((id) => {
 | 
			
		||||
					$selection.toggle(new ListFileItem(id));
 | 
			
		||||
				});
 | 
			
		||||
				return $selection;
 | 
			
		||||
			});
 | 
			
		||||
            selection.update(($selection) => {
 | 
			
		||||
                $selection.clear();
 | 
			
		||||
                ids.forEach((id) => {
 | 
			
		||||
                    $selection.toggle(new ListFileItem(id));
 | 
			
		||||
                });
 | 
			
		||||
                return $selection;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
			if (hash.length === 0) {
 | 
			
		||||
				map.subscribe(($map) => {
 | 
			
		||||
					if ($map) {
 | 
			
		||||
						$map.fitBounds(
 | 
			
		||||
							[
 | 
			
		||||
								bounds.southWest.lon,
 | 
			
		||||
								bounds.southWest.lat,
 | 
			
		||||
								bounds.northEast.lon,
 | 
			
		||||
								bounds.northEast.lat
 | 
			
		||||
							],
 | 
			
		||||
							{
 | 
			
		||||
								padding: 80,
 | 
			
		||||
								linear: true,
 | 
			
		||||
								easing: () => 1
 | 
			
		||||
							}
 | 
			
		||||
						);
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
            if (hash.length === 0) {
 | 
			
		||||
                map.subscribe(($map) => {
 | 
			
		||||
                    if ($map) {
 | 
			
		||||
                        $map.fitBounds(
 | 
			
		||||
                            [
 | 
			
		||||
                                bounds.southWest.lon,
 | 
			
		||||
                                bounds.southWest.lat,
 | 
			
		||||
                                bounds.northEast.lon,
 | 
			
		||||
                                bounds.northEast.lat
 | 
			
		||||
                            ],
 | 
			
		||||
                            {
 | 
			
		||||
                                padding: 80,
 | 
			
		||||
                                linear: true,
 | 
			
		||||
                                easing: () => 1
 | 
			
		||||
                            }
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
		if (options.basemap !== $currentBasemap && allowedEmbeddingBasemaps.includes(options.basemap)) {
 | 
			
		||||
			$currentBasemap = options.basemap;
 | 
			
		||||
		}
 | 
			
		||||
        if (
 | 
			
		||||
            options.basemap !== $currentBasemap &&
 | 
			
		||||
            allowedEmbeddingBasemaps.includes(options.basemap)
 | 
			
		||||
        ) {
 | 
			
		||||
            $currentBasemap = options.basemap;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		if (options.distanceMarkers !== $distanceMarkers) {
 | 
			
		||||
			$distanceMarkers = options.distanceMarkers;
 | 
			
		||||
		}
 | 
			
		||||
        if (options.distanceMarkers !== $distanceMarkers) {
 | 
			
		||||
            $distanceMarkers = options.distanceMarkers;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		if (options.directionMarkers !== $directionMarkers) {
 | 
			
		||||
			$directionMarkers = options.directionMarkers;
 | 
			
		||||
		}
 | 
			
		||||
        if (options.directionMarkers !== $directionMarkers) {
 | 
			
		||||
            $directionMarkers = options.directionMarkers;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		if (options.distanceUnits !== $distanceUnits) {
 | 
			
		||||
			$distanceUnits = options.distanceUnits;
 | 
			
		||||
		}
 | 
			
		||||
        if (options.distanceUnits !== $distanceUnits) {
 | 
			
		||||
            $distanceUnits = options.distanceUnits;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		if (options.velocityUnits !== $velocityUnits) {
 | 
			
		||||
			$velocityUnits = options.velocityUnits;
 | 
			
		||||
		}
 | 
			
		||||
        if (options.velocityUnits !== $velocityUnits) {
 | 
			
		||||
            $velocityUnits = options.velocityUnits;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		if (options.temperatureUnits !== $temperatureUnits) {
 | 
			
		||||
			$temperatureUnits = options.temperatureUnits;
 | 
			
		||||
		}
 | 
			
		||||
        if (options.temperatureUnits !== $temperatureUnits) {
 | 
			
		||||
            $temperatureUnits = options.temperatureUnits;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		if (options.theme !== $mode) {
 | 
			
		||||
			setMode(options.theme);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
        if (options.theme !== $mode) {
 | 
			
		||||
            setMode(options.theme);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		prevSettings.distanceMarkers = $distanceMarkers;
 | 
			
		||||
		prevSettings.directionMarkers = $directionMarkers;
 | 
			
		||||
		prevSettings.distanceUnits = $distanceUnits;
 | 
			
		||||
		prevSettings.velocityUnits = $velocityUnits;
 | 
			
		||||
		prevSettings.temperatureUnits = $temperatureUnits;
 | 
			
		||||
		prevSettings.theme = $mode ?? 'system';
 | 
			
		||||
	});
 | 
			
		||||
    onMount(() => {
 | 
			
		||||
        prevSettings.distanceMarkers = $distanceMarkers;
 | 
			
		||||
        prevSettings.directionMarkers = $directionMarkers;
 | 
			
		||||
        prevSettings.distanceUnits = $distanceUnits;
 | 
			
		||||
        prevSettings.velocityUnits = $velocityUnits;
 | 
			
		||||
        prevSettings.temperatureUnits = $temperatureUnits;
 | 
			
		||||
        prevSettings.theme = $mode ?? 'system';
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
	$: if (options) {
 | 
			
		||||
		applyOptions();
 | 
			
		||||
	}
 | 
			
		||||
    $: if (options) {
 | 
			
		||||
        applyOptions();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	$: if ($fileOrder) {
 | 
			
		||||
		updateGPXData();
 | 
			
		||||
	}
 | 
			
		||||
    $: if ($fileOrder) {
 | 
			
		||||
        updateGPXData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	onDestroy(() => {
 | 
			
		||||
		if ($distanceMarkers !== prevSettings.distanceMarkers) {
 | 
			
		||||
			$distanceMarkers = prevSettings.distanceMarkers;
 | 
			
		||||
		}
 | 
			
		||||
    onDestroy(() => {
 | 
			
		||||
        if ($distanceMarkers !== prevSettings.distanceMarkers) {
 | 
			
		||||
            $distanceMarkers = prevSettings.distanceMarkers;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		if ($directionMarkers !== prevSettings.directionMarkers) {
 | 
			
		||||
			$directionMarkers = prevSettings.directionMarkers;
 | 
			
		||||
		}
 | 
			
		||||
        if ($directionMarkers !== prevSettings.directionMarkers) {
 | 
			
		||||
            $directionMarkers = prevSettings.directionMarkers;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		if ($distanceUnits !== prevSettings.distanceUnits) {
 | 
			
		||||
			$distanceUnits = prevSettings.distanceUnits;
 | 
			
		||||
		}
 | 
			
		||||
        if ($distanceUnits !== prevSettings.distanceUnits) {
 | 
			
		||||
            $distanceUnits = prevSettings.distanceUnits;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		if ($velocityUnits !== prevSettings.velocityUnits) {
 | 
			
		||||
			$velocityUnits = prevSettings.velocityUnits;
 | 
			
		||||
		}
 | 
			
		||||
        if ($velocityUnits !== prevSettings.velocityUnits) {
 | 
			
		||||
            $velocityUnits = prevSettings.velocityUnits;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		if ($temperatureUnits !== prevSettings.temperatureUnits) {
 | 
			
		||||
			$temperatureUnits = prevSettings.temperatureUnits;
 | 
			
		||||
		}
 | 
			
		||||
        if ($temperatureUnits !== prevSettings.temperatureUnits) {
 | 
			
		||||
            $temperatureUnits = prevSettings.temperatureUnits;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		if ($mode !== prevSettings.theme) {
 | 
			
		||||
			setMode(prevSettings.theme);
 | 
			
		||||
		}
 | 
			
		||||
        if ($mode !== prevSettings.theme) {
 | 
			
		||||
            setMode(prevSettings.theme);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		$selection.clear();
 | 
			
		||||
		$fileObservers.clear();
 | 
			
		||||
		$fileOrder = $fileOrder.filter((id) => !id.includes('embed'));
 | 
			
		||||
	});
 | 
			
		||||
        $selection.clear();
 | 
			
		||||
        $fileObservers.clear();
 | 
			
		||||
        $fileOrder = $fileOrder.filter((id) => !id.includes('embed'));
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="absolute flex flex-col h-full w-full border rounded-xl overflow-clip">
 | 
			
		||||
	<div class="grow relative">
 | 
			
		||||
		<Map
 | 
			
		||||
			class="h-full {$fileObservers.size > 1 ? 'horizontal' : ''}"
 | 
			
		||||
			accessToken={options.token}
 | 
			
		||||
			geocoder={false}
 | 
			
		||||
			geolocate={false}
 | 
			
		||||
			hash={useHash}
 | 
			
		||||
		/>
 | 
			
		||||
		<OpenIn bind:files={options.files} />
 | 
			
		||||
		<LayerControl />
 | 
			
		||||
		<GPXLayers />
 | 
			
		||||
		{#if $fileObservers.size > 1}
 | 
			
		||||
			<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
 | 
			
		||||
				<FileList orientation="horizontal" />
 | 
			
		||||
			</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</div>
 | 
			
		||||
	<div
 | 
			
		||||
		class="{options.elevation.show ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"
 | 
			
		||||
		style={options.elevation.show ? `height: ${options.elevation.height}px` : ''}
 | 
			
		||||
	>
 | 
			
		||||
		<GPXStatistics
 | 
			
		||||
			{gpxStatistics}
 | 
			
		||||
			{slicedGPXStatistics}
 | 
			
		||||
			panelSize={options.elevation.height}
 | 
			
		||||
			orientation={options.elevation.show ? 'vertical' : 'horizontal'}
 | 
			
		||||
		/>
 | 
			
		||||
		{#if options.elevation.show}
 | 
			
		||||
			<ElevationProfile
 | 
			
		||||
				{gpxStatistics}
 | 
			
		||||
				{slicedGPXStatistics}
 | 
			
		||||
				additionalDatasets={[
 | 
			
		||||
					options.elevation.speed ? 'speed' : null,
 | 
			
		||||
					options.elevation.hr ? 'hr' : null,
 | 
			
		||||
					options.elevation.cad ? 'cad' : null,
 | 
			
		||||
					options.elevation.temp ? 'temp' : null,
 | 
			
		||||
					options.elevation.power ? 'power' : null
 | 
			
		||||
				].filter((dataset) => dataset !== null)}
 | 
			
		||||
				elevationFill={options.elevation.fill}
 | 
			
		||||
				panelSize={options.elevation.height}
 | 
			
		||||
				showControls={options.elevation.controls}
 | 
			
		||||
				class="py-2"
 | 
			
		||||
			/>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</div>
 | 
			
		||||
    <div class="grow relative">
 | 
			
		||||
        <Map
 | 
			
		||||
            class="h-full {$fileObservers.size > 1 ? 'horizontal' : ''}"
 | 
			
		||||
            accessToken={options.token}
 | 
			
		||||
            geocoder={false}
 | 
			
		||||
            geolocate={false}
 | 
			
		||||
            hash={useHash}
 | 
			
		||||
        />
 | 
			
		||||
        <OpenIn bind:files={options.files} bind:ids={options.ids} />
 | 
			
		||||
        <LayerControl />
 | 
			
		||||
        <GPXLayers />
 | 
			
		||||
        {#if $fileObservers.size > 1}
 | 
			
		||||
            <div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
 | 
			
		||||
                <FileList orientation="horizontal" />
 | 
			
		||||
            </div>
 | 
			
		||||
        {/if}
 | 
			
		||||
    </div>
 | 
			
		||||
    <div
 | 
			
		||||
        class="{options.elevation.show ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"
 | 
			
		||||
        style={options.elevation.show ? `height: ${options.elevation.height}px` : ''}
 | 
			
		||||
    >
 | 
			
		||||
        <GPXStatistics
 | 
			
		||||
            {gpxStatistics}
 | 
			
		||||
            {slicedGPXStatistics}
 | 
			
		||||
            panelSize={options.elevation.height}
 | 
			
		||||
            orientation={options.elevation.show ? 'vertical' : 'horizontal'}
 | 
			
		||||
        />
 | 
			
		||||
        {#if options.elevation.show}
 | 
			
		||||
            <ElevationProfile
 | 
			
		||||
                {gpxStatistics}
 | 
			
		||||
                {slicedGPXStatistics}
 | 
			
		||||
                additionalDatasets={[
 | 
			
		||||
                    options.elevation.speed ? 'speed' : null,
 | 
			
		||||
                    options.elevation.hr ? 'hr' : null,
 | 
			
		||||
                    options.elevation.cad ? 'cad' : null,
 | 
			
		||||
                    options.elevation.temp ? 'temp' : null,
 | 
			
		||||
                    options.elevation.power ? 'power' : null
 | 
			
		||||
                ].filter((dataset) => dataset !== null)}
 | 
			
		||||
                elevationFill={options.elevation.fill}
 | 
			
		||||
                panelSize={options.elevation.height}
 | 
			
		||||
                showControls={options.elevation.controls}
 | 
			
		||||
                class="py-2"
 | 
			
		||||
            />
 | 
			
		||||
        {/if}
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,140 +2,147 @@ import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
 | 
			
		||||
import { basemaps } from '$lib/assets/layers';
 | 
			
		||||
 | 
			
		||||
export type EmbeddingOptions = {
 | 
			
		||||
	token: string;
 | 
			
		||||
	files: string[];
 | 
			
		||||
	basemap: string;
 | 
			
		||||
	elevation: {
 | 
			
		||||
		show: boolean;
 | 
			
		||||
		height: number;
 | 
			
		||||
		controls: boolean;
 | 
			
		||||
		fill: 'slope' | 'surface' | undefined;
 | 
			
		||||
		speed: boolean;
 | 
			
		||||
		hr: boolean;
 | 
			
		||||
		cad: boolean;
 | 
			
		||||
		temp: boolean;
 | 
			
		||||
		power: boolean;
 | 
			
		||||
	};
 | 
			
		||||
	distanceMarkers: boolean;
 | 
			
		||||
	directionMarkers: boolean;
 | 
			
		||||
	distanceUnits: 'metric' | 'imperial' | 'nautical';
 | 
			
		||||
	velocityUnits: 'speed' | 'pace';
 | 
			
		||||
	temperatureUnits: 'celsius' | 'fahrenheit';
 | 
			
		||||
	theme: 'system' | 'light' | 'dark';
 | 
			
		||||
    token: string;
 | 
			
		||||
    files: string[];
 | 
			
		||||
    ids: string[];
 | 
			
		||||
    basemap: string;
 | 
			
		||||
    elevation: {
 | 
			
		||||
        show: boolean;
 | 
			
		||||
        height: number;
 | 
			
		||||
        controls: boolean;
 | 
			
		||||
        fill: 'slope' | 'surface' | undefined;
 | 
			
		||||
        speed: boolean;
 | 
			
		||||
        hr: boolean;
 | 
			
		||||
        cad: boolean;
 | 
			
		||||
        temp: boolean;
 | 
			
		||||
        power: boolean;
 | 
			
		||||
    };
 | 
			
		||||
    distanceMarkers: boolean;
 | 
			
		||||
    directionMarkers: boolean;
 | 
			
		||||
    distanceUnits: 'metric' | 'imperial' | 'nautical';
 | 
			
		||||
    velocityUnits: 'speed' | 'pace';
 | 
			
		||||
    temperatureUnits: 'celsius' | 'fahrenheit';
 | 
			
		||||
    theme: 'system' | 'light' | 'dark';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const defaultEmbeddingOptions = {
 | 
			
		||||
	token: '',
 | 
			
		||||
	files: [],
 | 
			
		||||
	basemap: 'mapboxOutdoors',
 | 
			
		||||
	elevation: {
 | 
			
		||||
		show: true,
 | 
			
		||||
		height: 170,
 | 
			
		||||
		controls: true,
 | 
			
		||||
		fill: undefined,
 | 
			
		||||
		speed: false,
 | 
			
		||||
		hr: false,
 | 
			
		||||
		cad: false,
 | 
			
		||||
		temp: false,
 | 
			
		||||
		power: false
 | 
			
		||||
	},
 | 
			
		||||
	distanceMarkers: false,
 | 
			
		||||
	directionMarkers: false,
 | 
			
		||||
	distanceUnits: 'metric',
 | 
			
		||||
	velocityUnits: 'speed',
 | 
			
		||||
	temperatureUnits: 'celsius',
 | 
			
		||||
	theme: 'system'
 | 
			
		||||
    token: '',
 | 
			
		||||
    files: [],
 | 
			
		||||
    ids: [],
 | 
			
		||||
    basemap: 'mapboxOutdoors',
 | 
			
		||||
    elevation: {
 | 
			
		||||
        show: true,
 | 
			
		||||
        height: 170,
 | 
			
		||||
        controls: true,
 | 
			
		||||
        fill: undefined,
 | 
			
		||||
        speed: false,
 | 
			
		||||
        hr: false,
 | 
			
		||||
        cad: false,
 | 
			
		||||
        temp: false,
 | 
			
		||||
        power: false
 | 
			
		||||
    },
 | 
			
		||||
    distanceMarkers: false,
 | 
			
		||||
    directionMarkers: false,
 | 
			
		||||
    distanceUnits: 'metric',
 | 
			
		||||
    velocityUnits: 'speed',
 | 
			
		||||
    temperatureUnits: 'celsius',
 | 
			
		||||
    theme: 'system'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function getDefaultEmbeddingOptions(): EmbeddingOptions {
 | 
			
		||||
	return JSON.parse(JSON.stringify(defaultEmbeddingOptions));
 | 
			
		||||
    return JSON.parse(JSON.stringify(defaultEmbeddingOptions));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getMergedEmbeddingOptions(
 | 
			
		||||
	options: any,
 | 
			
		||||
	defaultOptions: any = defaultEmbeddingOptions
 | 
			
		||||
    options: any,
 | 
			
		||||
    defaultOptions: any = defaultEmbeddingOptions
 | 
			
		||||
): EmbeddingOptions {
 | 
			
		||||
	const mergedOptions = JSON.parse(JSON.stringify(defaultOptions));
 | 
			
		||||
	for (const key in options) {
 | 
			
		||||
		if (typeof options[key] === 'object' && options[key] !== null && !Array.isArray(options[key])) {
 | 
			
		||||
			mergedOptions[key] = getMergedEmbeddingOptions(options[key], defaultOptions[key]);
 | 
			
		||||
		} else {
 | 
			
		||||
			mergedOptions[key] = options[key];
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return mergedOptions;
 | 
			
		||||
    const mergedOptions = JSON.parse(JSON.stringify(defaultOptions));
 | 
			
		||||
    for (const key in options) {
 | 
			
		||||
        if (typeof options[key] === 'object' && options[key] !== null && !Array.isArray(options[key])) {
 | 
			
		||||
            mergedOptions[key] = getMergedEmbeddingOptions(options[key], defaultOptions[key]);
 | 
			
		||||
        } else {
 | 
			
		||||
            mergedOptions[key] = options[key];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return mergedOptions;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getCleanedEmbeddingOptions(
 | 
			
		||||
	options: any,
 | 
			
		||||
	defaultOptions: any = defaultEmbeddingOptions
 | 
			
		||||
    options: any,
 | 
			
		||||
    defaultOptions: any = defaultEmbeddingOptions
 | 
			
		||||
): any {
 | 
			
		||||
	const cleanedOptions = JSON.parse(JSON.stringify(options));
 | 
			
		||||
	for (const key in cleanedOptions) {
 | 
			
		||||
		if (
 | 
			
		||||
			typeof cleanedOptions[key] === 'object' &&
 | 
			
		||||
			cleanedOptions[key] !== null &&
 | 
			
		||||
			!Array.isArray(cleanedOptions[key])
 | 
			
		||||
		) {
 | 
			
		||||
			cleanedOptions[key] = getCleanedEmbeddingOptions(cleanedOptions[key], defaultOptions[key]);
 | 
			
		||||
			if (Object.keys(cleanedOptions[key]).length === 0) {
 | 
			
		||||
				delete cleanedOptions[key];
 | 
			
		||||
			}
 | 
			
		||||
		} else if (JSON.stringify(cleanedOptions[key]) === JSON.stringify(defaultOptions[key])) {
 | 
			
		||||
			delete cleanedOptions[key];
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return cleanedOptions;
 | 
			
		||||
    const cleanedOptions = JSON.parse(JSON.stringify(options));
 | 
			
		||||
    for (const key in cleanedOptions) {
 | 
			
		||||
        if (
 | 
			
		||||
            typeof cleanedOptions[key] === 'object' &&
 | 
			
		||||
            cleanedOptions[key] !== null &&
 | 
			
		||||
            !Array.isArray(cleanedOptions[key])
 | 
			
		||||
        ) {
 | 
			
		||||
            cleanedOptions[key] = getCleanedEmbeddingOptions(cleanedOptions[key], defaultOptions[key]);
 | 
			
		||||
            if (Object.keys(cleanedOptions[key]).length === 0) {
 | 
			
		||||
                delete cleanedOptions[key];
 | 
			
		||||
            }
 | 
			
		||||
        } else if (JSON.stringify(cleanedOptions[key]) === JSON.stringify(defaultOptions[key])) {
 | 
			
		||||
            delete cleanedOptions[key];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return cleanedOptions;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const allowedEmbeddingBasemaps = Object.keys(basemaps).filter(
 | 
			
		||||
	(basemap) => !['ordnanceSurvey'].includes(basemap)
 | 
			
		||||
    (basemap) => !['ordnanceSurvey'].includes(basemap)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export function getFilesFromEmbeddingOptions(options: EmbeddingOptions): string[] {
 | 
			
		||||
    return options.files.concat(options.ids.map((id) => getURLForGoogleDriveFile(id)));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getURLForGoogleDriveFile(fileId: string): string {
 | 
			
		||||
	return `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media&key=AIzaSyA2ZadQob_hXiT2VaYIkAyafPvz_4ZMssk`;
 | 
			
		||||
    return `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media&key=AIzaSyA2ZadQob_hXiT2VaYIkAyafPvz_4ZMssk`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function convertOldEmbeddingOptions(options: URLSearchParams): any {
 | 
			
		||||
	let newOptions: any = {
 | 
			
		||||
		token: PUBLIC_MAPBOX_TOKEN,
 | 
			
		||||
		files: []
 | 
			
		||||
	};
 | 
			
		||||
	if (options.has('state')) {
 | 
			
		||||
		let state = JSON.parse(options.get('state')!);
 | 
			
		||||
		if (state.ids) {
 | 
			
		||||
			newOptions.files.push(...state.ids.map(getURLForGoogleDriveFile));
 | 
			
		||||
		}
 | 
			
		||||
		if (state.urls) {
 | 
			
		||||
			newOptions.files.push(...state.urls);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if (options.has('source')) {
 | 
			
		||||
		let basemap = options.get('source')!;
 | 
			
		||||
		if (basemap === 'satellite') {
 | 
			
		||||
			newOptions.basemap = 'mapboxSatellite';
 | 
			
		||||
		} else if (basemap === 'otm') {
 | 
			
		||||
			newOptions.basemap = 'openTopoMap';
 | 
			
		||||
		} else if (basemap === 'ohm') {
 | 
			
		||||
			newOptions.basemap = 'openHikingMap';
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if (options.has('imperial')) {
 | 
			
		||||
		newOptions.distanceUnits = 'imperial';
 | 
			
		||||
	}
 | 
			
		||||
	if (options.has('running')) {
 | 
			
		||||
		newOptions.velocityUnits = 'pace';
 | 
			
		||||
	}
 | 
			
		||||
	if (options.has('distance')) {
 | 
			
		||||
		newOptions.distanceMarkers = true;
 | 
			
		||||
	}
 | 
			
		||||
	if (options.has('direction')) {
 | 
			
		||||
		newOptions.directionMarkers = true;
 | 
			
		||||
	}
 | 
			
		||||
	if (options.has('slope')) {
 | 
			
		||||
		newOptions.elevation = {
 | 
			
		||||
			fill: 'slope'
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
	return newOptions;
 | 
			
		||||
    let newOptions: any = {
 | 
			
		||||
        token: PUBLIC_MAPBOX_TOKEN,
 | 
			
		||||
        files: [],
 | 
			
		||||
        ids: [],
 | 
			
		||||
    };
 | 
			
		||||
    if (options.has('state')) {
 | 
			
		||||
        let state = JSON.parse(options.get('state')!);
 | 
			
		||||
        if (state.ids) {
 | 
			
		||||
            newOptions.ids.push(...state.ids);
 | 
			
		||||
        }
 | 
			
		||||
        if (state.urls) {
 | 
			
		||||
            newOptions.files.push(...state.urls);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (options.has('source')) {
 | 
			
		||||
        let basemap = options.get('source')!;
 | 
			
		||||
        if (basemap === 'satellite') {
 | 
			
		||||
            newOptions.basemap = 'mapboxSatellite';
 | 
			
		||||
        } else if (basemap === 'otm') {
 | 
			
		||||
            newOptions.basemap = 'openTopoMap';
 | 
			
		||||
        } else if (basemap === 'ohm') {
 | 
			
		||||
            newOptions.basemap = 'openHikingMap';
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (options.has('imperial')) {
 | 
			
		||||
        newOptions.distanceUnits = 'imperial';
 | 
			
		||||
    }
 | 
			
		||||
    if (options.has('running')) {
 | 
			
		||||
        newOptions.velocityUnits = 'pace';
 | 
			
		||||
    }
 | 
			
		||||
    if (options.has('distance')) {
 | 
			
		||||
        newOptions.distanceMarkers = true;
 | 
			
		||||
    }
 | 
			
		||||
    if (options.has('direction')) {
 | 
			
		||||
        newOptions.directionMarkers = true;
 | 
			
		||||
    }
 | 
			
		||||
    if (options.has('slope')) {
 | 
			
		||||
        newOptions.elevation = {
 | 
			
		||||
            fill: 'slope'
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    return newOptions;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,15 +35,21 @@
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    let files = options.files[0];
 | 
			
		||||
    let driveIds = '';
 | 
			
		||||
    $: if (files || driveIds) {
 | 
			
		||||
    $: {
 | 
			
		||||
        let urls = files.split(',');
 | 
			
		||||
        urls = urls.filter((url) => url.length > 0);
 | 
			
		||||
        let ids = driveIds.split(',');
 | 
			
		||||
        ids = ids.filter((id) => id.length > 0);
 | 
			
		||||
        urls.push(...ids.map(getURLForGoogleDriveFile));
 | 
			
		||||
        if (JSON.stringify(urls) !== JSON.stringify(options.files)) {
 | 
			
		||||
            options.files = urls;
 | 
			
		||||
            console.log(options.files);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    let driveIds = '';
 | 
			
		||||
    $: {
 | 
			
		||||
        let ids = driveIds.split(',');
 | 
			
		||||
        ids = ids.filter((id) => id.length > 0);
 | 
			
		||||
        if (JSON.stringify(ids) !== JSON.stringify(options.ids)) {
 | 
			
		||||
            options.ids = ids;
 | 
			
		||||
            console.log(options.ids);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,23 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { Button } from '$lib/components/ui/button';
 | 
			
		||||
	import Logo from '$lib/components/Logo.svelte';
 | 
			
		||||
	import { getURLForLanguage } from '$lib/utils';
 | 
			
		||||
	import { _, locale } from 'svelte-i18n';
 | 
			
		||||
    import { Button } from '$lib/components/ui/button';
 | 
			
		||||
    import Logo from '$lib/components/Logo.svelte';
 | 
			
		||||
    import { getURLForLanguage } from '$lib/utils';
 | 
			
		||||
    import { _, locale } from 'svelte-i18n';
 | 
			
		||||
 | 
			
		||||
	export let files: string[];
 | 
			
		||||
    export let files: string[];
 | 
			
		||||
    export let ids: string[];
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Button
 | 
			
		||||
	variant="ghost"
 | 
			
		||||
	class="absolute top-0 flex-wrap h-fit bg-background font-semibold rounded-md py-1 px-2 gap-1.5 xs:text-base mt-2.5 ml-2.5 mr-12"
 | 
			
		||||
	href="{getURLForLanguage($locale, '/app')}?files={encodeURIComponent(JSON.stringify(files))}"
 | 
			
		||||
	target="_blank"
 | 
			
		||||
    variant="ghost"
 | 
			
		||||
    class="absolute top-0 flex-wrap h-fit bg-background font-semibold rounded-md py-1 px-2 gap-1.5 xs:text-base mt-2.5 ml-2.5 mr-12"
 | 
			
		||||
    href="{getURLForLanguage($locale, '/app')}?{files.length > 0
 | 
			
		||||
        ? `files=${encodeURIComponent(JSON.stringify(files))}`
 | 
			
		||||
        : ''}{files.length > 0 && ids.length > 0 ? '&' : ''}{ids.length > 0
 | 
			
		||||
        ? `ids=${encodeURIComponent(JSON.stringify(ids))}`
 | 
			
		||||
        : ''}"
 | 
			
		||||
    target="_blank"
 | 
			
		||||
>
 | 
			
		||||
	{$_('menu.open_in')}
 | 
			
		||||
	<Logo class="h-[18px] xs:h-5 translate-y-[1px]" />
 | 
			
		||||
    {$_('menu.open_in')}
 | 
			
		||||
    <Logo class="h-[18px] xs:h-5 translate-y-[1px]" />
 | 
			
		||||
</Button>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@
 | 
			
		||||
    import { page } from '$app/stores';
 | 
			
		||||
    import { languages } from '$lib/languages';
 | 
			
		||||
    import { getURLForLanguage } from '$lib/utils';
 | 
			
		||||
    import { getURLForGoogleDriveFile } from '$lib/components/embedding/Embedding';
 | 
			
		||||
 | 
			
		||||
    const {
 | 
			
		||||
        verticalFileView,
 | 
			
		||||
@@ -30,11 +31,13 @@
 | 
			
		||||
    onMount(() => {
 | 
			
		||||
        observeFilesFromDatabase();
 | 
			
		||||
 | 
			
		||||
        let files = JSON.parse($page.url.searchParams.get('files') || '[]');
 | 
			
		||||
        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));
 | 
			
		||||
 | 
			
		||||
        if (files.length > 0) {
 | 
			
		||||
        if (urls.length > 0) {
 | 
			
		||||
            let downloads: Promise<File | null>[] = [];
 | 
			
		||||
            files.forEach((url) => {
 | 
			
		||||
            urls.forEach((url) => {
 | 
			
		||||
                downloads.push(
 | 
			
		||||
                    fetch(url)
 | 
			
		||||
                        .then((response) => response.blob())
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user