mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-12-02 10:02:12 +00:00
fix embedding + playground
This commit is contained in:
@@ -1,40 +1,35 @@
|
||||
<script lang="ts">
|
||||
// import GPXLayers from '$lib/components/map/gpx-layer/GPXLayers.svelte';
|
||||
// import ElevationProfile from '$lib/components/elevation-profile/ElevationProfile.svelte';
|
||||
// import FileList from '$lib/components/file-list/FileList.svelte';
|
||||
// import GPXStatistics from '$lib/components/GPXStatistics.svelte';
|
||||
import GPXLayers from '$lib/components/map/gpx-layer/GPXLayers.svelte';
|
||||
import ElevationProfile from '$lib/components/elevation-profile/ElevationProfile.svelte';
|
||||
import FileList from '$lib/components/file-list/FileList.svelte';
|
||||
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
|
||||
import Map from '$lib/components/map/Map.svelte';
|
||||
import { map } from '$lib/components/map/map';
|
||||
// import LayerControl from '$lib/components/map/layer-control/LayerControl.svelte';
|
||||
import LayerControl from '$lib/components/map/layer-control/LayerControl.svelte';
|
||||
import OpenIn from '$lib/components/embedding/OpenIn.svelte';
|
||||
import {
|
||||
gpxStatistics,
|
||||
slicedGPXStatistics,
|
||||
embedding,
|
||||
loadFile,
|
||||
updateGPXData,
|
||||
} from '$lib/stores';
|
||||
import { onDestroy, onMount, setContext } from 'svelte';
|
||||
import { readable } from 'svelte/store';
|
||||
import { readable, writable } from 'svelte/store';
|
||||
import type { GPXFile } from 'gpx';
|
||||
import { ListFileItem } from '$lib/components/file-list/file-list';
|
||||
import {
|
||||
allowedEmbeddingBasemaps,
|
||||
getFilesFromEmbeddingOptions,
|
||||
type EmbeddingOptions,
|
||||
} from './Embedding';
|
||||
import { mode, setMode } from 'mode-watcher';
|
||||
import { browser } from '$app/environment';
|
||||
} from './embedding';
|
||||
import { setMode } from 'mode-watcher';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
|
||||
import { GPXStatisticsTree } from '$lib/logic/statistics-tree';
|
||||
import { loadFile } from '$lib/logic/file-actions';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
let {
|
||||
useHash = true,
|
||||
options = $bindable(),
|
||||
hash,
|
||||
hash = $bindable(),
|
||||
}: { useHash?: boolean; options: EmbeddingOptions; hash: string } = $props();
|
||||
|
||||
setContext('embedding', true);
|
||||
let additionalDatasets = writable<string[]>([]);
|
||||
let elevationFill = writable<'slope' | 'surface' | 'highway' | undefined>(undefined);
|
||||
|
||||
const {
|
||||
currentBasemap,
|
||||
@@ -46,190 +41,72 @@
|
||||
directionMarkers,
|
||||
} = settings;
|
||||
|
||||
let prevSettings: {
|
||||
distanceMarkers: boolean;
|
||||
directionMarkers: boolean;
|
||||
distanceUnits: 'metric' | 'imperial' | 'nautical';
|
||||
velocityUnits: 'speed' | 'pace';
|
||||
temperatureUnits: 'celsius' | 'fahrenheit';
|
||||
theme: 'light' | 'dark' | 'system';
|
||||
} = {
|
||||
distanceMarkers: false,
|
||||
directionMarkers: false,
|
||||
distanceUnits: 'metric',
|
||||
velocityUnits: 'speed',
|
||||
temperatureUnits: 'celsius',
|
||||
theme: 'system',
|
||||
};
|
||||
|
||||
function applyOptions() {
|
||||
// fileObservers.update(($fileObservers) => {
|
||||
// $fileObservers.clear();
|
||||
// return $fileObservers;
|
||||
// });
|
||||
// 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,
|
||||
// },
|
||||
// };
|
||||
// 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);
|
||||
// $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;
|
||||
// });
|
||||
// $fileOrder = [...$fileOrder.filter((id) => !id.includes('embed')), ...ids];
|
||||
// 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 (
|
||||
// options.basemap !== $currentBasemap &&
|
||||
// allowedEmbeddingBasemaps.includes(options.basemap)
|
||||
// ) {
|
||||
// $currentBasemap = options.basemap;
|
||||
// }
|
||||
// if (options.distanceMarkers !== $distanceMarkers) {
|
||||
// $distanceMarkers = options.distanceMarkers;
|
||||
// }
|
||||
// if (options.directionMarkers !== $directionMarkers) {
|
||||
// $directionMarkers = options.directionMarkers;
|
||||
// }
|
||||
// if (options.distanceUnits !== $distanceUnits) {
|
||||
// $distanceUnits = options.distanceUnits;
|
||||
// }
|
||||
// if (options.velocityUnits !== $velocityUnits) {
|
||||
// $velocityUnits = options.velocityUnits;
|
||||
// }
|
||||
// if (options.temperatureUnits !== $temperatureUnits) {
|
||||
// $temperatureUnits = options.temperatureUnits;
|
||||
// }
|
||||
// if (options.theme !== $mode) {
|
||||
// setMode(options.theme);
|
||||
// }
|
||||
let downloads: Promise<GPXFile | null>[] = getFilesFromEmbeddingOptions(options).map(
|
||||
(url) => {
|
||||
return fetch(url)
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => new File([blob], url.split('/').pop() ?? url))
|
||||
.then(loadFile);
|
||||
}
|
||||
);
|
||||
Promise.all(downloads).then((answers) => {
|
||||
const files = answers.filter((file) => file !== null) as GPXFile[];
|
||||
let ids: string[] = [];
|
||||
files.forEach((file, index) => {
|
||||
let id = `gpx-${index}-embed`;
|
||||
file._data.id = id;
|
||||
ids.push(id);
|
||||
});
|
||||
fileStateCollection.setEmbeddedFiles(files);
|
||||
$fileOrder = ids;
|
||||
selection.selectAll();
|
||||
});
|
||||
if (allowedEmbeddingBasemaps.includes(options.basemap)) {
|
||||
$currentBasemap = options.basemap;
|
||||
}
|
||||
$distanceMarkers = options.distanceMarkers;
|
||||
$directionMarkers = options.directionMarkers;
|
||||
$distanceUnits = options.distanceUnits;
|
||||
$velocityUnits = options.velocityUnits;
|
||||
$temperatureUnits = options.temperatureUnits;
|
||||
if (options.theme != 'system') {
|
||||
setMode(options.theme);
|
||||
}
|
||||
|
||||
additionalDatasets.set(
|
||||
[
|
||||
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.set(options.elevation.fill == 'none' ? undefined : options.elevation.fill);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
prevSettings.distanceMarkers = distanceMarkers.value;
|
||||
prevSettings.directionMarkers = directionMarkers.value;
|
||||
prevSettings.distanceUnits = distanceUnits.value;
|
||||
prevSettings.velocityUnits = velocityUnits.value;
|
||||
prevSettings.temperatureUnits = temperatureUnits.value;
|
||||
prevSettings.theme = mode.current ?? 'system';
|
||||
});
|
||||
|
||||
// $: if (browser && options) {
|
||||
// applyOptions();
|
||||
// }
|
||||
|
||||
// $: if ($fileOrder) {
|
||||
// updateGPXData();
|
||||
// }
|
||||
|
||||
onDestroy(() => {
|
||||
if (distanceMarkers.value !== prevSettings.distanceMarkers) {
|
||||
distanceMarkers.value = prevSettings.distanceMarkers;
|
||||
}
|
||||
|
||||
if (directionMarkers.value !== prevSettings.directionMarkers) {
|
||||
directionMarkers.value = prevSettings.directionMarkers;
|
||||
}
|
||||
|
||||
if (distanceUnits.value !== prevSettings.distanceUnits) {
|
||||
distanceUnits.value = prevSettings.distanceUnits;
|
||||
}
|
||||
|
||||
if (velocityUnits.value !== prevSettings.velocityUnits) {
|
||||
velocityUnits.value = prevSettings.velocityUnits;
|
||||
}
|
||||
|
||||
if (temperatureUnits.value !== prevSettings.temperatureUnits) {
|
||||
temperatureUnits.value = prevSettings.temperatureUnits;
|
||||
}
|
||||
|
||||
if (mode.current !== prevSettings.theme) {
|
||||
setMode(prevSettings.theme);
|
||||
}
|
||||
|
||||
// $selection.clear();
|
||||
// $fileObservers.clear();
|
||||
fileOrder.value = fileOrder.value.filter((id) => !id.includes('embed'));
|
||||
$effect(() => {
|
||||
options;
|
||||
untrack(applyOptions);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="absolute flex flex-col h-full w-full border rounded-xl overflow-clip">
|
||||
<div class="grow relative">
|
||||
<Map
|
||||
class="h-full {fileStateCollection.files.size > 1 ? 'horizontal' : ''}"
|
||||
class="h-full {$fileStateCollection.size > 1 ? 'horizontal' : ''}"
|
||||
accessToken={options.token}
|
||||
geocoder={false}
|
||||
geolocate={false}
|
||||
geolocate={true}
|
||||
hash={useHash}
|
||||
/>
|
||||
<OpenIn files={options.files} ids={options.ids} />
|
||||
<!-- <LayerControl /> -->
|
||||
<!-- <GPXLayers /> -->
|
||||
{#if fileStateCollection.files.size > 1}
|
||||
<LayerControl />
|
||||
<GPXLayers />
|
||||
{#if $fileStateCollection.size > 1}
|
||||
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
|
||||
<!-- <FileList orientation="horizontal" /> -->
|
||||
<FileList orientation="horizontal" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -237,26 +114,20 @@
|
||||
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
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
panelSize={options.elevation.height}
|
||||
orientation={options.elevation.show ? 'vertical' : 'horizontal'}
|
||||
/> -->
|
||||
/>
|
||||
{#if options.elevation.show}
|
||||
<!-- <ElevationProfile
|
||||
<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}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
showControls={options.elevation.controls}
|
||||
/> -->
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,63 +18,61 @@
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import {
|
||||
allowedEmbeddingBasemaps,
|
||||
defaultEmbeddingOptions,
|
||||
getCleanedEmbeddingOptions,
|
||||
getDefaultEmbeddingOptions,
|
||||
} from './Embedding';
|
||||
getMergedEmbeddingOptions,
|
||||
} 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 { onDestroy } from 'svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import { mode } from 'mode-watcher';
|
||||
|
||||
let options = getDefaultEmbeddingOptions();
|
||||
options.token = 'YOUR_MAPBOX_TOKEN';
|
||||
options.files = [
|
||||
'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx',
|
||||
];
|
||||
let options = $state(
|
||||
getMergedEmbeddingOptions(
|
||||
{
|
||||
token: 'YOUR_MAPBOX_TOKEN',
|
||||
theme: mode.current,
|
||||
},
|
||||
defaultEmbeddingOptions
|
||||
)
|
||||
);
|
||||
let files = $state(
|
||||
'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx'
|
||||
);
|
||||
let driveIds = $state('');
|
||||
|
||||
let files = options.files[0];
|
||||
$: {
|
||||
let urls = files.split(',');
|
||||
urls = urls.filter((url) => url.length > 0);
|
||||
if (JSON.stringify(urls) !== JSON.stringify(options.files)) {
|
||||
options.files = urls;
|
||||
let iframeOptions = $derived(
|
||||
getMergedEmbeddingOptions(
|
||||
{
|
||||
token:
|
||||
options.token.length === 0 || options.token === 'YOUR_MAPBOX_TOKEN'
|
||||
? PUBLIC_MAPBOX_TOKEN
|
||||
: options.token,
|
||||
files: files.split(',').filter((url) => url.length > 0),
|
||||
ids: driveIds.split(',').filter((id) => id.length > 0),
|
||||
elevation: {
|
||||
fill: options.elevation.fill === 'none' ? undefined : options.elevation.fill,
|
||||
},
|
||||
},
|
||||
options
|
||||
)
|
||||
);
|
||||
|
||||
let manualCamera = $state(false);
|
||||
let zoom = $state('0');
|
||||
let lat = $state('0');
|
||||
let lon = $state('0');
|
||||
let bearing = $state('0');
|
||||
let pitch = $state('0');
|
||||
let hash = $derived(manualCamera ? `#${zoom}/${lat}/${lon}/${bearing}/${pitch}` : '');
|
||||
|
||||
$effect(() => {
|
||||
if (options.elevation.show || options.elevation.height) {
|
||||
map.resize();
|
||||
}
|
||||
}
|
||||
let driveIds = '';
|
||||
$: {
|
||||
let ids = driveIds.split(',');
|
||||
ids = ids.filter((id) => id.length > 0);
|
||||
if (JSON.stringify(ids) !== JSON.stringify(options.ids)) {
|
||||
options.ids = ids;
|
||||
}
|
||||
}
|
||||
|
||||
let manualCamera = false;
|
||||
|
||||
let zoom = '0';
|
||||
let lat = '0';
|
||||
let lon = '0';
|
||||
let bearing = '0';
|
||||
let pitch = '0';
|
||||
|
||||
$: hash = manualCamera ? `#${zoom}/${lat}/${lon}/${bearing}/${pitch}` : '';
|
||||
|
||||
$: iframeOptions =
|
||||
options.token.length === 0 || options.token === 'YOUR_MAPBOX_TOKEN'
|
||||
? Object.assign({}, options, { token: PUBLIC_MAPBOX_TOKEN })
|
||||
: options;
|
||||
|
||||
async function resizeMap() {
|
||||
if ($map) {
|
||||
await tick();
|
||||
$map.resize();
|
||||
}
|
||||
}
|
||||
|
||||
$: if (options.elevation.height || options.elevation.show) {
|
||||
resizeMap();
|
||||
}
|
||||
});
|
||||
|
||||
function updateCamera() {
|
||||
if ($map) {
|
||||
@@ -87,9 +85,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($map) {
|
||||
$map.on('moveend', updateCamera);
|
||||
}
|
||||
map.onLoad((map_) => {
|
||||
map_.on('moveend', updateCamera);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if ($map) {
|
||||
$map.off('moveend', updateCamera);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Card.Root id="embedding-playground">
|
||||
@@ -105,19 +109,9 @@
|
||||
<Label for="drive_ids">{i18n._('embedding.drive_ids')}</Label>
|
||||
<Input id="drive_ids" type="text" class="h-8" bind:value={driveIds} />
|
||||
<Label for="basemap">{i18n._('embedding.basemap')}</Label>
|
||||
<Select.Root
|
||||
selected={{
|
||||
value: options.basemap,
|
||||
label: i18n._(`layers.label.${options.basemap}`),
|
||||
}}
|
||||
onSelectedChange={(selected) => {
|
||||
if (selected?.value) {
|
||||
options.basemap = selected?.value;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select.Root type="single" bind:value={options.basemap}>
|
||||
<Select.Trigger id="basemap" class="w-full h-8">
|
||||
<Select.Value />
|
||||
{i18n._(`layers.label.${options.basemap}`)}
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-60 overflow-y-scroll">
|
||||
{#each allowedEmbeddingBasemaps as basemap}
|
||||
@@ -145,23 +139,11 @@
|
||||
<span class="shrink-0">
|
||||
{i18n._('embedding.fill_by')}
|
||||
</span>
|
||||
<Select.Root
|
||||
selected={{ value: 'none', label: i18n._('embedding.none') }}
|
||||
onSelectedChange={(selected) => {
|
||||
let value = selected?.value;
|
||||
if (value === 'none') {
|
||||
options.elevation.fill = undefined;
|
||||
} else if (
|
||||
value === 'slope' ||
|
||||
value === 'surface' ||
|
||||
value === 'highway'
|
||||
) {
|
||||
options.elevation.fill = value;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select.Root type="single" bind:value={options.elevation.fill}>
|
||||
<Select.Trigger class="grow h-8">
|
||||
<Select.Value />
|
||||
{options.elevation.fill !== 'none'
|
||||
? i18n._(`quantities.${options.elevation.fill}`)
|
||||
: i18n._('embedding.none')}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="slope">{i18n._('quantities.slope')}</Select.Item
|
||||
@@ -331,7 +313,7 @@
|
||||
{i18n._('embedding.preview')}
|
||||
</Label>
|
||||
<div class="relative h-[600px]">
|
||||
<Embedding bind:options={iframeOptions} bind:hash useHash={false} />
|
||||
<Embedding options={iframeOptions} bind:hash useHash={false} />
|
||||
</div>
|
||||
<Label>
|
||||
{i18n._('embedding.code')}
|
||||
@@ -339,7 +321,7 @@
|
||||
<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?options=${encodeURIComponent(JSON.stringify(getCleanedEmbeddingOptions(options)))}${hash}" width="100%" height="600px" frameborder="0" style="outline: none;"/>`}
|
||||
{`<iframe src="https://gpx.studio${base}/embed?options=${encodeURIComponent(JSON.stringify(getCleanedEmbeddingOptions(iframeOptions)))}${hash}" width="100%" height="600px" frameborder="0" style="outline: none;"/>`}
|
||||
</code>
|
||||
</pre>
|
||||
</fieldset>
|
||||
|
||||
@@ -10,7 +10,7 @@ export type EmbeddingOptions = {
|
||||
show: boolean;
|
||||
height: number;
|
||||
controls: boolean;
|
||||
fill: 'slope' | 'surface' | 'highway' | undefined;
|
||||
fill: 'slope' | 'surface' | 'highway' | 'none';
|
||||
speed: boolean;
|
||||
hr: boolean;
|
||||
cad: boolean;
|
||||
@@ -34,7 +34,7 @@ export const defaultEmbeddingOptions = {
|
||||
show: true,
|
||||
height: 170,
|
||||
controls: true,
|
||||
fill: undefined,
|
||||
fill: 'none',
|
||||
speed: false,
|
||||
hr: false,
|
||||
cad: false,
|
||||
@@ -49,10 +49,6 @@ export const defaultEmbeddingOptions = {
|
||||
theme: 'system',
|
||||
};
|
||||
|
||||
export function getDefaultEmbeddingOptions(): EmbeddingOptions {
|
||||
return JSON.parse(JSON.stringify(defaultEmbeddingOptions));
|
||||
}
|
||||
|
||||
export function getMergedEmbeddingOptions(
|
||||
options: any,
|
||||
defaultOptions: any = defaultEmbeddingOptions
|
||||
Reference in New Issue
Block a user