10 Commits

Author SHA1 Message Date
vcoppe
b2a5462372 improve website link 2025-11-09 20:14:52 +01:00
vcoppe
9d61f51270 fix spacing 2025-11-09 20:11:35 +01:00
vcoppe
a0eb7d61cc remove swedish map 2025-11-09 20:05:00 +01:00
vcoppe
9861b319f4 fix popup max height 2025-11-09 19:52:02 +01:00
vcoppe
b04e0f10b2 resize map on load 2025-11-09 19:20:10 +01:00
vcoppe
e6d089b34b close -> delete 2025-11-09 19:09:16 +01:00
vcoppe
9df014e986 fix sortable 2025-11-09 19:00:33 +01:00
vcoppe
39b8d2e70d initialize missing settings 2025-11-09 18:45:20 +01:00
vcoppe
59710d2e1a fix embedding + playground 2025-11-09 18:03:27 +01:00
vcoppe
ec3eb387e5 sortable file list, to be fixed 2025-11-02 16:01:17 +01:00
22 changed files with 434 additions and 616 deletions

View File

@@ -276,68 +276,6 @@ export const basemaps: { [key: string]: string | StyleSpecification } = {
},
],
},
swedenTopo: {
version: 8,
sources: {
swedenTopoWMTS: {
type: 'raster',
tiles: [
'https://api.lantmateriet.se/open/topowebb-ccby/v1/wmts/token/1d54dd14-a28c-38a9-b6f3-b4ebfcc3c204/1.0.0/topowebb/default/3857/{z}/{y}/{x}.png',
],
tileSize: 256,
maxzoom: 14,
attribution:
'&copy; <a href="https://www.lantmateriet.se" target="_blank">Lantmäteriet</a>',
},
swedenTopoWMS: {
type: 'raster',
tiles: [
'https://minkarta.lantmateriet.se/map/topowebb?REQUEST=GetMap&SERVICE=WMS&VERSION=1.1.1&FORMAT=image%2Fpng&STYLES=&TRANSPARENT=false&LAYERS=topowebbkartan&TILED=true&MAP_RESOLUTION=180&WIDTH=512&HEIGHT=512&SRS=EPSG%3A3857&BBOX={bbox-epsg-3857}',
],
tileSize: 512,
minzoom: 14,
maxzoom: 20,
attribution:
'&copy; <a href="https://www.lantmateriet.se" target="_blank">Lantmäteriet</a>',
},
},
layers: [
{
id: 'swedenTopoWMTS',
type: 'raster',
source: 'swedenTopoWMTS',
maxzoom: 14,
},
{
id: 'swedenTopoWMS',
type: 'raster',
source: 'swedenTopoWMS',
minzoom: 14,
},
],
},
swedenSatellite: {
version: 8,
sources: {
swedenSatellite: {
type: 'raster',
tiles: [
'https://minkarta.lantmateriet.se/map/ortofoto?REQUEST=GetMap&SERVICE=WMS&VERSION=1.1.1&FORMAT=image%2Fpng&STYLES=&TRANSPARENT=false&LAYERS=Ortofoto_0.5%2COrtofoto_0.4%2COrtofoto_0.25%2COrtofoto_0.16&TILED=true&MAP_RESOLUTION=180&WIDTH=512&HEIGHT=512&SRS=EPSG%3A3857&BBOX={bbox-epsg-3857}',
],
tileSize: 512,
maxzoom: 22,
attribution:
'&copy; <a href="https://www.lantmateriet.se" target="_blank">Lantmäteriet</a>',
},
},
layers: [
{
id: 'swedenSatellite',
type: 'raster',
source: 'swedenSatellite',
},
],
},
finlandTopo: {
version: 8,
sources: {
@@ -833,10 +771,6 @@ export const basemapTree: LayerTreeType = {
ignEs: true,
ignEsSatellite: true,
},
sweden: {
swedenTopo: true,
swedenSatellite: true,
},
switzerland: {
swisstopoRaster: true,
swisstopoVector: true,
@@ -1054,10 +988,6 @@ export const defaultBasemapTree: LayerTreeType = {
ignEs: false,
ignEsSatellite: false,
},
sweden: {
swedenTopo: false,
swedenSatellite: false,
},
switzerland: {
swisstopoRaster: false,
swisstopoVector: false,

View File

@@ -148,7 +148,7 @@
disabled={$selection.size == 0}
>
<FileX size="16" />
{i18n._('menu.close')}
{i18n._('menu.delete')}
<Shortcut key="⌫" ctrl={true} />
</Menubar.Item>
<Menubar.Item
@@ -156,7 +156,7 @@
disabled={fileStateCollection.size == 0}
>
<FileX size="16" />
{i18n._('menu.close_all')}
{i18n._('menu.delete_all')}
<Shortcut key="⌫" ctrl={true} shift={true} />
</Menubar.Item>
<Menubar.Separator />

View File

@@ -594,6 +594,7 @@ export class ElevationProfile {
destroy() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
if (this._marker) {
this._marker.remove();

View File

@@ -1,40 +1,34 @@
<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 { 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 { 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 +40,74 @@
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',
};
settings.initialize();
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 +115,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>

View File

@@ -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>

View File

@@ -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

View File

@@ -28,16 +28,7 @@
setContext('recursive', recursive);
onMount(() => {
if (orientation === 'vertical') {
selection.update(($selection) => {
$selection.forEach((item) => {
if ($selection.hasAnyChildren(item, false)) {
$selection.toggle(item);
}
});
return $selection;
});
} else {
if (orientation === 'horizontal') {
selection.update(($selection) => {
$selection.forEach((item) => {
if (!(item instanceof ListFileItem)) {

View File

@@ -53,6 +53,12 @@
);
});
$effect(() => {
if (sortable && node) {
sortable.updateElements();
}
});
onDestroy(() => {
sortable.destroy();
});

View File

@@ -1,5 +1,5 @@
import { isMac } from '$lib/utils';
import Sortable, { type Direction } from 'sortablejs';
import Sortable, { type Direction } from 'sortablejs/Sortable';
import { ListItem, ListLevel, ListRootItem } from './file-list';
import { selection } from '$lib/logic/selection';
import { getFileIds, moveItems } from '$lib/logic/file-actions';
@@ -7,6 +7,7 @@ import { get, writable, type Readable } from 'svelte/store';
import { settings } from '$lib/logic/settings';
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
import type { AnyGPXTreeElement, GPXTreeElement, Waypoint } from 'gpx';
import { tick } from 'svelte';
const { fileOrder } = settings;
@@ -41,6 +42,7 @@ export class SortableFileList {
private _container: HTMLElement;
private _sortable: Sortable | null = null;
private _elements: { [id: string]: HTMLElement } = {};
private _updatingSelection: boolean = false;
private _unsubscribes: (() => void)[] = [];
constructor(
@@ -70,75 +72,13 @@ export class SortableFileList {
multiDrag: true,
multiDragKey: isMac() ? 'Meta' : 'Ctrl',
avoidImplicitDeselect: true,
onSelect: (e) => this.updateToSelection(e),
onDeselect: (e) => this.updateToSelection(e),
onStart: () => {
dragging.set(sortableLevel);
},
onEnd: () => {
dragging.set(null);
},
onSort: (e: Sortable.SortableEvent) => {
this.updateToFileOrder();
const from = Sortable.get(e.from);
const to = Sortable.get(e.to);
if (!from || !to) {
return;
}
let fromItem = from._item;
let toItem = to._item;
console.log('onSort', e);
if (item === toItem && !(fromItem instanceof ListRootItem)) {
// Event is triggered on source and destination list, only handle it once
let fromItems = [];
let toItems = [];
if (from._waypointRoot) {
fromItems = [fromItem.extend('waypoints')];
} else {
let oldIndices: number[] =
e.oldIndicies.length > 0
? e.oldIndicies.map((i) => i.index)
: [e.oldIndex];
oldIndices = oldIndices.filter((i) => i >= 0);
oldIndices.sort((a, b) => a - b);
fromItems = oldIndices.map((i) => fromItem.extend(i));
}
if (from._waypointRoot && to._waypointRoot) {
toItems = [toItem.extend('waypoints')];
} else {
if (to._waypointRoot) {
toItem = toItem.extend('waypoints');
}
let newIndices: number[] =
e.newIndicies.length > 0
? e.newIndicies.map((i) => i.index)
: [e.newIndex];
newIndices = newIndices.filter((i) => i >= 0);
newIndices.sort((a, b) => a - b);
if (toItem instanceof ListRootItem) {
let newFileIds = getFileIds(newIndices.length);
toItems = newIndices.map((i, index) => {
get(fileOrder).splice(i, 0, newFileIds[index]);
return item.extend(newFileIds[index]);
});
} else {
toItems = newIndices.map((i) => toItem.extend(i));
}
}
moveItems(fromItem, toItem, fromItems, toItems);
}
},
onSelect: (e: Sortable.SortableEvent) =>
setTimeout(() => this.updateToSelection(e), 50),
onDeselect: (e: Sortable.SortableEvent) =>
setTimeout(() => this.updateToSelection(e), 50),
onStart: () => dragging.set(sortableLevel),
onEnd: () => dragging.set(null),
onSort: (e: Sortable.SortableEvent) => this.onSort(e),
});
Object.defineProperty(this._sortable, '_item', {
value: item,
@@ -150,47 +90,73 @@ export class SortableFileList {
writable: true,
});
this._unsubscribes.push(selection.subscribe(() => this.updateFromSelection()));
this._unsubscribes.push(
selection.subscribe(() => tick().then(() => this.updateFromSelection()))
);
this._unsubscribes.push(fileOrder.subscribe(() => this.updateFromFileOrder()));
}
updateToSelection(e: Sortable.SortableEvent) {
console.log('updateToSelection', e);
onSort(e: Sortable.SortableEvent) {
this.updateToFileOrder();
let changed = this.getChangedIds();
if (changed.length > 0) {
selection.update(($selection) => {
$selection.clear();
Object.entries(this._elements).forEach(([id, element]) => {
$selection.set(
this._item.extend(this.getRealId(id)),
element.classList.contains('sortable-selected')
);
});
const from = Sortable.get(e.from);
const to = Sortable.get(e.to);
if (
e.originalEvent &&
!(
e.originalEvent.ctrlKey ||
e.originalEvent.metaKey ||
e.originalEvent.shiftKey
) &&
($selection.size > 1 ||
!$selection.has(this._item.extend(this.getRealId(changed[0]))))
) {
// Fix bug that sometimes causes a single select to be treated as a multi-select
$selection.clear();
$selection.set(this._item.extend(this.getRealId(changed[0])), true);
if (!from || !to) {
return;
}
let fromItem = from._item;
let toItem = to._item;
if (this._item === toItem && !(fromItem instanceof ListRootItem)) {
// Event is triggered on source and destination list, only handle it once
let fromItems = [];
let toItems = [];
if (from._waypointRoot) {
fromItems = [fromItem.extend('waypoints')];
} else {
let oldIndices: number[] =
e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex];
oldIndices = oldIndices.filter((i) => i >= 0);
oldIndices.sort((a, b) => a - b);
fromItems = oldIndices.map((i) => fromItem.extend(i));
}
if (from._waypointRoot && to._waypointRoot) {
toItems = [toItem.extend('waypoints')];
} else {
if (to._waypointRoot) {
toItem = toItem.extend('waypoints');
}
return $selection;
});
let newIndices: number[] =
e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex];
newIndices = newIndices.filter((i) => i >= 0);
newIndices.sort((a, b) => a - b);
if (toItem instanceof ListRootItem) {
let newFileIds = getFileIds(newIndices.length);
toItems = newIndices.map((i, index) => {
get(fileOrder).splice(i, 0, newFileIds[index]);
return this._item.extend(newFileIds[index]);
});
} else {
toItems = newIndices.map((i) => toItem.extend(i));
}
}
moveItems(fromItem, toItem, fromItems, toItems);
}
}
updateFromSelection() {
console.log('updateFromSelection');
let changed = this.getChangedIds();
const changed = this.getChangedIds();
if (changed.length === 0) {
return;
}
const selection_ = get(selection);
for (let id of changed) {
let element = this._elements[id];
@@ -208,11 +174,43 @@ export class SortableFileList {
}
}
updateToSelection(e: Sortable.SortableEvent) {
if (!this._sortable) return;
if (this._updatingSelection) return;
this._updatingSelection = true;
const changed = this.getChangedIds();
if (changed.length == 0) {
this._updatingSelection = false;
return;
}
selection.update(($selection) => {
$selection.clear();
Object.entries(this._elements).forEach(([id, element]) => {
$selection.set(
this._item.extend(this.getRealId(id)),
element.classList.contains('sortable-selected')
);
});
if (
e.originalEvent &&
!(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey) &&
($selection.size > 1 ||
!$selection.has(this._item.extend(this.getRealId(changed[0]))))
) {
// Fix bug that sometimes causes a single select to be treated as a multi-select
$selection.clear();
$selection.set(this._item.extend(this.getRealId(changed[0])), true);
}
return $selection;
});
this._updatingSelection = false;
}
updateFromFileOrder() {
if (!this._sortable || this._sortableLevel !== ListLevel.FILE) {
return;
}
console.log('updateFromFileOrder');
const fileOrder_ = get(fileOrder);
const sortableOrder = this._sortable.toArray();
@@ -229,7 +227,6 @@ export class SortableFileList {
if (!this._sortable || this._sortableLevel !== ListLevel.FILE) {
return;
}
console.log('updateToFileOrder');
const fileOrder_ = get(fileOrder);
const sortableOrder = this._sortable.toArray();
@@ -256,12 +253,10 @@ export class SortableFileList {
}
}
});
// syncFileOrder();
// updateFromSelection();
}
destroy() {
this._sortable = null;
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
this._unsubscribes = [];
}

View File

@@ -22,6 +22,7 @@ export class StartEndMarkers {
this.start = new mapboxgl.Marker({ element: startElement });
this.end = new mapboxgl.Marker({ element: endElement });
map.onLoad(() => this.update());
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
this.unsubscribes.push(slicedGPXStatistics.subscribe(this.updateBinded));
this.unsubscribes.push(currentTool.subscribe(this.updateBinded));

View File

@@ -47,8 +47,8 @@
}
</script>
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
<Card.Header class="p-0">
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw] gap-0">
<Card.Header class="p-0 gap-0">
<Card.Title class="text-md">
<div class="flex flex-row gap-3">
<div class="flex flex-col">
@@ -70,7 +70,7 @@
</Card.Title>
</Card.Header>
<Card.Content class="flex flex-col p-0 text-sm mt-1 whitespace-normal break-all">
<ScrollArea class="flex flex-col" viewportClasses="max-h-[30dvh]">
<ScrollArea class="flex flex-col max-h-[30dvh]">
{#if tags.image || tags['image:0']}
<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
<!-- svelte-ignore a11y-missing-attribute -->
@@ -81,7 +81,7 @@
{#each Object.entries(tags) as [key, value]}
{#if key !== 'name' && !key.includes('image')}
<span class="font-mono">{key}</span>
{#if key === 'website' || key.startsWith('website:') || key === 'contact:website' || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
{#if key === 'website' || key.startsWith('website:') || key.endsWith(':website') || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
<a href={value} target="_blank" class="text-link underline">{value}</a>
{:else if key === 'phone' || key === 'contact:phone'}
<a href={'tel:' + value} class="text-link underline">{value}</a>

View File

@@ -1,6 +1,6 @@
import { TrackPoint, Waypoint } from 'gpx';
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 MapPopupComponent from '$lib/components/map/MapPopup.svelte';
@@ -69,7 +69,7 @@ export class MapPopup {
remove() {
this.popup.remove();
this.component.$destroy();
unmount(this.component);
}
getCoordinates() {

View File

@@ -166,6 +166,7 @@ export class MapboxGLMap {
map.on('load', () => {
this._map.set(map); // only set the store after the map has loaded
window._map = map; // entry point for extensions
this.resize();
scaleControl.setUnit(get(distanceUnits));
this._onLoadCallbacks.forEach((callback) => callback(map));

View File

@@ -2,61 +2,12 @@ import { get } from 'svelte/store';
import { selection } from '$lib/logic/selection';
import mapboxgl from 'mapbox-gl';
import { ListFileItem, ListWaypointItem } from '$lib/components/file-list/file-list';
import {
fileStateCollection,
GPXFileState,
GPXFileStateCollectionObserver,
} from '$lib/logic/file-state';
import { fileStateCollection, GPXFileStateCollectionObserver } from '$lib/logic/file-state';
import { gpxStatistics } from '$lib/logic/statistics';
import { map } from '$lib/components/map/map';
import type { GPXFileWithStatistics } from './statistics-tree';
import type { Coordinates } from 'gpx';
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 {
private _bounds: mapboxgl.LngLatBounds = new mapboxgl.LngLatBounds();

View File

@@ -2,11 +2,7 @@ import { db, type Database } from '$lib/db';
import { liveQuery } from 'dexie';
import type { GPXFile } from 'gpx';
import { applyPatches, produceWithPatches, type Patch, type WritableDraft } from 'immer';
import {
fileStateCollection,
GPXFileStateCollectionObserver,
type GPXFileStateCollection,
} from '$lib/logic/file-state';
import { GPXFileStateCollectionObserver } from '$lib/logic/file-state';
import {
derived,
get,
@@ -30,7 +26,7 @@ export class FileActionManager {
private _canUndo: Readable<boolean>;
private _canRedo: Readable<boolean>;
constructor(db: Database, fileStateCollection: GPXFileStateCollection) {
constructor(db: Database) {
this._db = db;
this._files = new Map();
this._fileSubscriptions = new Map();
@@ -156,7 +152,7 @@ export class FileActionManager {
selection.updateFiles(updatedFiles, deletedFileIds);
// @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) {
await this._db.fileids.bulkPut(updatedFileIds, updatedFileIds);
await this._db.files.bulkPut(updatedFiles, updatedFileIds);
@@ -254,4 +250,4 @@ function getChangedFileIds(patch: Patch[]): string[] {
return Array.from(changedFileIds);
}
export const fileActionManager = new FileActionManager(db, fileStateCollection);
export const fileActionManager = new FileActionManager(db);

View File

@@ -1,5 +1,5 @@
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 { GPXFile } from 'gpx';
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
export class GPXFileState {
private _fileId: string;
private _file: Writable<GPXFileWithStatistics | undefined>;
private _subscription: { unsubscribe: () => void } | undefined;
constructor(db: Database, fileId: string) {
this._file = writable(undefined);
constructor(fileId: string, file?: GPXFile) {
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) {
let file = new GPXFile(value);
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
export class GPXFileStateCollection {
private _files: Writable<Map<string, GPXFileState>>;
private _subscription: { unsubscribe: () => void } | null = null;
constructor(db: Database) {
constructor() {
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);
// Find new files to observe
let newFiles = dbFileIds
@@ -64,7 +73,9 @@ export class GPXFileStateCollection {
// Update the map of file states
this._files.update(($files) => {
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) => {
$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) {
return this._files.subscribe(run, invalidate);
}
@@ -117,7 +153,7 @@ export class GPXFileStateCollection {
}
// 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 class GPXFileStateCollectionObserver {

View File

@@ -1,4 +1,4 @@
import { db, type Database } from '$lib/db';
import { type Database } from '$lib/db';
import { liveQuery } from 'dexie';
import {
defaultBasemap,
@@ -14,17 +14,22 @@ import { browser } from '$app/environment';
import { get, writable, type Writable } from 'svelte/store';
export class Setting<V> {
private _db: Database;
private _db: Database | null = null;
private _subscription: { unsubscribe: () => void } | null = null;
private _key: string;
private _value: Writable<V>;
constructor(db: Database, key: string, initial: V) {
this._db = db;
constructor(key: string, initial: V) {
this._key = key;
this._value = writable(initial);
}
connectToDatabase(db: Database) {
if (this._db) return;
this._db = db;
let first = true;
liveQuery(() => db.settings.get(key)).subscribe((value) => {
this._subscription = liveQuery(() => db.settings.get(this._key)).subscribe((value) => {
if (value === undefined) {
if (!first) {
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) {
return this._value.subscribe(run, invalidate);
}
set(newValue: V) {
if (typeof newValue === 'object' || newValue !== get(this._value)) {
this._db.settings.put(newValue, this._key);
set(value: V) {
if (typeof value === 'object' || value !== get(this._value)) {
if (this._db) {
this._db.settings.put(value, this._key);
} else {
this._value.set(value);
}
}
}
update(callback: (value: any) => any) {
let newValue = callback(get(this._value));
if (typeof newValue === 'object' || newValue !== get(this._value)) {
this._db.settings.put(newValue, this._key);
}
this.set(callback(get(this._value)));
}
}
export class SettingInitOnFirstRead<V> {
private _db: Database;
private _db: Database | null = null;
private _subscription: { unsubscribe: () => void } | null = null;
private _key: string;
private _value: Writable<V | undefined>;
private _initial: V;
constructor(db: Database, key: string, initial: V) {
this._db = db;
constructor(key: string, initial: V) {
this._key = key;
this._value = writable(undefined);
this._initial = initial;
}
connectToDatabase(db: Database) {
if (this._db) return;
this._db = db;
let first = true;
liveQuery(() => db.settings.get(key)).subscribe((value) => {
this._subscription = liveQuery(() => db.settings.get(this._key)).subscribe((value) => {
if (value === undefined) {
if (first) {
this._value.set(initial);
this._value.set(this._initial);
} else {
this._value.set(value);
}
@@ -79,58 +98,92 @@ export class SettingInitOnFirstRead<V> {
});
}
initialize() {
this.set(this._initial);
}
disconnectFromDatabase() {
this._subscription?.unsubscribe();
this._subscription = null;
this._db = null;
}
subscribe(run: (value: V | undefined) => void, invalidate?: (value?: V | undefined) => void) {
return this._value.subscribe(run, invalidate);
}
set(newValue: V) {
if (typeof newValue === 'object' || newValue !== get(this._value)) {
this._db.settings.put(newValue, this._key);
set(value: V) {
if (typeof value === 'object' || value !== get(this._value)) {
if (this._db) {
this._db.settings.put(value, this._key);
} else {
this._value.set(value);
}
}
}
update(callback: (value: any) => any) {
let newValue = callback(get(this._value));
if (typeof newValue === 'object' || newValue !== get(this._value)) {
this._db.settings.put(newValue, this._key);
}
this.set(callback(get(this._value)));
}
}
export const settings = {
distanceUnits: new Setting<'metric' | 'imperial' | 'nautical'>(db, 'distanceUnits', 'metric'),
velocityUnits: new Setting<'speed' | 'pace'>(db, 'velocityUnits', 'speed'),
temperatureUnits: new Setting<'celsius' | 'fahrenheit'>(db, 'temperatureUnits', 'celsius'),
elevationProfile: new Setting<boolean>(db, 'elevationProfile', true),
additionalDatasets: new Setting<string[]>(db, 'additionalDatasets', []),
elevationFill: new Setting<'slope' | 'surface' | undefined>(db, 'elevationFill', undefined),
treeFileView: new Setting<boolean>(db, 'fileView', false),
minimizeRoutingMenu: new Setting(db, 'minimizeRoutingMenu', false),
routing: new Setting(db, 'routing', true),
routingProfile: new Setting(db, 'routingProfile', 'bike'),
privateRoads: new Setting(db, 'privateRoads', false),
currentBasemap: new Setting(db, 'currentBasemap', defaultBasemap),
previousBasemap: new Setting(db, 'previousBasemap', defaultBasemap),
selectedBasemapTree: new Setting(db, 'selectedBasemapTree', defaultBasemapTree),
currentOverlays: new SettingInitOnFirstRead(db, 'currentOverlays', defaultOverlays),
previousOverlays: new Setting(db, 'previousOverlays', defaultOverlays),
selectedOverlayTree: new Setting(db, 'selectedOverlayTree', defaultOverlayTree),
distanceUnits: new Setting<'metric' | 'imperial' | 'nautical'>('distanceUnits', 'metric'),
velocityUnits: new Setting<'speed' | 'pace'>('velocityUnits', 'speed'),
temperatureUnits: new Setting<'celsius' | 'fahrenheit'>('temperatureUnits', 'celsius'),
elevationProfile: new Setting<boolean>('elevationProfile', true),
additionalDatasets: new Setting<string[]>('additionalDatasets', []),
elevationFill: new Setting<'slope' | 'surface' | undefined>('elevationFill', undefined),
treeFileView: new Setting<boolean>('fileView', false),
minimizeRoutingMenu: new Setting('minimizeRoutingMenu', false),
routing: new Setting('routing', true),
routingProfile: new Setting('routingProfile', 'bike'),
privateRoads: new Setting('privateRoads', false),
currentBasemap: new Setting('currentBasemap', defaultBasemap),
previousBasemap: new Setting('previousBasemap', defaultBasemap),
selectedBasemapTree: new Setting('selectedBasemapTree', defaultBasemapTree),
currentOverlays: new SettingInitOnFirstRead('currentOverlays', defaultOverlays),
previousOverlays: new Setting('previousOverlays', defaultOverlays),
selectedOverlayTree: new Setting('selectedOverlayTree', defaultOverlayTree),
currentOverpassQueries: new SettingInitOnFirstRead(
db,
'currentOverpassQueries',
defaultOverpassQueries
),
selectedOverpassTree: new Setting(db, 'selectedOverpassTree', defaultOverpassTree),
opacities: new Setting(db, 'opacities', defaultOpacities),
customLayers: new Setting<Record<string, CustomLayer>>(db, 'customLayers', {}),
customBasemapOrder: new Setting<string[]>(db, 'customBasemapOrder', []),
customOverlayOrder: new Setting<string[]>(db, 'customOverlayOrder', []),
directionMarkers: new Setting(db, 'directionMarkers', false),
distanceMarkers: new Setting(db, 'distanceMarkers', false),
streetViewSource: new Setting(db, 'streetViewSource', 'mapillary'),
fileOrder: new Setting<string[]>(db, 'fileOrder', []),
defaultOpacity: new Setting(db, 'defaultOpacity', 0.7),
defaultWidth: new Setting(db, 'defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),
bottomPanelSize: new Setting(db, 'bottomPanelSize', 170),
rightPanelSize: new Setting(db, 'rightPanelSize', 240),
selectedOverpassTree: new Setting('selectedOverpassTree', defaultOverpassTree),
opacities: new Setting('opacities', defaultOpacities),
customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}),
customBasemapOrder: new Setting<string[]>('customBasemapOrder', []),
customOverlayOrder: new Setting<string[]>('customOverlayOrder', []),
directionMarkers: new Setting('directionMarkers', false),
distanceMarkers: new Setting('distanceMarkers', false),
streetViewSource: new Setting('streetViewSource', 'mapillary'),
fileOrder: new Setting<string[]>('fileOrder', []),
defaultOpacity: new Setting('defaultOpacity', 0.7),
defaultWidth: new Setting('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),
bottomPanelSize: new Setting('bottomPanelSize', 170),
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();
}
}
},
initialize() {
for (const key in settings) {
const setting = (settings as any)[key];
if (setting instanceof SettingInitOnFirstRead) {
setting.initialize();
}
}
},
};

View File

@@ -14,8 +14,6 @@
"new_segment": "New segment",
"open": "Open...",
"duplicate": "Duplicate",
"close": "Close",
"close_all": "Close all",
"copy": "Copy",
"paste": "Paste",
"cut": "Cut",
@@ -30,6 +28,7 @@
"undo": "Undo",
"redo": "Redo",
"delete": "Delete",
"delete_all": "Delete all",
"select_all": "Select all",
"view": "View",
"elevation_profile": "Elevation profile",
@@ -320,8 +319,6 @@
"ignEsSatellite": "IGN Satellite",
"ordnanceSurvey": "Ordnance Survey",
"norwayTopo": "Topografisk Norgeskart 4",
"swedenTopo": "Lantmäteriet Topo",
"swedenSatellite": "Lantmäteriet Satellite",
"finlandTopo": "Lantmäteriverket Terrängkarta",
"bgMountains": "BGMountains",
"usgs": "USGS",

View File

@@ -6,7 +6,7 @@
import Nav from '$lib/components/Nav.svelte';
import Footer from '$lib/components/Footer.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 { languages } from '$lib/languages';
import { browser } from '$app/environment';

View File

@@ -2,7 +2,7 @@
import { Button } from '$lib/components/ui/button';
import Logo from '$lib/components/Logo.svelte';
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';
</script>
@@ -15,7 +15,7 @@
href={getURLForLanguage(i18n.lang, '/')}
class="text-base w-1/4 min-w-fit rounded-full"
>
<Home size="18" />
<House size="18" />
{i18n._('homepage.home')}
</Button>
<Button

View File

@@ -16,10 +16,12 @@
import { i18n } from '$lib/i18n.svelte';
import { settings } from '$lib/logic/settings';
import { loadFiles } from '$lib/logic/file-actions';
import { onMount } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import { page } from '$app/state';
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 {
treeFileView,
@@ -49,6 +51,14 @@
loadFiles(files.filter((file) => file !== null));
});
}
settings.connectToDatabase(db);
fileStateCollection.connectToDatabase(db);
});
onDestroy(() => {
settings.disconnectFromDatabase();
fileStateCollection.disconnectFromDatabase();
});
</script>

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { page } from '$app/state';
import { onMount } from 'svelte';
// import Embedding from '$lib/components/embedding/Embedding.svelte';
import Embedding from '$lib/components/embedding/Embedding.svelte';
import {
getMergedEmbeddingOptions,
type EmbeddingOptions,
} from '$lib/components/embedding/Embedding';
} from '$lib/components/embedding/embedding';
let embeddingOptions: EmbeddingOptions | undefined = undefined;
@@ -23,5 +23,5 @@
</script>
{#if embeddingOptions}
<!-- <Embedding options={embeddingOptions} hash={$page.url.hash} /> -->
<Embedding options={embeddingOptions} hash={page.url.hash} />
{/if}