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:
@@ -594,6 +594,7 @@ export class ElevationProfile {
|
|||||||
destroy() {
|
destroy() {
|
||||||
if (this._chart) {
|
if (this._chart) {
|
||||||
this._chart.destroy();
|
this._chart.destroy();
|
||||||
|
this._chart = null;
|
||||||
}
|
}
|
||||||
if (this._marker) {
|
if (this._marker) {
|
||||||
this._marker.remove();
|
this._marker.remove();
|
||||||
|
|||||||
@@ -1,40 +1,35 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// import GPXLayers from '$lib/components/map/gpx-layer/GPXLayers.svelte';
|
import GPXLayers from '$lib/components/map/gpx-layer/GPXLayers.svelte';
|
||||||
// import ElevationProfile from '$lib/components/elevation-profile/ElevationProfile.svelte';
|
import ElevationProfile from '$lib/components/elevation-profile/ElevationProfile.svelte';
|
||||||
// import FileList from '$lib/components/file-list/FileList.svelte';
|
import FileList from '$lib/components/file-list/FileList.svelte';
|
||||||
// import GPXStatistics from '$lib/components/GPXStatistics.svelte';
|
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
|
||||||
import Map from '$lib/components/map/Map.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 OpenIn from '$lib/components/embedding/OpenIn.svelte';
|
||||||
import {
|
import { readable, writable } from 'svelte/store';
|
||||||
gpxStatistics,
|
|
||||||
slicedGPXStatistics,
|
|
||||||
embedding,
|
|
||||||
loadFile,
|
|
||||||
updateGPXData,
|
|
||||||
} from '$lib/stores';
|
|
||||||
import { onDestroy, onMount, setContext } from 'svelte';
|
|
||||||
import { readable } from 'svelte/store';
|
|
||||||
import type { GPXFile } from 'gpx';
|
import type { GPXFile } from 'gpx';
|
||||||
import { ListFileItem } from '$lib/components/file-list/file-list';
|
|
||||||
import {
|
import {
|
||||||
allowedEmbeddingBasemaps,
|
allowedEmbeddingBasemaps,
|
||||||
getFilesFromEmbeddingOptions,
|
getFilesFromEmbeddingOptions,
|
||||||
type EmbeddingOptions,
|
type EmbeddingOptions,
|
||||||
} from './Embedding';
|
} from './embedding';
|
||||||
import { mode, setMode } from 'mode-watcher';
|
import { setMode } from 'mode-watcher';
|
||||||
import { browser } from '$app/environment';
|
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
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 {
|
let {
|
||||||
useHash = true,
|
useHash = true,
|
||||||
options = $bindable(),
|
options = $bindable(),
|
||||||
hash,
|
hash = $bindable(),
|
||||||
}: { useHash?: boolean; options: EmbeddingOptions; hash: string } = $props();
|
}: { useHash?: boolean; options: EmbeddingOptions; hash: string } = $props();
|
||||||
|
|
||||||
setContext('embedding', true);
|
let additionalDatasets = writable<string[]>([]);
|
||||||
|
let elevationFill = writable<'slope' | 'surface' | 'highway' | undefined>(undefined);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentBasemap,
|
currentBasemap,
|
||||||
@@ -46,190 +41,72 @@
|
|||||||
directionMarkers,
|
directionMarkers,
|
||||||
} = settings;
|
} = 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() {
|
function applyOptions() {
|
||||||
// fileObservers.update(($fileObservers) => {
|
let downloads: Promise<GPXFile | null>[] = getFilesFromEmbeddingOptions(options).map(
|
||||||
// $fileObservers.clear();
|
(url) => {
|
||||||
// return $fileObservers;
|
return fetch(url)
|
||||||
// });
|
.then((response) => response.blob())
|
||||||
// let downloads: Promise<GPXFile | null>[] = [];
|
.then((blob) => new File([blob], url.split('/').pop() ?? url))
|
||||||
// getFilesFromEmbeddingOptions(options).forEach((url) => {
|
.then(loadFile);
|
||||||
// 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);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
onMount(() => {
|
Promise.all(downloads).then((answers) => {
|
||||||
prevSettings.distanceMarkers = distanceMarkers.value;
|
const files = answers.filter((file) => file !== null) as GPXFile[];
|
||||||
prevSettings.directionMarkers = directionMarkers.value;
|
let ids: string[] = [];
|
||||||
prevSettings.distanceUnits = distanceUnits.value;
|
files.forEach((file, index) => {
|
||||||
prevSettings.velocityUnits = velocityUnits.value;
|
let id = `gpx-${index}-embed`;
|
||||||
prevSettings.temperatureUnits = temperatureUnits.value;
|
file._data.id = id;
|
||||||
prevSettings.theme = mode.current ?? 'system';
|
ids.push(id);
|
||||||
});
|
});
|
||||||
|
fileStateCollection.setEmbeddedFiles(files);
|
||||||
// $: if (browser && options) {
|
$fileOrder = ids;
|
||||||
// applyOptions();
|
selection.selectAll();
|
||||||
// }
|
});
|
||||||
|
if (allowedEmbeddingBasemaps.includes(options.basemap)) {
|
||||||
// $: if ($fileOrder) {
|
$currentBasemap = options.basemap;
|
||||||
// updateGPXData();
|
}
|
||||||
// }
|
$distanceMarkers = options.distanceMarkers;
|
||||||
|
$directionMarkers = options.directionMarkers;
|
||||||
onDestroy(() => {
|
$distanceUnits = options.distanceUnits;
|
||||||
if (distanceMarkers.value !== prevSettings.distanceMarkers) {
|
$velocityUnits = options.velocityUnits;
|
||||||
distanceMarkers.value = prevSettings.distanceMarkers;
|
$temperatureUnits = options.temperatureUnits;
|
||||||
|
if (options.theme != 'system') {
|
||||||
|
setMode(options.theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (directionMarkers.value !== prevSettings.directionMarkers) {
|
additionalDatasets.set(
|
||||||
directionMarkers.value = prevSettings.directionMarkers;
|
[
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (distanceUnits.value !== prevSettings.distanceUnits) {
|
$effect(() => {
|
||||||
distanceUnits.value = prevSettings.distanceUnits;
|
options;
|
||||||
}
|
untrack(applyOptions);
|
||||||
|
|
||||||
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'));
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="absolute flex flex-col h-full w-full border rounded-xl overflow-clip">
|
<div class="absolute flex flex-col h-full w-full border rounded-xl overflow-clip">
|
||||||
<div class="grow relative">
|
<div class="grow relative">
|
||||||
<Map
|
<Map
|
||||||
class="h-full {fileStateCollection.files.size > 1 ? 'horizontal' : ''}"
|
class="h-full {$fileStateCollection.size > 1 ? 'horizontal' : ''}"
|
||||||
accessToken={options.token}
|
accessToken={options.token}
|
||||||
geocoder={false}
|
geocoder={false}
|
||||||
geolocate={false}
|
geolocate={true}
|
||||||
hash={useHash}
|
hash={useHash}
|
||||||
/>
|
/>
|
||||||
<OpenIn files={options.files} ids={options.ids} />
|
<OpenIn files={options.files} ids={options.ids} />
|
||||||
<!-- <LayerControl /> -->
|
<LayerControl />
|
||||||
<!-- <GPXLayers /> -->
|
<GPXLayers />
|
||||||
{#if fileStateCollection.files.size > 1}
|
{#if $fileStateCollection.size > 1}
|
||||||
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
|
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
|
||||||
<!-- <FileList orientation="horizontal" /> -->
|
<FileList orientation="horizontal" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -237,26 +114,20 @@
|
|||||||
class="{options.elevation.show ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"
|
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` : ''}
|
style={options.elevation.show ? `height: ${options.elevation.height}px` : ''}
|
||||||
>
|
>
|
||||||
<!-- <GPXStatistics
|
<GPXStatistics
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
panelSize={options.elevation.height}
|
panelSize={options.elevation.height}
|
||||||
orientation={options.elevation.show ? 'vertical' : 'horizontal'}
|
orientation={options.elevation.show ? 'vertical' : 'horizontal'}
|
||||||
/> -->
|
/>
|
||||||
{#if options.elevation.show}
|
{#if options.elevation.show}
|
||||||
<!-- <ElevationProfile
|
<ElevationProfile
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
additionalDatasets={[
|
{additionalDatasets}
|
||||||
options.elevation.speed ? 'speed' : null,
|
{elevationFill}
|
||||||
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}
|
|
||||||
showControls={options.elevation.controls}
|
showControls={options.elevation.controls}
|
||||||
/> -->
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,63 +18,61 @@
|
|||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import {
|
import {
|
||||||
allowedEmbeddingBasemaps,
|
allowedEmbeddingBasemaps,
|
||||||
|
defaultEmbeddingOptions,
|
||||||
getCleanedEmbeddingOptions,
|
getCleanedEmbeddingOptions,
|
||||||
getDefaultEmbeddingOptions,
|
getMergedEmbeddingOptions,
|
||||||
} from './Embedding';
|
} from './embedding';
|
||||||
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
import Embedding from './Embedding.svelte';
|
import Embedding from './Embedding.svelte';
|
||||||
import { map } from '$lib/stores';
|
import { onDestroy } from 'svelte';
|
||||||
import { tick } from 'svelte';
|
|
||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
|
import { map } from '$lib/components/map/map';
|
||||||
|
import { mode } from 'mode-watcher';
|
||||||
|
|
||||||
let options = getDefaultEmbeddingOptions();
|
let options = $state(
|
||||||
options.token = 'YOUR_MAPBOX_TOKEN';
|
getMergedEmbeddingOptions(
|
||||||
options.files = [
|
{
|
||||||
'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx',
|
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 iframeOptions = $derived(
|
||||||
$: {
|
getMergedEmbeddingOptions(
|
||||||
let urls = files.split(',');
|
{
|
||||||
urls = urls.filter((url) => url.length > 0);
|
token:
|
||||||
if (JSON.stringify(urls) !== JSON.stringify(options.files)) {
|
|
||||||
options.files = urls;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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'
|
options.token.length === 0 || options.token === 'YOUR_MAPBOX_TOKEN'
|
||||||
? Object.assign({}, options, { token: PUBLIC_MAPBOX_TOKEN })
|
? PUBLIC_MAPBOX_TOKEN
|
||||||
: options;
|
: 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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
async function resizeMap() {
|
let manualCamera = $state(false);
|
||||||
if ($map) {
|
let zoom = $state('0');
|
||||||
await tick();
|
let lat = $state('0');
|
||||||
$map.resize();
|
let lon = $state('0');
|
||||||
}
|
let bearing = $state('0');
|
||||||
}
|
let pitch = $state('0');
|
||||||
|
let hash = $derived(manualCamera ? `#${zoom}/${lat}/${lon}/${bearing}/${pitch}` : '');
|
||||||
|
|
||||||
$: if (options.elevation.height || options.elevation.show) {
|
$effect(() => {
|
||||||
resizeMap();
|
if (options.elevation.show || options.elevation.height) {
|
||||||
|
map.resize();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function updateCamera() {
|
function updateCamera() {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
@@ -87,9 +85,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($map) {
|
map.onLoad((map_) => {
|
||||||
$map.on('moveend', updateCamera);
|
map_.on('moveend', updateCamera);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if ($map) {
|
||||||
|
$map.off('moveend', updateCamera);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root id="embedding-playground">
|
<Card.Root id="embedding-playground">
|
||||||
@@ -105,19 +109,9 @@
|
|||||||
<Label for="drive_ids">{i18n._('embedding.drive_ids')}</Label>
|
<Label for="drive_ids">{i18n._('embedding.drive_ids')}</Label>
|
||||||
<Input id="drive_ids" type="text" class="h-8" bind:value={driveIds} />
|
<Input id="drive_ids" type="text" class="h-8" bind:value={driveIds} />
|
||||||
<Label for="basemap">{i18n._('embedding.basemap')}</Label>
|
<Label for="basemap">{i18n._('embedding.basemap')}</Label>
|
||||||
<Select.Root
|
<Select.Root type="single" bind:value={options.basemap}>
|
||||||
selected={{
|
|
||||||
value: options.basemap,
|
|
||||||
label: i18n._(`layers.label.${options.basemap}`),
|
|
||||||
}}
|
|
||||||
onSelectedChange={(selected) => {
|
|
||||||
if (selected?.value) {
|
|
||||||
options.basemap = selected?.value;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Select.Trigger id="basemap" class="w-full h-8">
|
<Select.Trigger id="basemap" class="w-full h-8">
|
||||||
<Select.Value />
|
{i18n._(`layers.label.${options.basemap}`)}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content class="max-h-60 overflow-y-scroll">
|
<Select.Content class="max-h-60 overflow-y-scroll">
|
||||||
{#each allowedEmbeddingBasemaps as basemap}
|
{#each allowedEmbeddingBasemaps as basemap}
|
||||||
@@ -145,23 +139,11 @@
|
|||||||
<span class="shrink-0">
|
<span class="shrink-0">
|
||||||
{i18n._('embedding.fill_by')}
|
{i18n._('embedding.fill_by')}
|
||||||
</span>
|
</span>
|
||||||
<Select.Root
|
<Select.Root type="single" bind:value={options.elevation.fill}>
|
||||||
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.Trigger class="grow h-8">
|
<Select.Trigger class="grow h-8">
|
||||||
<Select.Value />
|
{options.elevation.fill !== 'none'
|
||||||
|
? i18n._(`quantities.${options.elevation.fill}`)
|
||||||
|
: i18n._('embedding.none')}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
<Select.Item value="slope">{i18n._('quantities.slope')}</Select.Item
|
<Select.Item value="slope">{i18n._('quantities.slope')}</Select.Item
|
||||||
@@ -331,7 +313,7 @@
|
|||||||
{i18n._('embedding.preview')}
|
{i18n._('embedding.preview')}
|
||||||
</Label>
|
</Label>
|
||||||
<div class="relative h-[600px]">
|
<div class="relative h-[600px]">
|
||||||
<Embedding bind:options={iframeOptions} bind:hash useHash={false} />
|
<Embedding options={iframeOptions} bind:hash useHash={false} />
|
||||||
</div>
|
</div>
|
||||||
<Label>
|
<Label>
|
||||||
{i18n._('embedding.code')}
|
{i18n._('embedding.code')}
|
||||||
@@ -339,7 +321,7 @@
|
|||||||
<pre
|
<pre
|
||||||
class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all">
|
class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all">
|
||||||
<code class="language-html">
|
<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>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export type EmbeddingOptions = {
|
|||||||
show: boolean;
|
show: boolean;
|
||||||
height: number;
|
height: number;
|
||||||
controls: boolean;
|
controls: boolean;
|
||||||
fill: 'slope' | 'surface' | 'highway' | undefined;
|
fill: 'slope' | 'surface' | 'highway' | 'none';
|
||||||
speed: boolean;
|
speed: boolean;
|
||||||
hr: boolean;
|
hr: boolean;
|
||||||
cad: boolean;
|
cad: boolean;
|
||||||
@@ -34,7 +34,7 @@ export const defaultEmbeddingOptions = {
|
|||||||
show: true,
|
show: true,
|
||||||
height: 170,
|
height: 170,
|
||||||
controls: true,
|
controls: true,
|
||||||
fill: undefined,
|
fill: 'none',
|
||||||
speed: false,
|
speed: false,
|
||||||
hr: false,
|
hr: false,
|
||||||
cad: false,
|
cad: false,
|
||||||
@@ -49,10 +49,6 @@ export const defaultEmbeddingOptions = {
|
|||||||
theme: 'system',
|
theme: 'system',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getDefaultEmbeddingOptions(): EmbeddingOptions {
|
|
||||||
return JSON.parse(JSON.stringify(defaultEmbeddingOptions));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMergedEmbeddingOptions(
|
export function getMergedEmbeddingOptions(
|
||||||
options: any,
|
options: any,
|
||||||
defaultOptions: any = defaultEmbeddingOptions
|
defaultOptions: any = defaultEmbeddingOptions
|
||||||
@@ -22,6 +22,7 @@ export class StartEndMarkers {
|
|||||||
this.start = new mapboxgl.Marker({ element: startElement });
|
this.start = new mapboxgl.Marker({ element: startElement });
|
||||||
this.end = new mapboxgl.Marker({ element: endElement });
|
this.end = new mapboxgl.Marker({ element: endElement });
|
||||||
|
|
||||||
|
map.onLoad(() => this.update());
|
||||||
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
|
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(slicedGPXStatistics.subscribe(this.updateBinded));
|
this.unsubscribes.push(slicedGPXStatistics.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(currentTool.subscribe(this.updateBinded));
|
this.unsubscribes.push(currentTool.subscribe(this.updateBinded));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TrackPoint, Waypoint } from 'gpx';
|
import { TrackPoint, Waypoint } from 'gpx';
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { mount, tick } from 'svelte';
|
import { mount, tick, unmount } from 'svelte';
|
||||||
import { get, writable, type Writable } from 'svelte/store';
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
import MapPopupComponent from '$lib/components/map/MapPopup.svelte';
|
import MapPopupComponent from '$lib/components/map/MapPopup.svelte';
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export class MapPopup {
|
|||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.popup.remove();
|
this.popup.remove();
|
||||||
this.component.$destroy();
|
unmount(this.component);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCoordinates() {
|
getCoordinates() {
|
||||||
|
|||||||
@@ -2,61 +2,12 @@ import { get } from 'svelte/store';
|
|||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { ListFileItem, ListWaypointItem } from '$lib/components/file-list/file-list';
|
import { ListFileItem, ListWaypointItem } from '$lib/components/file-list/file-list';
|
||||||
import {
|
import { fileStateCollection, GPXFileStateCollectionObserver } from '$lib/logic/file-state';
|
||||||
fileStateCollection,
|
|
||||||
GPXFileState,
|
|
||||||
GPXFileStateCollectionObserver,
|
|
||||||
} from '$lib/logic/file-state';
|
|
||||||
import { gpxStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics } from '$lib/logic/statistics';
|
||||||
import { map } from '$lib/components/map/map';
|
import { map } from '$lib/components/map/map';
|
||||||
import type { GPXFileWithStatistics } from './statistics-tree';
|
import type { GPXFileWithStatistics } from './statistics-tree';
|
||||||
import type { Coordinates } from 'gpx';
|
import type { Coordinates } from 'gpx';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { browser } from '$app/environment';
|
|
||||||
|
|
||||||
// const targetMapBounds: {
|
|
||||||
// bounds: mapboxgl.LngLatBounds;
|
|
||||||
// ids: string[];
|
|
||||||
// total: number;
|
|
||||||
// } = $state({
|
|
||||||
// bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]),
|
|
||||||
// ids: [],
|
|
||||||
// total: 0,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// $effect(() => {
|
|
||||||
// if (
|
|
||||||
// map.current === null ||
|
|
||||||
// targetMapBounds.ids.length > 0 ||
|
|
||||||
// (targetMapBounds.bounds.getSouth() === 90 &&
|
|
||||||
// targetMapBounds.bounds.getWest() === 180 &&
|
|
||||||
// targetMapBounds.bounds.getNorth() === -90 &&
|
|
||||||
// targetMapBounds.bounds.getEast() === -180)
|
|
||||||
// ) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let currentZoom = map.current.getZoom();
|
|
||||||
// let currentBounds = map.current.getBounds();
|
|
||||||
// if (
|
|
||||||
// targetMapBounds.total !== get(fileObservers).size &&
|
|
||||||
// currentBounds &&
|
|
||||||
// currentZoom > 2 // Extend current bounds only if the map is zoomed in
|
|
||||||
// ) {
|
|
||||||
// // There are other files on the map
|
|
||||||
// if (
|
|
||||||
// currentBounds.contains(targetMapBounds.bounds.getSouthEast()) &&
|
|
||||||
// currentBounds.contains(targetMapBounds.bounds.getNorthWest())
|
|
||||||
// ) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// targetMapBounds.bounds.extend(currentBounds.getSouthWest());
|
|
||||||
// targetMapBounds.bounds.extend(currentBounds.getNorthEast());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// map.current.fitBounds(targetMapBounds.bounds, { padding: 80, linear: true, easing: () => 1 });
|
|
||||||
// });
|
|
||||||
|
|
||||||
export class BoundsManager {
|
export class BoundsManager {
|
||||||
private _bounds: mapboxgl.LngLatBounds = new mapboxgl.LngLatBounds();
|
private _bounds: mapboxgl.LngLatBounds = new mapboxgl.LngLatBounds();
|
||||||
|
|||||||
@@ -2,11 +2,7 @@ import { db, type Database } from '$lib/db';
|
|||||||
import { liveQuery } from 'dexie';
|
import { liveQuery } from 'dexie';
|
||||||
import type { GPXFile } from 'gpx';
|
import type { GPXFile } from 'gpx';
|
||||||
import { applyPatches, produceWithPatches, type Patch, type WritableDraft } from 'immer';
|
import { applyPatches, produceWithPatches, type Patch, type WritableDraft } from 'immer';
|
||||||
import {
|
import { GPXFileStateCollectionObserver } from '$lib/logic/file-state';
|
||||||
fileStateCollection,
|
|
||||||
GPXFileStateCollectionObserver,
|
|
||||||
type GPXFileStateCollection,
|
|
||||||
} from '$lib/logic/file-state';
|
|
||||||
import {
|
import {
|
||||||
derived,
|
derived,
|
||||||
get,
|
get,
|
||||||
@@ -30,7 +26,7 @@ export class FileActionManager {
|
|||||||
private _canUndo: Readable<boolean>;
|
private _canUndo: Readable<boolean>;
|
||||||
private _canRedo: Readable<boolean>;
|
private _canRedo: Readable<boolean>;
|
||||||
|
|
||||||
constructor(db: Database, fileStateCollection: GPXFileStateCollection) {
|
constructor(db: Database) {
|
||||||
this._db = db;
|
this._db = db;
|
||||||
this._files = new Map();
|
this._files = new Map();
|
||||||
this._fileSubscriptions = new Map();
|
this._fileSubscriptions = new Map();
|
||||||
@@ -156,7 +152,7 @@ export class FileActionManager {
|
|||||||
selection.updateFiles(updatedFiles, deletedFileIds);
|
selection.updateFiles(updatedFiles, deletedFileIds);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return db.transaction('rw', db.fileids, db.files, async () => {
|
return this._db.transaction('rw', this._db.fileids, this._db.files, async () => {
|
||||||
if (updatedFileIds.length > 0) {
|
if (updatedFileIds.length > 0) {
|
||||||
await this._db.fileids.bulkPut(updatedFileIds, updatedFileIds);
|
await this._db.fileids.bulkPut(updatedFileIds, updatedFileIds);
|
||||||
await this._db.files.bulkPut(updatedFiles, updatedFileIds);
|
await this._db.files.bulkPut(updatedFiles, updatedFileIds);
|
||||||
@@ -254,4 +250,4 @@ function getChangedFileIds(patch: Patch[]): string[] {
|
|||||||
return Array.from(changedFileIds);
|
return Array.from(changedFileIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fileActionManager = new FileActionManager(db, fileStateCollection);
|
export const fileActionManager = new FileActionManager(db);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/simplify';
|
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/simplify';
|
||||||
import { db, type Database } from '$lib/db';
|
import { type Database } from '$lib/db';
|
||||||
import { liveQuery } from 'dexie';
|
import { liveQuery } from 'dexie';
|
||||||
import { GPXFile } from 'gpx';
|
import { GPXFile } from 'gpx';
|
||||||
import { GPXStatisticsTree, type GPXFileWithStatistics } from '$lib/logic/statistics-tree';
|
import { GPXStatisticsTree, type GPXFileWithStatistics } from '$lib/logic/statistics-tree';
|
||||||
@@ -8,13 +8,18 @@ import { get, writable, type Subscriber, type Writable } from 'svelte/store';
|
|||||||
|
|
||||||
// Observe a single file from the database, and maintain its statistics
|
// Observe a single file from the database, and maintain its statistics
|
||||||
export class GPXFileState {
|
export class GPXFileState {
|
||||||
|
private _fileId: string;
|
||||||
private _file: Writable<GPXFileWithStatistics | undefined>;
|
private _file: Writable<GPXFileWithStatistics | undefined>;
|
||||||
private _subscription: { unsubscribe: () => void } | undefined;
|
private _subscription: { unsubscribe: () => void } | undefined;
|
||||||
|
|
||||||
constructor(db: Database, fileId: string) {
|
constructor(fileId: string, file?: GPXFile) {
|
||||||
this._file = writable(undefined);
|
this._fileId = fileId;
|
||||||
|
this._file = writable(file ? { file, statistics: new GPXStatisticsTree(file) } : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
this._subscription = liveQuery(() => db.files.get(fileId)).subscribe((value) => {
|
connectToDatabase(db: Database) {
|
||||||
|
if (this._subscription) return;
|
||||||
|
this._subscription = liveQuery(() => db.files.get(this._fileId)).subscribe((value) => {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
let file = new GPXFile(value);
|
let file = new GPXFile(value);
|
||||||
updateAnchorPoints(file);
|
updateAnchorPoints(file);
|
||||||
@@ -45,11 +50,15 @@ export class GPXFileState {
|
|||||||
// Observe the file ids in the database, and maintain a map of file states for the corresponding files
|
// Observe the file ids in the database, and maintain a map of file states for the corresponding files
|
||||||
export class GPXFileStateCollection {
|
export class GPXFileStateCollection {
|
||||||
private _files: Writable<Map<string, GPXFileState>>;
|
private _files: Writable<Map<string, GPXFileState>>;
|
||||||
|
private _subscription: { unsubscribe: () => void } | null = null;
|
||||||
|
|
||||||
constructor(db: Database) {
|
constructor() {
|
||||||
this._files = writable(new Map());
|
this._files = writable(new Map());
|
||||||
|
}
|
||||||
|
|
||||||
liveQuery(() => db.fileids.toArray()).subscribe((dbFileIds) => {
|
connectToDatabase(db: Database) {
|
||||||
|
if (this._subscription) return;
|
||||||
|
this._subscription = liveQuery(() => db.fileids.toArray()).subscribe((dbFileIds) => {
|
||||||
const currentFiles = get(this._files);
|
const currentFiles = get(this._files);
|
||||||
// Find new files to observe
|
// Find new files to observe
|
||||||
let newFiles = dbFileIds
|
let newFiles = dbFileIds
|
||||||
@@ -64,7 +73,9 @@ export class GPXFileStateCollection {
|
|||||||
// Update the map of file states
|
// Update the map of file states
|
||||||
this._files.update(($files) => {
|
this._files.update(($files) => {
|
||||||
newFiles.forEach((id) => {
|
newFiles.forEach((id) => {
|
||||||
$files.set(id, new GPXFileState(db, id));
|
const fileState = new GPXFileState(id);
|
||||||
|
fileState.connectToDatabase(db);
|
||||||
|
$files.set(id, fileState);
|
||||||
});
|
});
|
||||||
deletedFiles.forEach((id) => {
|
deletedFiles.forEach((id) => {
|
||||||
$files.get(id)?.destroy();
|
$files.get(id)?.destroy();
|
||||||
@@ -85,6 +96,31 @@ export class GPXFileStateCollection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnectFromDatabase() {
|
||||||
|
this._subscription?.unsubscribe();
|
||||||
|
this._subscription = null;
|
||||||
|
this._files.update(($files) => {
|
||||||
|
$files.forEach((fileState) => {
|
||||||
|
fileState.destroy();
|
||||||
|
});
|
||||||
|
return new Map();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setEmbeddedFiles(files: GPXFile[]) {
|
||||||
|
this._files.update(($files) => {
|
||||||
|
$files.clear();
|
||||||
|
files.forEach((file) => {
|
||||||
|
const id = file._data.id;
|
||||||
|
if (!$files.has(id)) {
|
||||||
|
const fileState = new GPXFileState(id, file);
|
||||||
|
$files.set(id, fileState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return $files;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
subscribe(run: Subscriber<Map<string, GPXFileState>>, invalidate?: () => void) {
|
subscribe(run: Subscriber<Map<string, GPXFileState>>, invalidate?: () => void) {
|
||||||
return this._files.subscribe(run, invalidate);
|
return this._files.subscribe(run, invalidate);
|
||||||
}
|
}
|
||||||
@@ -117,7 +153,7 @@ export class GPXFileStateCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Collection of all file states
|
// Collection of all file states
|
||||||
export const fileStateCollection = new GPXFileStateCollection(db);
|
export const fileStateCollection = new GPXFileStateCollection();
|
||||||
|
|
||||||
export type GPXFileStateCallback = (files: Map<string, GPXFileState>) => void;
|
export type GPXFileStateCallback = (files: Map<string, GPXFileState>) => void;
|
||||||
export class GPXFileStateCollectionObserver {
|
export class GPXFileStateCollectionObserver {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { db, type Database } from '$lib/db';
|
import { type Database } from '$lib/db';
|
||||||
import { liveQuery } from 'dexie';
|
import { liveQuery } from 'dexie';
|
||||||
import {
|
import {
|
||||||
defaultBasemap,
|
defaultBasemap,
|
||||||
@@ -14,17 +14,22 @@ import { browser } from '$app/environment';
|
|||||||
import { get, writable, type Writable } from 'svelte/store';
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
|
|
||||||
export class Setting<V> {
|
export class Setting<V> {
|
||||||
private _db: Database;
|
private _db: Database | null = null;
|
||||||
|
private _subscription: { unsubscribe: () => void } | null = null;
|
||||||
private _key: string;
|
private _key: string;
|
||||||
private _value: Writable<V>;
|
private _value: Writable<V>;
|
||||||
|
|
||||||
constructor(db: Database, key: string, initial: V) {
|
constructor(key: string, initial: V) {
|
||||||
this._db = db;
|
|
||||||
this._key = key;
|
this._key = key;
|
||||||
this._value = writable(initial);
|
this._value = writable(initial);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectToDatabase(db: Database) {
|
||||||
|
if (this._db) return;
|
||||||
|
this._db = db;
|
||||||
|
|
||||||
let first = true;
|
let first = true;
|
||||||
liveQuery(() => db.settings.get(key)).subscribe((value) => {
|
this._subscription = liveQuery(() => db.settings.get(this._key)).subscribe((value) => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
if (!first) {
|
if (!first) {
|
||||||
this._value.set(value);
|
this._value.set(value);
|
||||||
@@ -36,39 +41,53 @@ export class Setting<V> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnectFromDatabase() {
|
||||||
|
this._subscription?.unsubscribe();
|
||||||
|
this._subscription = null;
|
||||||
|
this._db = null;
|
||||||
|
}
|
||||||
|
|
||||||
subscribe(run: (value: V) => void, invalidate?: (value?: V) => void) {
|
subscribe(run: (value: V) => void, invalidate?: (value?: V) => void) {
|
||||||
return this._value.subscribe(run, invalidate);
|
return this._value.subscribe(run, invalidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
set(newValue: V) {
|
set(value: V) {
|
||||||
if (typeof newValue === 'object' || newValue !== get(this._value)) {
|
if (typeof value === 'object' || value !== get(this._value)) {
|
||||||
this._db.settings.put(newValue, this._key);
|
if (this._db) {
|
||||||
|
this._db.settings.put(value, this._key);
|
||||||
|
} else {
|
||||||
|
this._value.set(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(callback: (value: any) => any) {
|
update(callback: (value: any) => any) {
|
||||||
let newValue = callback(get(this._value));
|
this.set(callback(get(this._value)));
|
||||||
if (typeof newValue === 'object' || newValue !== get(this._value)) {
|
|
||||||
this._db.settings.put(newValue, this._key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SettingInitOnFirstRead<V> {
|
export class SettingInitOnFirstRead<V> {
|
||||||
private _db: Database;
|
private _db: Database | null = null;
|
||||||
|
private _subscription: { unsubscribe: () => void } | null = null;
|
||||||
private _key: string;
|
private _key: string;
|
||||||
private _value: Writable<V | undefined>;
|
private _value: Writable<V | undefined>;
|
||||||
|
private _initial: V;
|
||||||
|
|
||||||
constructor(db: Database, key: string, initial: V) {
|
constructor(key: string, initial: V) {
|
||||||
this._db = db;
|
|
||||||
this._key = key;
|
this._key = key;
|
||||||
this._value = writable(undefined);
|
this._value = writable(undefined);
|
||||||
|
this._initial = initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectToDatabase(db: Database) {
|
||||||
|
if (this._db) return;
|
||||||
|
this._db = db;
|
||||||
|
|
||||||
let first = true;
|
let first = true;
|
||||||
liveQuery(() => db.settings.get(key)).subscribe((value) => {
|
this._subscription = liveQuery(() => db.settings.get(this._key)).subscribe((value) => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
if (first) {
|
if (first) {
|
||||||
this._value.set(initial);
|
this._value.set(this._initial);
|
||||||
} else {
|
} else {
|
||||||
this._value.set(value);
|
this._value.set(value);
|
||||||
}
|
}
|
||||||
@@ -79,58 +98,80 @@ export class SettingInitOnFirstRead<V> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnectFromDatabase() {
|
||||||
|
this._subscription?.unsubscribe();
|
||||||
|
this._subscription = null;
|
||||||
|
this._db = null;
|
||||||
|
}
|
||||||
|
|
||||||
subscribe(run: (value: V | undefined) => void, invalidate?: (value?: V | undefined) => void) {
|
subscribe(run: (value: V | undefined) => void, invalidate?: (value?: V | undefined) => void) {
|
||||||
return this._value.subscribe(run, invalidate);
|
return this._value.subscribe(run, invalidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
set(newValue: V) {
|
set(value: V) {
|
||||||
if (typeof newValue === 'object' || newValue !== get(this._value)) {
|
if (typeof value === 'object' || value !== get(this._value)) {
|
||||||
this._db.settings.put(newValue, this._key);
|
if (this._db) {
|
||||||
|
this._db.settings.put(value, this._key);
|
||||||
|
} else {
|
||||||
|
this._value.set(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(callback: (value: any) => any) {
|
update(callback: (value: any) => any) {
|
||||||
let newValue = callback(get(this._value));
|
this.set(callback(get(this._value)));
|
||||||
if (typeof newValue === 'object' || newValue !== get(this._value)) {
|
|
||||||
this._db.settings.put(newValue, this._key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const settings = {
|
export const settings = {
|
||||||
distanceUnits: new Setting<'metric' | 'imperial' | 'nautical'>(db, 'distanceUnits', 'metric'),
|
distanceUnits: new Setting<'metric' | 'imperial' | 'nautical'>('distanceUnits', 'metric'),
|
||||||
velocityUnits: new Setting<'speed' | 'pace'>(db, 'velocityUnits', 'speed'),
|
velocityUnits: new Setting<'speed' | 'pace'>('velocityUnits', 'speed'),
|
||||||
temperatureUnits: new Setting<'celsius' | 'fahrenheit'>(db, 'temperatureUnits', 'celsius'),
|
temperatureUnits: new Setting<'celsius' | 'fahrenheit'>('temperatureUnits', 'celsius'),
|
||||||
elevationProfile: new Setting<boolean>(db, 'elevationProfile', true),
|
elevationProfile: new Setting<boolean>('elevationProfile', true),
|
||||||
additionalDatasets: new Setting<string[]>(db, 'additionalDatasets', []),
|
additionalDatasets: new Setting<string[]>('additionalDatasets', []),
|
||||||
elevationFill: new Setting<'slope' | 'surface' | undefined>(db, 'elevationFill', undefined),
|
elevationFill: new Setting<'slope' | 'surface' | undefined>('elevationFill', undefined),
|
||||||
treeFileView: new Setting<boolean>(db, 'fileView', false),
|
treeFileView: new Setting<boolean>('fileView', false),
|
||||||
minimizeRoutingMenu: new Setting(db, 'minimizeRoutingMenu', false),
|
minimizeRoutingMenu: new Setting('minimizeRoutingMenu', false),
|
||||||
routing: new Setting(db, 'routing', true),
|
routing: new Setting('routing', true),
|
||||||
routingProfile: new Setting(db, 'routingProfile', 'bike'),
|
routingProfile: new Setting('routingProfile', 'bike'),
|
||||||
privateRoads: new Setting(db, 'privateRoads', false),
|
privateRoads: new Setting('privateRoads', false),
|
||||||
currentBasemap: new Setting(db, 'currentBasemap', defaultBasemap),
|
currentBasemap: new Setting('currentBasemap', defaultBasemap),
|
||||||
previousBasemap: new Setting(db, 'previousBasemap', defaultBasemap),
|
previousBasemap: new Setting('previousBasemap', defaultBasemap),
|
||||||
selectedBasemapTree: new Setting(db, 'selectedBasemapTree', defaultBasemapTree),
|
selectedBasemapTree: new Setting('selectedBasemapTree', defaultBasemapTree),
|
||||||
currentOverlays: new SettingInitOnFirstRead(db, 'currentOverlays', defaultOverlays),
|
currentOverlays: new SettingInitOnFirstRead('currentOverlays', defaultOverlays),
|
||||||
previousOverlays: new Setting(db, 'previousOverlays', defaultOverlays),
|
previousOverlays: new Setting('previousOverlays', defaultOverlays),
|
||||||
selectedOverlayTree: new Setting(db, 'selectedOverlayTree', defaultOverlayTree),
|
selectedOverlayTree: new Setting('selectedOverlayTree', defaultOverlayTree),
|
||||||
currentOverpassQueries: new SettingInitOnFirstRead(
|
currentOverpassQueries: new SettingInitOnFirstRead(
|
||||||
db,
|
|
||||||
'currentOverpassQueries',
|
'currentOverpassQueries',
|
||||||
defaultOverpassQueries
|
defaultOverpassQueries
|
||||||
),
|
),
|
||||||
selectedOverpassTree: new Setting(db, 'selectedOverpassTree', defaultOverpassTree),
|
selectedOverpassTree: new Setting('selectedOverpassTree', defaultOverpassTree),
|
||||||
opacities: new Setting(db, 'opacities', defaultOpacities),
|
opacities: new Setting('opacities', defaultOpacities),
|
||||||
customLayers: new Setting<Record<string, CustomLayer>>(db, 'customLayers', {}),
|
customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}),
|
||||||
customBasemapOrder: new Setting<string[]>(db, 'customBasemapOrder', []),
|
customBasemapOrder: new Setting<string[]>('customBasemapOrder', []),
|
||||||
customOverlayOrder: new Setting<string[]>(db, 'customOverlayOrder', []),
|
customOverlayOrder: new Setting<string[]>('customOverlayOrder', []),
|
||||||
directionMarkers: new Setting(db, 'directionMarkers', false),
|
directionMarkers: new Setting('directionMarkers', false),
|
||||||
distanceMarkers: new Setting(db, 'distanceMarkers', false),
|
distanceMarkers: new Setting('distanceMarkers', false),
|
||||||
streetViewSource: new Setting(db, 'streetViewSource', 'mapillary'),
|
streetViewSource: new Setting('streetViewSource', 'mapillary'),
|
||||||
fileOrder: new Setting<string[]>(db, 'fileOrder', []),
|
fileOrder: new Setting<string[]>('fileOrder', []),
|
||||||
defaultOpacity: new Setting(db, 'defaultOpacity', 0.7),
|
defaultOpacity: new Setting('defaultOpacity', 0.7),
|
||||||
defaultWidth: new Setting(db, 'defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),
|
defaultWidth: new Setting('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),
|
||||||
bottomPanelSize: new Setting(db, 'bottomPanelSize', 170),
|
bottomPanelSize: new Setting('bottomPanelSize', 170),
|
||||||
rightPanelSize: new Setting(db, 'rightPanelSize', 240),
|
rightPanelSize: new Setting('rightPanelSize', 240),
|
||||||
|
connectToDatabase(db: Database) {
|
||||||
|
for (const key in settings) {
|
||||||
|
const setting = (settings as any)[key];
|
||||||
|
if (setting instanceof Setting || setting instanceof SettingInitOnFirstRead) {
|
||||||
|
setting.connectToDatabase(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disconnectFromDatabase() {
|
||||||
|
for (const key in settings) {
|
||||||
|
const setting = (settings as any)[key];
|
||||||
|
if (setting instanceof Setting || setting instanceof SettingInitOnFirstRead) {
|
||||||
|
setting.disconnectFromDatabase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import Nav from '$lib/components/Nav.svelte';
|
import Nav from '$lib/components/Nav.svelte';
|
||||||
import Footer from '$lib/components/Footer.svelte';
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
import { onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import { convertOldEmbeddingOptions } from '$lib/components/embedding/Embedding';
|
import { convertOldEmbeddingOptions } from '$lib/components/embedding/embedding';
|
||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
import { languages } from '$lib/languages';
|
import { languages } from '$lib/languages';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import Logo from '$lib/components/Logo.svelte';
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { Home, Map, BookOpenText } from '@lucide/svelte';
|
import { House, Map, BookOpenText } from '@lucide/svelte';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
href={getURLForLanguage(i18n.lang, '/')}
|
href={getURLForLanguage(i18n.lang, '/')}
|
||||||
class="text-base w-1/4 min-w-fit rounded-full"
|
class="text-base w-1/4 min-w-fit rounded-full"
|
||||||
>
|
>
|
||||||
<Home size="18" />
|
<House size="18" />
|
||||||
{i18n._('homepage.home')}
|
{i18n._('homepage.home')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -16,10 +16,12 @@
|
|||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { loadFiles } from '$lib/logic/file-actions';
|
import { loadFiles } from '$lib/logic/file-actions';
|
||||||
import { onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
|
||||||
import { getURLForGoogleDriveFile } from '$lib/components/embedding/Embedding';
|
import { getURLForGoogleDriveFile } from '$lib/components/embedding/embedding';
|
||||||
|
import { db } from '$lib/db';
|
||||||
|
import { fileStateCollection } from '$lib/logic/file-state';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
treeFileView,
|
treeFileView,
|
||||||
@@ -49,6 +51,14 @@
|
|||||||
loadFiles(files.filter((file) => file !== null));
|
loadFiles(files.filter((file) => file !== null));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileStateCollection.connectToDatabase(db);
|
||||||
|
settings.connectToDatabase(db);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
fileStateCollection.disconnectFromDatabase();
|
||||||
|
settings.disconnectFromDatabase();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
// import Embedding from '$lib/components/embedding/Embedding.svelte';
|
import Embedding from '$lib/components/embedding/Embedding.svelte';
|
||||||
import {
|
import {
|
||||||
getMergedEmbeddingOptions,
|
getMergedEmbeddingOptions,
|
||||||
type EmbeddingOptions,
|
type EmbeddingOptions,
|
||||||
} from '$lib/components/embedding/Embedding';
|
} from '$lib/components/embedding/embedding';
|
||||||
|
|
||||||
let embeddingOptions: EmbeddingOptions | undefined = undefined;
|
let embeddingOptions: EmbeddingOptions | undefined = undefined;
|
||||||
|
|
||||||
@@ -23,5 +23,5 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if embeddingOptions}
|
{#if embeddingOptions}
|
||||||
<!-- <Embedding options={embeddingOptions} hash={$page.url.hash} /> -->
|
<Embedding options={embeddingOptions} hash={page.url.hash} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user