mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-02 00:32:33 +00:00
custom layers
This commit is contained in:
@@ -266,12 +266,15 @@ export const basemaps: { [key: string]: string | Style; } = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.values(basemaps).forEach((basemap) => {
|
export function extendBasemap(basemap: string | Style): string | Style {
|
||||||
if (typeof basemap === 'object') {
|
if (typeof basemap === 'object') {
|
||||||
basemap["glyphs"] = "mapbox://fonts/mapbox/{fontstack}/{range}.pbf";
|
basemap["glyphs"] = "mapbox://fonts/mapbox/{fontstack}/{range}.pbf";
|
||||||
basemap["sprite"] = `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${mapboxAccessToken}`;
|
basemap["sprite"] = `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${mapboxAccessToken}`;
|
||||||
}
|
}
|
||||||
});
|
return basemap;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.values(basemaps).forEach(extendBasemap);
|
||||||
|
|
||||||
export const font: { [key: string]: string; } = {
|
export const font: { [key: string]: string; } = {
|
||||||
swisstopo: 'Frutiger Neue Condensed Regular',
|
swisstopo: 'Frutiger Neue Condensed Regular',
|
||||||
@@ -678,6 +681,16 @@ export const defaultOverlayTree: LayerTreeType = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CustomLayer = {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
tileUrls: string[],
|
||||||
|
maxZoom: number,
|
||||||
|
layerType: 'basemap' | 'overlay',
|
||||||
|
resourceType: 'raster' | 'vector',
|
||||||
|
value: string | {},
|
||||||
|
};
|
||||||
|
|
||||||
export const stravaHeatmapServers = ['https://heatmap-external-a.strava.com/tiles-auth', 'https://heatmap-external-b.strava.com/tiles-auth', 'https://heatmap-external-c.strava.com/tiles-auth'];
|
export const stravaHeatmapServers = ['https://heatmap-external-a.strava.com/tiles-auth', 'https://heatmap-external-b.strava.com/tiles-auth', 'https://heatmap-external-c.strava.com/tiles-auth'];
|
||||||
export const stravaHeatmapActivityIds: { [key: string]: string } = {
|
export const stravaHeatmapActivityIds: { [key: string]: string } = {
|
||||||
stravaHeatmapRun: 'sport_Run',
|
stravaHeatmapRun: 'sport_Run',
|
||||||
|
@@ -395,8 +395,10 @@
|
|||||||
cutSelection();
|
cutSelection();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (e.key === 'v' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'v' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') {
|
||||||
pasteSelection();
|
pasteSelection();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
}
|
||||||
} else if ((e.key === 's' || e.key == 'S') && (e.metaKey || e.ctrlKey)) {
|
} else if ((e.key === 's' || e.key == 'S') && (e.metaKey || e.ctrlKey)) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
exportAllFiles();
|
exportAllFiles();
|
||||||
|
264
website/src/lib/components/layer-control/CustomLayers.svelte
Normal file
264
website/src/lib/components/layer-control/CustomLayers.svelte
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import { Label } from '$lib/components/ui/label';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||||
|
import { CirclePlus, CircleX, Minus, Pencil, Plus, Save, Trash2 } from 'lucide-svelte';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { settings } from '$lib/db';
|
||||||
|
import { defaultBasemap, extendBasemap, type CustomLayer } from '$lib/assets/layers';
|
||||||
|
import { map } from '$lib/stores';
|
||||||
|
|
||||||
|
const {
|
||||||
|
customLayers,
|
||||||
|
selectedBasemapTree,
|
||||||
|
selectedOverlayTree,
|
||||||
|
currentBasemap,
|
||||||
|
previousBasemap,
|
||||||
|
currentOverlays,
|
||||||
|
previousOverlays
|
||||||
|
} = settings;
|
||||||
|
|
||||||
|
let name: string = '';
|
||||||
|
let tileUrls: string[] = [''];
|
||||||
|
let maxZoom: number = 20;
|
||||||
|
let layerType: 'basemap' | 'overlay' = 'basemap';
|
||||||
|
let resourceType: 'raster' | 'vector' = 'raster';
|
||||||
|
|
||||||
|
$: if (tileUrls[0].length > 0) {
|
||||||
|
if (tileUrls[0].includes('.json')) {
|
||||||
|
resourceType = 'vector';
|
||||||
|
layerType = 'basemap';
|
||||||
|
} else {
|
||||||
|
resourceType = 'raster';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLayer() {
|
||||||
|
let layerId = selectedLayerId ?? getLayerId();
|
||||||
|
let layer: CustomLayer = {
|
||||||
|
id: layerId,
|
||||||
|
name: name,
|
||||||
|
tileUrls: tileUrls,
|
||||||
|
maxZoom: maxZoom,
|
||||||
|
layerType: layerType,
|
||||||
|
resourceType: resourceType,
|
||||||
|
value: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
if (resourceType === 'vector') {
|
||||||
|
layer.value = tileUrls[0];
|
||||||
|
} else {
|
||||||
|
if (layerType === 'basemap') {
|
||||||
|
layer.value = extendBasemap({
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
[layerId]: {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: tileUrls,
|
||||||
|
maxzoom: maxZoom
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: layerId,
|
||||||
|
type: 'raster',
|
||||||
|
source: layerId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
layer.value = {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: tileUrls,
|
||||||
|
maxzoom: maxZoom
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$customLayers[layerId] = layer;
|
||||||
|
addLayer(layerId);
|
||||||
|
selectedLayerId = undefined;
|
||||||
|
setDataFromSelectedLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLayerId() {
|
||||||
|
for (let id = 0; ; id++) {
|
||||||
|
if (!$customLayers.hasOwnProperty(`custom-${id}`)) {
|
||||||
|
return `custom-${id}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLayer(layerId: string) {
|
||||||
|
if (layerType === 'basemap') {
|
||||||
|
if (!$selectedBasemapTree.basemaps.hasOwnProperty('custom')) {
|
||||||
|
$selectedBasemapTree.basemaps['custom'] = {};
|
||||||
|
}
|
||||||
|
$selectedBasemapTree.basemaps['custom'][layerId] = true;
|
||||||
|
} else {
|
||||||
|
if (!$selectedOverlayTree.overlays.hasOwnProperty('custom')) {
|
||||||
|
$selectedOverlayTree.overlays['custom'] = {};
|
||||||
|
}
|
||||||
|
$selectedOverlayTree.overlays['custom'][layerId] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryDeleteLayer(node: any, id: string): any {
|
||||||
|
if (node.hasOwnProperty(id)) {
|
||||||
|
delete node[id];
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteLayer(layerId: string) {
|
||||||
|
let layer = $customLayers[layerId];
|
||||||
|
if (layer.layerType === 'basemap') {
|
||||||
|
if (layerId === $currentBasemap) {
|
||||||
|
$currentBasemap = defaultBasemap;
|
||||||
|
}
|
||||||
|
if (layerId === $previousBasemap) {
|
||||||
|
$previousBasemap = defaultBasemap;
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedBasemapTree.basemaps['custom'] = tryDeleteLayer(
|
||||||
|
$selectedBasemapTree.basemaps['custom'],
|
||||||
|
layerId
|
||||||
|
);
|
||||||
|
if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) {
|
||||||
|
$selectedBasemapTree.basemaps = tryDeleteLayer($selectedBasemapTree.basemaps, 'custom');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$currentOverlays = tryDeleteLayer($currentOverlays, layerId);
|
||||||
|
$previousOverlays = tryDeleteLayer($previousOverlays, layerId);
|
||||||
|
|
||||||
|
$selectedOverlayTree.overlays['custom'] = tryDeleteLayer(
|
||||||
|
$selectedOverlayTree.overlays['custom'],
|
||||||
|
layerId
|
||||||
|
);
|
||||||
|
if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) {
|
||||||
|
$selectedOverlayTree.overlays = tryDeleteLayer($selectedOverlayTree.overlays, 'custom');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($map) {
|
||||||
|
if ($map.getLayer(layerId)) {
|
||||||
|
$map.removeLayer(layerId);
|
||||||
|
}
|
||||||
|
if ($map.getSource(layerId)) {
|
||||||
|
$map.removeSource(layerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$customLayers = tryDeleteLayer($customLayers, layerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedLayerId: string | undefined = undefined;
|
||||||
|
|
||||||
|
function setDataFromSelectedLayer() {
|
||||||
|
if (selectedLayerId) {
|
||||||
|
const layer = $customLayers[selectedLayerId];
|
||||||
|
name = layer.name;
|
||||||
|
tileUrls = layer.tileUrls;
|
||||||
|
maxZoom = layer.maxZoom;
|
||||||
|
layerType = layer.layerType;
|
||||||
|
resourceType = layer.resourceType;
|
||||||
|
} else {
|
||||||
|
name = '';
|
||||||
|
tileUrls = [''];
|
||||||
|
maxZoom = 20;
|
||||||
|
layerType = 'basemap';
|
||||||
|
resourceType = 'raster';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: selectedLayerId, setDataFromSelectedLayer();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if Object.keys($customLayers).length > 0}
|
||||||
|
<div class="flex flex-col gap-1 mb-3">
|
||||||
|
{#each Object.entries($customLayers) as [id, layer] (id)}
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<span class="grow">{layer.name}</span>
|
||||||
|
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-8">
|
||||||
|
<Pencil size="16" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-8">
|
||||||
|
<Trash2 size="16" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header class="p-3">
|
||||||
|
<Card.Title class="text-base">
|
||||||
|
{#if selectedLayerId}
|
||||||
|
{$_('layers.custom_layers.edit')}
|
||||||
|
{:else}
|
||||||
|
{$_('layers.custom_layers.new')}
|
||||||
|
{/if}
|
||||||
|
</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="p-3 pt-0">
|
||||||
|
<fieldset class="flex flex-col gap-2">
|
||||||
|
<Label for="name">{$_('menu.metadata.name')}</Label>
|
||||||
|
<Input bind:value={name} id="name" class="h-8" />
|
||||||
|
<Label for="url">{$_('layers.custom_layers.urls')}</Label>
|
||||||
|
{#each tileUrls as url, i}
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<Input bind:value={tileUrls[i]} id="url" class="h-8" />
|
||||||
|
{#if tileUrls.length > 1}
|
||||||
|
<Button
|
||||||
|
on:click={() => (tileUrls = tileUrls.filter((_, index) => index !== i))}
|
||||||
|
variant="outline"
|
||||||
|
class="p-1 h-8"
|
||||||
|
>
|
||||||
|
<Minus size="16" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if i === tileUrls.length - 1}
|
||||||
|
<Button
|
||||||
|
on:click={() => (tileUrls = [...tileUrls, ''])}
|
||||||
|
variant="outline"
|
||||||
|
class="p-1 h-8"
|
||||||
|
>
|
||||||
|
<Plus size="16" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{#if resourceType === 'raster'}
|
||||||
|
<Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label>
|
||||||
|
<Input type="number" bind:value={maxZoom} id="maxZoom" min={0} max={22} class="h-8" />
|
||||||
|
{/if}
|
||||||
|
<Label>{$_('layers.custom_layers.layer_type')}</Label>
|
||||||
|
<RadioGroup.Root bind:value={layerType} class="flex flex-row">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="basemap" id="basemap" />
|
||||||
|
<Label for="basemap">{$_('layers.custom_layers.basemap')}</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="overlay" id="overlay" disabled={resourceType === 'vector'} />
|
||||||
|
<Label for="overlay">{$_('layers.custom_layers.overlay')}</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
{#if selectedLayerId}
|
||||||
|
<div class="mt-2 flex flex-row gap-2">
|
||||||
|
<Button variant="outline" on:click={createLayer} class="grow">
|
||||||
|
<Save size="16" class="mr-1" />
|
||||||
|
{$_('layers.custom_layers.update')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" on:click={() => (selectedLayerId = undefined)}>
|
||||||
|
<CircleX size="16" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Button variant="outline" class="mt-2" on:click={createLayer}>
|
||||||
|
<CirclePlus size="16" class="mr-1" />
|
||||||
|
{$_('layers.custom_layers.create')}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</fieldset>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
@@ -18,12 +18,16 @@
|
|||||||
previousBasemap,
|
previousBasemap,
|
||||||
currentOverlays,
|
currentOverlays,
|
||||||
selectedBasemapTree,
|
selectedBasemapTree,
|
||||||
selectedOverlayTree
|
selectedOverlayTree,
|
||||||
|
customLayers
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
$: if ($map) {
|
$: if ($map) {
|
||||||
// Set style depending on the current basemap
|
// Set style depending on the current basemap
|
||||||
$map.setStyle(basemaps[$currentBasemap], {
|
let basemap = basemaps.hasOwnProperty($currentBasemap)
|
||||||
|
? basemaps[$currentBasemap]
|
||||||
|
: $customLayers[$currentBasemap].value;
|
||||||
|
$map.setStyle(basemap, {
|
||||||
diff: false
|
diff: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -65,17 +69,18 @@
|
|||||||
return () => {
|
return () => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
try {
|
try {
|
||||||
|
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
|
||||||
if (!$map.getSource(id)) {
|
if (!$map.getSource(id)) {
|
||||||
$map.addSource(id, overlays[id]);
|
$map.addSource(id, overlay);
|
||||||
}
|
}
|
||||||
$map.addLayer(
|
$map.addLayer(
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
type: overlays[id].type === 'raster' ? 'raster' : 'line',
|
type: overlay.type === 'raster' ? 'raster' : 'line',
|
||||||
source: id,
|
source: id,
|
||||||
paint: {
|
paint: {
|
||||||
...(id in opacities
|
...(id in opacities
|
||||||
? overlays[id].type === 'raster'
|
? overlay.type === 'raster'
|
||||||
? { 'raster-opacity': opacities[id] }
|
? { 'raster-opacity': opacities[id] }
|
||||||
: { 'line-opacity': opacities[id] }
|
: { 'line-opacity': opacities[id] }
|
||||||
: {})
|
: {})
|
||||||
|
@@ -15,6 +15,7 @@
|
|||||||
import { writable, get } from 'svelte/store';
|
import { writable, get } from 'svelte/store';
|
||||||
import { map, setStravaHeatmapURLs } from '$lib/stores';
|
import { map, setStravaHeatmapURLs } from '$lib/stores';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import CustomLayers from './CustomLayers.svelte';
|
||||||
|
|
||||||
const { selectedBasemapTree, selectedOverlayTree, stravaHeatmapColor, currentOverlays } =
|
const { selectedBasemapTree, selectedOverlayTree, stravaHeatmapColor, currentOverlays } =
|
||||||
settings;
|
settings;
|
||||||
@@ -111,9 +112,11 @@
|
|||||||
</Accordion.Content>
|
</Accordion.Content>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
<Accordion.Item value="item-2">
|
<Accordion.Item value="item-2">
|
||||||
<Accordion.Trigger>{$_('layers.custom_layers')}</Accordion.Trigger>
|
<Accordion.Trigger>{$_('layers.custom_layers.title')}</Accordion.Trigger>
|
||||||
<Accordion.Content>
|
<Accordion.Content>
|
||||||
<ScrollArea></ScrollArea>
|
<ScrollArea>
|
||||||
|
<CustomLayers />
|
||||||
|
</ScrollArea>
|
||||||
</Accordion.Content>
|
</Accordion.Content>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
<Accordion.Item value="item-3">
|
<Accordion.Item value="item-3">
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
import { anySelectedLayer } from './utils';
|
import { anySelectedLayer } from './utils';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { settings } from '$lib/db';
|
||||||
|
|
||||||
export let name: string;
|
export let name: string;
|
||||||
export let node: LayerTreeType;
|
export let node: LayerTreeType;
|
||||||
@@ -15,6 +16,9 @@
|
|||||||
|
|
||||||
export let checked: LayerTreeType;
|
export let checked: LayerTreeType;
|
||||||
|
|
||||||
|
const { customLayers } = settings;
|
||||||
|
|
||||||
|
$: if (checked !== undefined) {
|
||||||
Object.keys(node).forEach((id) => {
|
Object.keys(node).forEach((id) => {
|
||||||
if (!checked.hasOwnProperty(id)) {
|
if (!checked.hasOwnProperty(id)) {
|
||||||
if (typeof node[id] == 'boolean') {
|
if (typeof node[id] == 'boolean') {
|
||||||
@@ -24,6 +28,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-[3px]">
|
<div class="flex flex-col gap-[3px]">
|
||||||
@@ -42,9 +47,13 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<input id="{name}-{id}" type="radio" {name} value={id} bind:group={selected} />
|
<input id="{name}-{id}" type="radio" {name} value={id} bind:group={selected} />
|
||||||
{/if}
|
{/if}
|
||||||
<Label for="{name}-{id}" class="flex flex-row items-center gap-1"
|
<Label for="{name}-{id}" class="flex flex-row items-center gap-1">
|
||||||
>{$_(`layers.label.${id}`)}</Label
|
{#if $customLayers.hasOwnProperty(id)}
|
||||||
>
|
{$customLayers[id].name}
|
||||||
|
{:else}
|
||||||
|
{$_(`layers.label.${id}`)}
|
||||||
|
{/if}
|
||||||
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if anySelectedLayer(node[id])}
|
{:else if anySelectedLayer(node[id])}
|
||||||
|
@@ -4,7 +4,7 @@ import { enableMapSet, enablePatches, applyPatches, type Patch, type WritableDra
|
|||||||
import { writable, get, derived, type Readable, type Writable } from 'svelte/store';
|
import { writable, get, derived, type Readable, type Writable } from 'svelte/store';
|
||||||
import { gpxStatistics, initTargetMapBounds, splitAs, updateTargetMapBounds } from './stores';
|
import { gpxStatistics, initTargetMapBounds, splitAs, updateTargetMapBounds } from './stores';
|
||||||
import { mode } from 'mode-watcher';
|
import { mode } from 'mode-watcher';
|
||||||
import { defaultBasemap, defaultBasemapTree, defaultOverlayTree, defaultOverlays } from './assets/layers';
|
import { defaultBasemap, defaultBasemapTree, defaultOverlayTree, defaultOverlays, type CustomLayer } from './assets/layers';
|
||||||
import { applyToOrderedItemsFromFile, applyToOrderedSelectedItemsFromFile, selection } from '$lib/components/file-list/Selection';
|
import { applyToOrderedItemsFromFile, applyToOrderedSelectedItemsFromFile, selection } from '$lib/components/file-list/Selection';
|
||||||
import { ListFileItem, ListItem, ListTrackItem, ListLevel, ListTrackSegmentItem, ListWaypointItem, ListRootItem } from '$lib/components/file-list/FileList';
|
import { ListFileItem, ListItem, ListTrackItem, ListLevel, ListTrackSegmentItem, ListWaypointItem, ListRootItem } from '$lib/components/file-list/FileList';
|
||||||
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify';
|
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify';
|
||||||
@@ -48,13 +48,13 @@ function dexieSettingStore<T>(setting: string, initial: T): Writable<T> {
|
|||||||
return {
|
return {
|
||||||
subscribe: store.subscribe,
|
subscribe: store.subscribe,
|
||||||
set: (value: any) => {
|
set: (value: any) => {
|
||||||
if (value !== get(store)) {
|
if (typeof value === 'object' || value !== get(store)) {
|
||||||
db.settings.put(value, setting);
|
db.settings.put(value, setting);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update: (callback: (value: any) => any) => {
|
update: (callback: (value: any) => any) => {
|
||||||
let newValue = callback(get(store));
|
let newValue = callback(get(store));
|
||||||
if (newValue !== get(store)) {
|
if (typeof newValue === 'object' || newValue !== get(store)) {
|
||||||
db.settings.put(newValue, setting);
|
db.settings.put(newValue, setting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,6 +104,7 @@ export const settings = {
|
|||||||
currentOverlays: dexieUninitializedSettingStore('currentOverlays', defaultOverlays),
|
currentOverlays: dexieUninitializedSettingStore('currentOverlays', defaultOverlays),
|
||||||
previousOverlays: dexieSettingStore('previousOverlays', defaultOverlays),
|
previousOverlays: dexieSettingStore('previousOverlays', defaultOverlays),
|
||||||
selectedOverlayTree: dexieSettingStore('selectedOverlayTree', defaultOverlayTree),
|
selectedOverlayTree: dexieSettingStore('selectedOverlayTree', defaultOverlayTree),
|
||||||
|
customLayers: dexieSettingStore<Record<string, CustomLayer>>('customLayers', {}),
|
||||||
directionMarkers: dexieSettingStore('directionMarkers', false),
|
directionMarkers: dexieSettingStore('directionMarkers', false),
|
||||||
distanceMarkers: dexieSettingStore('distanceMarkers', false),
|
distanceMarkers: dexieSettingStore('distanceMarkers', false),
|
||||||
stravaHeatmapColor: dexieSettingStore('stravaHeatmapColor', 'bluered'),
|
stravaHeatmapColor: dexieSettingStore('stravaHeatmapColor', 'bluered'),
|
||||||
|
@@ -198,12 +198,24 @@
|
|||||||
"settings": "Layer settings",
|
"settings": "Layer settings",
|
||||||
"settings_help": "Select the map layers you want to show in the interface, add custom ones, and adjust their settings.",
|
"settings_help": "Select the map layers you want to show in the interface, add custom ones, and adjust their settings.",
|
||||||
"selection": "Layer selection",
|
"selection": "Layer selection",
|
||||||
"custom_layers": "Custom layers",
|
"custom_layers": {
|
||||||
|
"title": "Custom layers",
|
||||||
|
"new": "New custom layer",
|
||||||
|
"edit": "Edit custom layer",
|
||||||
|
"urls": "URL(s)",
|
||||||
|
"max_zoom": "Max zoom",
|
||||||
|
"layer_type": "Layer type",
|
||||||
|
"basemap": "Basemap",
|
||||||
|
"overlay": "Overlay",
|
||||||
|
"create": "Create layer",
|
||||||
|
"update": "Update layer"
|
||||||
|
},
|
||||||
"heatmap": "Strava Heatmap",
|
"heatmap": "Strava Heatmap",
|
||||||
"pois": "Points of interest",
|
"pois": "Points of interest",
|
||||||
"label": {
|
"label": {
|
||||||
"basemaps": "Basemaps",
|
"basemaps": "Basemaps",
|
||||||
"overlays": "Overlays",
|
"overlays": "Overlays",
|
||||||
|
"custom": "Custom",
|
||||||
"world": "World",
|
"world": "World",
|
||||||
"countries": "Countries",
|
"countries": "Countries",
|
||||||
"belgium": "Belgium",
|
"belgium": "Belgium",
|
||||||
|
Reference in New Issue
Block a user