mirror of
				https://github.com/gpxstudio/gpx.studio.git
				synced 2025-11-04 05:21:09 +00:00 
			
		
		
		
	embedding progress
This commit is contained in:
		
							
								
								
									
										1
									
								
								.github/workflows/deploy.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/deploy.yml
									
									
									
									
										vendored
									
									
								
							@@ -31,6 +31,7 @@ jobs:
 | 
			
		||||
      - name: Build website
 | 
			
		||||
        env:
 | 
			
		||||
          BASE_PATH: '/${{ github.event.repository.name }}'
 | 
			
		||||
          PUBLIC_MAPBOX_TOKEN: ${{ secrets.PUBLIC_MAPBOX_TOKEN }}
 | 
			
		||||
        run: |
 | 
			
		||||
          npm run build --prefix website
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								website/.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								website/.env.example
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
PUBLIC_MAPBOX_TOKEN=YOUR_MAPBOX_TOKEN
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
 | 
			
		||||
import { type AnySourceData, type Style } from 'mapbox-gl';
 | 
			
		||||
 | 
			
		||||
export const mapboxAccessToken = 'pk.eyJ1IjoidmNvcHBlIiwiYSI6ImNseG0zNHpwdTA1NXUycXF4ejJyODc0NWQifQ.4tiCPQ1SxnYl4o7aQc89VA';
 | 
			
		||||
 | 
			
		||||
export const basemaps: { [key: string]: string | Style; } = {
 | 
			
		||||
    mapboxOutdoors: 'mapbox://styles/mapbox/outdoors-v12',
 | 
			
		||||
    mapboxSatellite: 'mapbox://styles/mapbox/satellite-streets-v12',
 | 
			
		||||
@@ -269,7 +268,7 @@ export const basemaps: { [key: string]: string | Style; } = {
 | 
			
		||||
export function extendBasemap(basemap: string | Style): string | Style {
 | 
			
		||||
    if (typeof basemap === 'object') {
 | 
			
		||||
        basemap["glyphs"] = "mapbox://fonts/mapbox/{fontstack}/{range}.pbf";
 | 
			
		||||
        basemap["sprite"] = `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${mapboxAccessToken}`;
 | 
			
		||||
        basemap["sprite"] = `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}`;
 | 
			
		||||
    }
 | 
			
		||||
    return basemap;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,12 +11,13 @@
 | 
			
		||||
	import { settings } from '$lib/db';
 | 
			
		||||
	import { locale } from 'svelte-i18n';
 | 
			
		||||
	import { get } from 'svelte/store';
 | 
			
		||||
	import { mapboxAccessToken } from '$lib/assets/layers';
 | 
			
		||||
 | 
			
		||||
	mapboxgl.accessToken = mapboxAccessToken;
 | 
			
		||||
	import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
 | 
			
		||||
 | 
			
		||||
	export let accessToken = PUBLIC_MAPBOX_TOKEN;
 | 
			
		||||
	export let geocoder = true;
 | 
			
		||||
 | 
			
		||||
	mapboxgl.accessToken = accessToken;
 | 
			
		||||
 | 
			
		||||
	let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
 | 
			
		||||
		maxZoom: 15,
 | 
			
		||||
		linear: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
	import { getURLForLanguage } from '$lib/utils';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<nav class="w-full sticky top-0 bg-background z-10">
 | 
			
		||||
<nav class="w-full sticky top-0 bg-background z-50">
 | 
			
		||||
	<div class="mx-6 py-2 flex flex-row items-center border-b gap-4 sm:gap-8">
 | 
			
		||||
		<a href={getURLForLanguage($locale, '/')} class="shrink-0 translate-y-0.5">
 | 
			
		||||
			<Logo class="h-8 sm:hidden" iconOnly={true} />
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,12 @@
 | 
			
		||||
		@apply px-1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:global(.markdown a) {
 | 
			
		||||
	:global(.markdown > a) {
 | 
			
		||||
		@apply text-blue-500;
 | 
			
		||||
		@apply hover:underline;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:global(.markdown p > a) {
 | 
			
		||||
		@apply text-blue-500;
 | 
			
		||||
		@apply hover:underline;
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										205
									
								
								website/src/lib/components/embedding/Embedding.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								website/src/lib/components/embedding/Embedding.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,205 @@
 | 
			
		||||
<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 MapComponent from '$lib/components/Map.svelte';
 | 
			
		||||
	import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
 | 
			
		||||
	import OpenIn from '$lib/components/OpenIn.svelte';
 | 
			
		||||
 | 
			
		||||
	import { gpxStatistics, slicedGPXStatistics, embedding, loadFile, map } 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 { languages } from '$lib/languages';
 | 
			
		||||
	import { getURLForLanguage } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
	$embedding = true;
 | 
			
		||||
 | 
			
		||||
	const { currentBasemap, distanceUnits, velocityUnits, temperatureUnits } = settings;
 | 
			
		||||
 | 
			
		||||
	export let options: EmbeddingOptions;
 | 
			
		||||
 | 
			
		||||
	let prevUnits = {
 | 
			
		||||
		distance: '',
 | 
			
		||||
		velocity: '',
 | 
			
		||||
		temperature: ''
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
			);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		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;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					let id = `gpx-${index}`;
 | 
			
		||||
					file._data.id = id;
 | 
			
		||||
					let statistics = new GPXStatisticsTree(file);
 | 
			
		||||
 | 
			
		||||
					$fileObservers.set(
 | 
			
		||||
						id,
 | 
			
		||||
						readable({
 | 
			
		||||
							file,
 | 
			
		||||
							statistics
 | 
			
		||||
						})
 | 
			
		||||
					);
 | 
			
		||||
 | 
			
		||||
					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);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				return $fileObservers;
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			selection.update(($selection) => {
 | 
			
		||||
				$selection.clear();
 | 
			
		||||
				ids.forEach((id) => {
 | 
			
		||||
					$selection.toggle(new ListFileItem(id));
 | 
			
		||||
				});
 | 
			
		||||
				return $selection;
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			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.distanceUnits !== $distanceUnits) {
 | 
			
		||||
			$distanceUnits = options.distanceUnits;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (options.velocityUnits !== $velocityUnits) {
 | 
			
		||||
			$velocityUnits = options.velocityUnits;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (options.temperatureUnits !== $temperatureUnits) {
 | 
			
		||||
			$temperatureUnits = options.temperatureUnits;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		prevUnits.distance = $distanceUnits;
 | 
			
		||||
		prevUnits.velocity = $velocityUnits;
 | 
			
		||||
		prevUnits.temperature = $temperatureUnits;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	$: if (options) {
 | 
			
		||||
		applyOptions();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onDestroy(() => {
 | 
			
		||||
		if ($distanceUnits !== prevUnits.distance) {
 | 
			
		||||
			$distanceUnits = prevUnits.distance;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if ($velocityUnits !== prevUnits.velocity) {
 | 
			
		||||
			$velocityUnits = prevUnits.velocity;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if ($temperatureUnits !== prevUnits.temperature) {
 | 
			
		||||
			$temperatureUnits = prevUnits.temperature;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="absolute flex flex-col h-full w-full border rounded-xl overflow-clip">
 | 
			
		||||
	<div class="grow relative">
 | 
			
		||||
		<MapComponent
 | 
			
		||||
			class="h-full {$fileObservers.size > 1 ? 'horizontal' : ''}"
 | 
			
		||||
			accessToken={options.token}
 | 
			
		||||
			geocoder={false}
 | 
			
		||||
		/>
 | 
			
		||||
		<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>
 | 
			
		||||
							
								
								
									
										63
									
								
								website/src/lib/components/embedding/Embedding.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								website/src/lib/components/embedding/Embedding.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
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,
 | 
			
		||||
    },
 | 
			
		||||
    distanceUnits: 'metric' | 'imperial',
 | 
			
		||||
    velocityUnits: 'speed' | 'pace',
 | 
			
		||||
    temperatureUnits: 'celsius' | 'fahrenheit',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
    },
 | 
			
		||||
    distanceUnits: 'metric',
 | 
			
		||||
    velocityUnits: 'speed',
 | 
			
		||||
    temperatureUnits: 'celsius',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function getDefaultEmbeddingOptions(): EmbeddingOptions {
 | 
			
		||||
    return JSON.parse(JSON.stringify(defaultEmbeddingOptions));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getCleanedEmbeddingOptions(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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const allowedEmbeddingBasemaps = [
 | 
			
		||||
    'mapboxOutdoors',
 | 
			
		||||
    'mapboxSatellite',
 | 
			
		||||
];
 | 
			
		||||
@@ -5,42 +5,32 @@
 | 
			
		||||
	import * as Select from '$lib/components/ui/select';
 | 
			
		||||
	import { Checkbox } from '$lib/components/ui/checkbox';
 | 
			
		||||
	import * as RadioGroup from '$lib/components/ui/radio-group';
 | 
			
		||||
	import { basemaps } from '$lib/assets/layers';
 | 
			
		||||
	import { Zap, HeartPulse, Orbit, Thermometer, SquareActivity } from 'lucide-svelte';
 | 
			
		||||
	import { _ } from 'svelte-i18n';
 | 
			
		||||
	import {
 | 
			
		||||
		allowedEmbeddingBasemaps,
 | 
			
		||||
		getCleanedEmbeddingOptions,
 | 
			
		||||
		getDefaultEmbeddingOptions
 | 
			
		||||
	} from './Embedding';
 | 
			
		||||
	import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
 | 
			
		||||
	import Embedding from './Embedding.svelte';
 | 
			
		||||
	import { map } from '$lib/stores';
 | 
			
		||||
	import { tick } from 'svelte';
 | 
			
		||||
	import { base } from '$app/paths';
 | 
			
		||||
 | 
			
		||||
	let options = {
 | 
			
		||||
		files: ['https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx'],
 | 
			
		||||
		basemap: 'mapboxOutdoors',
 | 
			
		||||
		elevation: {
 | 
			
		||||
			show: true,
 | 
			
		||||
			height: 170,
 | 
			
		||||
			data: [],
 | 
			
		||||
			fill: undefined,
 | 
			
		||||
			controls: true
 | 
			
		||||
		},
 | 
			
		||||
		distanceUnits: 'metric',
 | 
			
		||||
		velocityUnits: 'speed',
 | 
			
		||||
		temperatureUnits: 'celsius'
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	let files =
 | 
			
		||||
		'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx';
 | 
			
		||||
	let options = getDefaultEmbeddingOptions();
 | 
			
		||||
	options.token = 'YOUR_MAPBOX_TOKEN';
 | 
			
		||||
	options.files = [
 | 
			
		||||
		'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx'
 | 
			
		||||
	];
 | 
			
		||||
 | 
			
		||||
	let files = options.files[0];
 | 
			
		||||
	$: if (files) {
 | 
			
		||||
		options.files = files.split(',');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let additionalData = {
 | 
			
		||||
		speed: false,
 | 
			
		||||
		hr: false,
 | 
			
		||||
		cad: false,
 | 
			
		||||
		temp: false,
 | 
			
		||||
		power: false
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	$: if (additionalData) {
 | 
			
		||||
		options.elevation.data = Object.keys(additionalData).filter((key) => additionalData[key]);
 | 
			
		||||
		let urls = files.split(',');
 | 
			
		||||
		urls = urls.filter((url) => url.length > 0);
 | 
			
		||||
		if (JSON.stringify(urls) !== JSON.stringify(options.files)) {
 | 
			
		||||
			options.files = urls;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let manualCamera = false;
 | 
			
		||||
@@ -53,36 +43,47 @@
 | 
			
		||||
 | 
			
		||||
	$: hash = manualCamera ? `#${zoom}/${lat}/${lon}/${bearing}/${pitch}` : '';
 | 
			
		||||
 | 
			
		||||
	$: console.log(options);
 | 
			
		||||
</script>
 | 
			
		||||
	$: iframeOptions =
 | 
			
		||||
		options.token.length === 0 || options.token === 'YOUR_MAPBOX_TOKEN'
 | 
			
		||||
			? Object.assign({}, options, { token: PUBLIC_MAPBOX_TOKEN })
 | 
			
		||||
			: options;
 | 
			
		||||
 | 
			
		||||
<iframe
 | 
			
		||||
	src={`../../embed?options=${encodeURIComponent(JSON.stringify(options))}${hash}`}
 | 
			
		||||
	style="width: 100%; height: 600px;"
 | 
			
		||||
></iframe>
 | 
			
		||||
	async function resizeMap() {
 | 
			
		||||
		if ($map) {
 | 
			
		||||
			await tick();
 | 
			
		||||
			$map.resize();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	$: if (options.elevation.height || options.elevation.show) {
 | 
			
		||||
		resizeMap();
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Card.Root>
 | 
			
		||||
	<Card.Header>
 | 
			
		||||
		<Card.Title>Card Title</Card.Title>
 | 
			
		||||
		<Card.Description>Card Description</Card.Description>
 | 
			
		||||
		<Card.Title>{$_('embedding.title')}</Card.Title>
 | 
			
		||||
	</Card.Header>
 | 
			
		||||
	<Card.Content>
 | 
			
		||||
		<fieldset class="flex flex-col gap-3">
 | 
			
		||||
			<Label for="token">{$_('embedding.mapbox_token')}</Label>
 | 
			
		||||
			<Input id="token" type="text" class="h-8" bind:value={options.token} />
 | 
			
		||||
			<Label for="file_urls">{$_('embedding.file_urls')}</Label>
 | 
			
		||||
			<Input id="file_urls" type="text" class="h-8" bind:value={files} />
 | 
			
		||||
			<Label for="basemap">{$_('embedding.basemap')}</Label>
 | 
			
		||||
			<Select.Root
 | 
			
		||||
				selected={{ value: options.basemap, label: $_(`layers.label.${options.basemap}`) }}
 | 
			
		||||
				onSelectedChange={(selected) => {
 | 
			
		||||
					if (selected?.value) {
 | 
			
		||||
						options.basemap = selected?.value;
 | 
			
		||||
					}
 | 
			
		||||
				}}
 | 
			
		||||
			>
 | 
			
		||||
				<Select.Trigger id="basemap" class="w-[180px] h-8">
 | 
			
		||||
				<Select.Trigger id="basemap" class="w-full h-8">
 | 
			
		||||
					<Select.Value />
 | 
			
		||||
				</Select.Trigger>
 | 
			
		||||
				<Select.Content class="max-h-60 overflow-y-scroll">
 | 
			
		||||
					{#each Object.keys(basemaps) as basemap}
 | 
			
		||||
					{#each allowedEmbeddingBasemaps as basemap}
 | 
			
		||||
						<Select.Item value={basemap}>{$_(`layers.label.${basemap}`)}</Select.Item>
 | 
			
		||||
					{/each}
 | 
			
		||||
				</Select.Content>
 | 
			
		||||
@@ -102,13 +103,13 @@
 | 
			
		||||
							{$_('embedding.fill_by')}
 | 
			
		||||
						</span>
 | 
			
		||||
						<Select.Root
 | 
			
		||||
							selected={{ value: 'none', label: $_('embedding.none') }}
 | 
			
		||||
							onSelectedChange={(selected) => {
 | 
			
		||||
								if (selected?.value) {
 | 
			
		||||
									if (selected?.value === 'none') {
 | 
			
		||||
										options.elevation.fill = undefined;
 | 
			
		||||
									} else {
 | 
			
		||||
										options.elevation.fill = selected?.value;
 | 
			
		||||
									}
 | 
			
		||||
								let value = selected?.value;
 | 
			
		||||
								if (value === 'none') {
 | 
			
		||||
									options.elevation.fill = undefined;
 | 
			
		||||
								} else if (value === 'slope' || value === 'surface') {
 | 
			
		||||
									options.elevation.fill = value;
 | 
			
		||||
								}
 | 
			
		||||
							}}
 | 
			
		||||
						>
 | 
			
		||||
@@ -127,35 +128,35 @@
 | 
			
		||||
						<Label for="controls">{$_('embedding.show_controls')}</Label>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="flex flex-row items-center gap-2">
 | 
			
		||||
						<Checkbox id="show-speed" bind:checked={additionalData.speed} />
 | 
			
		||||
						<Checkbox id="show-speed" bind:checked={options.elevation.speed} />
 | 
			
		||||
						<Label for="show-speed" class="flex flex-row items-center gap-1">
 | 
			
		||||
							<Zap size="16" />
 | 
			
		||||
							{$_('chart.show_speed')}
 | 
			
		||||
						</Label>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="flex flex-row items-center gap-2">
 | 
			
		||||
						<Checkbox id="show-hr" bind:checked={additionalData.hr} />
 | 
			
		||||
						<Checkbox id="show-hr" bind:checked={options.elevation.hr} />
 | 
			
		||||
						<Label for="show-hr" class="flex flex-row items-center gap-1">
 | 
			
		||||
							<HeartPulse size="16" />
 | 
			
		||||
							{$_('chart.show_heartrate')}
 | 
			
		||||
						</Label>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="flex flex-row items-center gap-2">
 | 
			
		||||
						<Checkbox id="show-cad" bind:checked={additionalData.cad} />
 | 
			
		||||
						<Checkbox id="show-cad" bind:checked={options.elevation.cad} />
 | 
			
		||||
						<Label for="show-cad" class="flex flex-row items-center gap-1">
 | 
			
		||||
							<Orbit size="16" />
 | 
			
		||||
							{$_('chart.show_cadence')}
 | 
			
		||||
						</Label>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="flex flex-row items-center gap-2">
 | 
			
		||||
						<Checkbox id="show-temp" bind:checked={additionalData.temp} />
 | 
			
		||||
						<Checkbox id="show-temp" bind:checked={options.elevation.temp} />
 | 
			
		||||
						<Label for="show-temp" class="flex flex-row items-center gap-1">
 | 
			
		||||
							<Thermometer size="16" />
 | 
			
		||||
							{$_('chart.show_temperature')}
 | 
			
		||||
						</Label>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="flex flex-row items-center gap-2">
 | 
			
		||||
						<Checkbox id="show-power" bind:checked={additionalData.power} />
 | 
			
		||||
						<Checkbox id="show-power" bind:checked={options.elevation.power} />
 | 
			
		||||
						<Label for="show-power" class="flex flex-row items-center gap-1">
 | 
			
		||||
							<SquareActivity size="16" />
 | 
			
		||||
							{$_('chart.show_power')}
 | 
			
		||||
@@ -204,6 +205,20 @@
 | 
			
		||||
					</RadioGroup.Root>
 | 
			
		||||
				</Label>
 | 
			
		||||
			</div>
 | 
			
		||||
			<Label>
 | 
			
		||||
				{$_('embedding.preview')}
 | 
			
		||||
			</Label>
 | 
			
		||||
			<div class="relative h-[600px]">
 | 
			
		||||
				<Embedding bind:options={iframeOptions} />
 | 
			
		||||
			</div>
 | 
			
		||||
			<Label>
 | 
			
		||||
				{$_('embedding.code')}
 | 
			
		||||
			</Label>
 | 
			
		||||
			<pre class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all">
 | 
			
		||||
                <code class="language-html">
 | 
			
		||||
                    {`<iframe src="https://gpx.studio${base}/embed?${JSON.stringify(getCleanedEmbeddingOptions(options))}${hash}" width="100%" height="600px" frameborder="0" />`}
 | 
			
		||||
                </code>
 | 
			
		||||
            </pre>
 | 
			
		||||
		</fieldset>
 | 
			
		||||
	</Card.Content>
 | 
			
		||||
</Card.Root>
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { map, gpxLayers } from '$lib/stores';
 | 
			
		||||
	import { GPXLayer } from './GPXLayer';
 | 
			
		||||
	import { get } from 'svelte/store';
 | 
			
		||||
	import WaypointPopup from './WaypointPopup.svelte';
 | 
			
		||||
	import { fileObservers } from '$lib/db';
 | 
			
		||||
	import { DistanceMarkers } from './DistanceMarkers';
 | 
			
		||||
@@ -21,14 +20,14 @@
 | 
			
		||||
		// add layers for new files
 | 
			
		||||
		$fileObservers.forEach((file, fileId) => {
 | 
			
		||||
			if (!gpxLayers.has(fileId)) {
 | 
			
		||||
				gpxLayers.set(fileId, new GPXLayer(get(map), fileId, file));
 | 
			
		||||
				gpxLayers.set(fileId, new GPXLayer($map, fileId, file));
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	$: if ($map) {
 | 
			
		||||
		distanceMarkers = new DistanceMarkers(get(map));
 | 
			
		||||
		startEndMarkers = new StartEndMarkers(get(map));
 | 
			
		||||
		distanceMarkers = new DistanceMarkers($map);
 | 
			
		||||
		startEndMarkers = new StartEndMarkers($map);
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,14 @@ title: Integration
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import EmbeddingPlaygound from '$lib/components/EmbeddingPlayground.svelte';
 | 
			
		||||
    import DocsNote from '$lib/components/docs/DocsNote.svelte';
 | 
			
		||||
    import EmbeddingPlaygound from '$lib/components/embedding/EmbeddingPlayground.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
# { title }
 | 
			
		||||
 | 
			
		||||
<DocsNote>
 | 
			
		||||
    This section is a work in progress.
 | 
			
		||||
</DocsNote>
 | 
			
		||||
 | 
			
		||||
<EmbeddingPlaygound />
 | 
			
		||||
 
 | 
			
		||||
@@ -372,6 +372,8 @@
 | 
			
		||||
        "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",
 | 
			
		||||
        "mapbox_token": "Mapbox access token",
 | 
			
		||||
        "file_urls": "File URLs (separated by commas)",
 | 
			
		||||
        "basemap": "Basemap",
 | 
			
		||||
        "height": "Height",
 | 
			
		||||
@@ -384,6 +386,8 @@
 | 
			
		||||
        "latitude": "Latitude",
 | 
			
		||||
        "longitude": "Longitude",
 | 
			
		||||
        "pitch": "Pitch",
 | 
			
		||||
        "bearing": "Bearing"
 | 
			
		||||
        "bearing": "Bearing",
 | 
			
		||||
        "preview": "Preview",
 | 
			
		||||
        "code": "Integration code"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -15,6 +15,8 @@
 | 
			
		||||
	import { gpxStatistics, loadFiles, slicedGPXStatistics } from '$lib/stores';
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
	import { page } from '$app/stores';
 | 
			
		||||
	import { languages } from '$lib/languages';
 | 
			
		||||
	import { getURLForLanguage } from '$lib/utils';
 | 
			
		||||
 | 
			
		||||
	const {
 | 
			
		||||
		verticalFileView,
 | 
			
		||||
@@ -99,6 +101,15 @@
 | 
			
		||||
	{/if}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- hidden links for svelte crawling -->
 | 
			
		||||
<div class="hidden">
 | 
			
		||||
	{#each Object.entries(languages) as [lang, label]}
 | 
			
		||||
		<a href={getURLForLanguage(lang, '/embed')}>
 | 
			
		||||
			{label}
 | 
			
		||||
		</a>
 | 
			
		||||
	{/each}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style lang="postcss">
 | 
			
		||||
	div :global(.toaster.group) {
 | 
			
		||||
		@apply absolute;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,38 +1,13 @@
 | 
			
		||||
<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 MapComponent from '$lib/components/Map.svelte';
 | 
			
		||||
	import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
 | 
			
		||||
	import OpenIn from '$lib/components/OpenIn.svelte';
 | 
			
		||||
 | 
			
		||||
	import { gpxStatistics, slicedGPXStatistics, embedding, loadFile, map } from '$lib/stores';
 | 
			
		||||
	import { page } from '$app/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 { onMount } from 'svelte';
 | 
			
		||||
	import Embedding from '$lib/components/embedding/Embedding.svelte';
 | 
			
		||||
	import {
 | 
			
		||||
		getDefaultEmbeddingOptions,
 | 
			
		||||
		type EmbeddingOptions
 | 
			
		||||
	} from '$lib/components/embedding/Embedding';
 | 
			
		||||
 | 
			
		||||
	$embedding = true;
 | 
			
		||||
 | 
			
		||||
	const { currentBasemap, distanceUnits, velocityUnits, temperatureUnits } = settings;
 | 
			
		||||
 | 
			
		||||
	let elevationProfile = true;
 | 
			
		||||
	let bottomPanelSize = 170;
 | 
			
		||||
	let additionalDatasets: string[] = [];
 | 
			
		||||
	let elevationFill: 'slope' | 'surface' | undefined = undefined;
 | 
			
		||||
	let elevationControls = true;
 | 
			
		||||
 | 
			
		||||
	let files: string[] = [];
 | 
			
		||||
 | 
			
		||||
	let prevUnits = {
 | 
			
		||||
		distance: '',
 | 
			
		||||
		velocity: '',
 | 
			
		||||
		temperature: ''
 | 
			
		||||
	};
 | 
			
		||||
	let embeddingOptions: EmbeddingOptions | undefined = undefined;
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		let options = $page.url.searchParams.get('options');
 | 
			
		||||
@@ -43,189 +18,10 @@
 | 
			
		||||
		if (options === null) {
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (options.files && Array.isArray(options.files)) {
 | 
			
		||||
			files = options.files;
 | 
			
		||||
			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()))
 | 
			
		||||
						.then(loadFile)
 | 
			
		||||
				);
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			Promise.all(downloads).then((files) => {
 | 
			
		||||
				let ids: string[] = [];
 | 
			
		||||
				let bounds = {
 | 
			
		||||
					southWest: {
 | 
			
		||||
						lat: 90,
 | 
			
		||||
						lon: 180
 | 
			
		||||
					},
 | 
			
		||||
					northEast: {
 | 
			
		||||
						lat: -90,
 | 
			
		||||
						lon: -180
 | 
			
		||||
					}
 | 
			
		||||
				};
 | 
			
		||||
 | 
			
		||||
				files.forEach((file, index) => {
 | 
			
		||||
					if (file === null) {
 | 
			
		||||
						return;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					let id = `gpx-${index}`;
 | 
			
		||||
					file._data.id = id;
 | 
			
		||||
					let statistics = new GPXStatisticsTree(file);
 | 
			
		||||
 | 
			
		||||
					fileObservers.update(($fileObservers) => {
 | 
			
		||||
						$fileObservers.set(
 | 
			
		||||
							id,
 | 
			
		||||
							readable({
 | 
			
		||||
								file,
 | 
			
		||||
								statistics
 | 
			
		||||
							})
 | 
			
		||||
						);
 | 
			
		||||
						return $fileObservers;
 | 
			
		||||
					});
 | 
			
		||||
 | 
			
		||||
					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);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				selection.update(($selection) => {
 | 
			
		||||
					$selection.clear();
 | 
			
		||||
					ids.forEach((id) => {
 | 
			
		||||
						$selection.toggle(new ListFileItem(id));
 | 
			
		||||
					});
 | 
			
		||||
					return $selection;
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				if ($page.url.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 !== undefined && typeof options.basemap === 'string') {
 | 
			
		||||
			currentBasemap.set(options.basemap);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (options.elevation !== undefined && typeof options.elevation === 'object') {
 | 
			
		||||
			const elevationOptions = options.elevation;
 | 
			
		||||
			if (elevationOptions.show !== undefined && typeof elevationOptions.show === 'boolean') {
 | 
			
		||||
				elevationProfile = elevationOptions.show;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (elevationOptions.data && Array.isArray(elevationOptions.data)) {
 | 
			
		||||
				elevationOptions.data.forEach((dataset) => {
 | 
			
		||||
					if (['speed', 'hr', 'cad', 'temp', 'power'].includes(dataset)) {
 | 
			
		||||
						additionalDatasets.push(dataset);
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (elevationOptions.fill === 'slope' || elevationOptions.fill === 'surface') {
 | 
			
		||||
				elevationFill = elevationOptions.fill;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (elevationOptions.height !== undefined && typeof elevationOptions.height === 'number') {
 | 
			
		||||
				bottomPanelSize = elevationOptions.height;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (
 | 
			
		||||
				elevationOptions.controls !== undefined &&
 | 
			
		||||
				typeof elevationOptions.controls === 'boolean'
 | 
			
		||||
			) {
 | 
			
		||||
				elevationControls = elevationOptions.controls;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		prevUnits.distance = $distanceUnits;
 | 
			
		||||
		prevUnits.velocity = $velocityUnits;
 | 
			
		||||
		prevUnits.temperature = $temperatureUnits;
 | 
			
		||||
 | 
			
		||||
		if (options.distanceUnits === 'metric' || options.distanceUnits === 'imperial') {
 | 
			
		||||
			$distanceUnits = options.distanceUnits;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (options.velocityUnits === 'speed' || options.velocityUnits === 'pace') {
 | 
			
		||||
			$velocityUnits = options.velocityUnits;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (options.temperatureUnits === 'celsius' || options.temperatureUnits === 'fahrenheit') {
 | 
			
		||||
			$temperatureUnits = options.temperatureUnits;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	onDestroy(() => {
 | 
			
		||||
		if ($distanceUnits !== prevUnits.distance) {
 | 
			
		||||
			$distanceUnits = prevUnits.distance;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if ($velocityUnits !== prevUnits.velocity) {
 | 
			
		||||
			$velocityUnits = prevUnits.velocity;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if ($temperatureUnits !== prevUnits.temperature) {
 | 
			
		||||
			$temperatureUnits = prevUnits.temperature;
 | 
			
		||||
		}
 | 
			
		||||
		embeddingOptions = Object.assign(getDefaultEmbeddingOptions(), options);
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="fixed flex flex-col h-full w-full border rounded-xl overflow-clip">
 | 
			
		||||
	<div class="grow relative">
 | 
			
		||||
		<MapComponent class="h-full {$fileObservers.size > 1 ? 'horizontal' : ''}" geocoder={false} />
 | 
			
		||||
		<OpenIn bind: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="{elevationProfile ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"
 | 
			
		||||
		style={elevationProfile ? `height: ${bottomPanelSize}px` : ''}
 | 
			
		||||
	>
 | 
			
		||||
		<GPXStatistics
 | 
			
		||||
			{gpxStatistics}
 | 
			
		||||
			{slicedGPXStatistics}
 | 
			
		||||
			panelSize={bottomPanelSize}
 | 
			
		||||
			orientation={elevationProfile ? 'vertical' : 'horizontal'}
 | 
			
		||||
		/>
 | 
			
		||||
		{#if elevationProfile}
 | 
			
		||||
			<ElevationProfile
 | 
			
		||||
				{gpxStatistics}
 | 
			
		||||
				{slicedGPXStatistics}
 | 
			
		||||
				{additionalDatasets}
 | 
			
		||||
				{elevationFill}
 | 
			
		||||
				panelSize={bottomPanelSize}
 | 
			
		||||
				showControls={elevationControls}
 | 
			
		||||
				class="py-2"
 | 
			
		||||
			/>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
{#if embeddingOptions}
 | 
			
		||||
	<Embedding options={embeddingOptions} />
 | 
			
		||||
{/if}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user