try to detect 512px custom tiles

This commit is contained in:
vcoppe
2024-10-17 11:55:13 +02:00
parent 1bda957778
commit 4ada271ad3

View File

@@ -1,435 +1,422 @@
<script lang="ts"> <script lang="ts">
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label'; import { Label } from '$lib/components/ui/label';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Separator } from '$lib/components/ui/separator'; import { Separator } from '$lib/components/ui/separator';
import * as RadioGroup from '$lib/components/ui/radio-group'; import * as RadioGroup from '$lib/components/ui/radio-group';
import { import {
CirclePlus, CirclePlus,
CircleX, CircleX,
Minus, Minus,
Pencil, Pencil,
Plus, Plus,
Save, Save,
Trash2, Trash2,
Move, Move,
Map, Map,
Layers2 Layers2
} from 'lucide-svelte'; } from 'lucide-svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { settings } from '$lib/db'; import { settings } from '$lib/db';
import { defaultBasemap, type CustomLayer } from '$lib/assets/layers'; import { defaultBasemap, type CustomLayer } from '$lib/assets/layers';
import { map } from '$lib/stores'; import { map } from '$lib/stores';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Sortable from 'sortablejs/Sortable'; import Sortable from 'sortablejs/Sortable';
import { customBasemapUpdate } from './utils'; import { customBasemapUpdate } from './utils';
const { const {
customLayers, customLayers,
selectedBasemapTree, selectedBasemapTree,
selectedOverlayTree, selectedOverlayTree,
currentBasemap, currentBasemap,
previousBasemap, previousBasemap,
currentOverlays, currentOverlays,
previousOverlays, previousOverlays,
customBasemapOrder, customBasemapOrder,
customOverlayOrder customOverlayOrder
} = settings; } = settings;
let name: string = ''; let name: string = '';
let tileUrls: string[] = ['']; let tileUrls: string[] = [''];
let maxZoom: number = 20; let maxZoom: number = 20;
let layerType: 'basemap' | 'overlay' = 'basemap'; let layerType: 'basemap' | 'overlay' = 'basemap';
let resourceType: 'raster' | 'vector' = 'raster'; let resourceType: 'raster' | 'vector' = 'raster';
let basemapContainer: HTMLElement; let basemapContainer: HTMLElement;
let overlayContainer: HTMLElement; let overlayContainer: HTMLElement;
let basemapSortable: Sortable; let basemapSortable: Sortable;
let overlaySortable: Sortable; let overlaySortable: Sortable;
onMount(() => { onMount(() => {
if ($customBasemapOrder.length === 0) { if ($customBasemapOrder.length === 0) {
$customBasemapOrder = Object.keys($customLayers).filter( $customBasemapOrder = Object.keys($customLayers).filter(
(id) => $customLayers[id].layerType === 'basemap' (id) => $customLayers[id].layerType === 'basemap'
); );
} }
if ($customOverlayOrder.length === 0) { if ($customOverlayOrder.length === 0) {
$customOverlayOrder = Object.keys($customLayers).filter( $customOverlayOrder = Object.keys($customLayers).filter(
(id) => $customLayers[id].layerType === 'overlay' (id) => $customLayers[id].layerType === 'overlay'
); );
} }
basemapSortable = Sortable.create(basemapContainer, { basemapSortable = Sortable.create(basemapContainer, {
onSort: (e) => { onSort: (e) => {
$customBasemapOrder = basemapSortable.toArray(); $customBasemapOrder = basemapSortable.toArray();
$selectedBasemapTree.basemaps['custom'] = $customBasemapOrder.reduce((acc, id) => { $selectedBasemapTree.basemaps['custom'] = $customBasemapOrder.reduce((acc, id) => {
acc[id] = true; acc[id] = true;
return acc; return acc;
}, {}); }, {});
} }
}); });
overlaySortable = Sortable.create(overlayContainer, { overlaySortable = Sortable.create(overlayContainer, {
onSort: (e) => { onSort: (e) => {
$customOverlayOrder = overlaySortable.toArray(); $customOverlayOrder = overlaySortable.toArray();
$selectedOverlayTree.overlays['custom'] = $customOverlayOrder.reduce((acc, id) => { $selectedOverlayTree.overlays['custom'] = $customOverlayOrder.reduce((acc, id) => {
acc[id] = true; acc[id] = true;
return acc; return acc;
}, {}); }, {});
} }
}); });
basemapSortable.sort($customBasemapOrder); basemapSortable.sort($customBasemapOrder);
overlaySortable.sort($customOverlayOrder); overlaySortable.sort($customOverlayOrder);
}); });
onDestroy(() => { onDestroy(() => {
basemapSortable.destroy(); basemapSortable.destroy();
overlaySortable.destroy(); overlaySortable.destroy();
}); });
$: if (tileUrls[0].length > 0) { $: if (tileUrls[0].length > 0) {
if ( if (
tileUrls[0].includes('.json') || tileUrls[0].includes('.json') ||
(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles')) (tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
) { ) {
resourceType = 'vector'; resourceType = 'vector';
} else { } else {
resourceType = 'raster'; resourceType = 'raster';
} }
} }
function createLayer() { function createLayer() {
if (selectedLayerId && $customLayers[selectedLayerId].layerType !== layerType) { if (selectedLayerId && $customLayers[selectedLayerId].layerType !== layerType) {
deleteLayer(selectedLayerId); deleteLayer(selectedLayerId);
} }
if (typeof maxZoom === 'string') { if (typeof maxZoom === 'string') {
maxZoom = parseInt(maxZoom); maxZoom = parseInt(maxZoom);
} }
let is512 = tileUrls.some((url) => url.includes('512'));
let layerId = selectedLayerId ?? getLayerId(); let layerId = selectedLayerId ?? getLayerId();
let layer: CustomLayer = { let layer: CustomLayer = {
id: layerId, id: layerId,
name: name, name: name,
tileUrls: tileUrls.map((url) => decodeURI(url.trim())), tileUrls: tileUrls.map((url) => decodeURI(url.trim())),
maxZoom: maxZoom, maxZoom: maxZoom,
layerType: layerType, layerType: layerType,
resourceType: resourceType, resourceType: resourceType,
value: '' value: ''
}; };
if (resourceType === 'vector') { if (resourceType === 'vector') {
layer.value = layer.tileUrls[0]; layer.value = layer.tileUrls[0];
} else { } else {
layer.value = { layer.value = {
version: 8, version: 8,
sources: { sources: {
[layerId]: { [layerId]: {
type: 'raster', type: 'raster',
tiles: layer.tileUrls, tiles: layer.tileUrls,
tileSize: 256, tileSize: is512 ? 512 : 256,
maxzoom: maxZoom maxzoom: maxZoom
} }
}, },
layers: [ layers: [
{ {
id: layerId, id: layerId,
type: 'raster', type: 'raster',
source: layerId source: layerId
} }
] ]
}; };
} }
$customLayers[layerId] = layer; $customLayers[layerId] = layer;
addLayer(layerId); addLayer(layerId);
selectedLayerId = undefined; selectedLayerId = undefined;
setDataFromSelectedLayer(); setDataFromSelectedLayer();
} }
function getLayerId() { function getLayerId() {
for (let id = 0; ; id++) { for (let id = 0; ; id++) {
if (!$customLayers.hasOwnProperty(`custom-${id}`)) { if (!$customLayers.hasOwnProperty(`custom-${id}`)) {
return `custom-${id}`; return `custom-${id}`;
} }
} }
} }
function addLayer(layerId: string) { function addLayer(layerId: string) {
if (layerType === 'basemap') { if (layerType === 'basemap') {
selectedBasemapTree.update(($tree) => { selectedBasemapTree.update(($tree) => {
if (!$tree.basemaps.hasOwnProperty('custom')) { if (!$tree.basemaps.hasOwnProperty('custom')) {
$tree.basemaps['custom'] = {}; $tree.basemaps['custom'] = {};
} }
$tree.basemaps['custom'][layerId] = true; $tree.basemaps['custom'][layerId] = true;
return $tree; return $tree;
}); });
if ($currentBasemap === layerId) { if ($currentBasemap === layerId) {
$customBasemapUpdate++; $customBasemapUpdate++;
} else { } else {
$currentBasemap = layerId; $currentBasemap = layerId;
} }
if (!$customBasemapOrder.includes(layerId)) { if (!$customBasemapOrder.includes(layerId)) {
$customBasemapOrder = [...$customBasemapOrder, layerId]; $customBasemapOrder = [...$customBasemapOrder, layerId];
} }
} else { } else {
selectedOverlayTree.update(($tree) => { selectedOverlayTree.update(($tree) => {
if (!$tree.overlays.hasOwnProperty('custom')) { if (!$tree.overlays.hasOwnProperty('custom')) {
$tree.overlays['custom'] = {}; $tree.overlays['custom'] = {};
} }
$tree.overlays['custom'][layerId] = true; $tree.overlays['custom'][layerId] = true;
return $tree; return $tree;
}); });
if ( if (
$currentOverlays.overlays['custom'] && $currentOverlays.overlays['custom'] &&
$currentOverlays.overlays['custom'][layerId] && $currentOverlays.overlays['custom'][layerId] &&
$map $map
) { ) {
try { try {
$map.removeImport(layerId); $map.removeImport(layerId);
} catch (e) { } catch (e) {
// No reliable way to check if the map is ready to remove sources and layers // No reliable way to check if the map is ready to remove sources and layers
} }
} }
if (!$currentOverlays.overlays.hasOwnProperty('custom')) { if (!$currentOverlays.overlays.hasOwnProperty('custom')) {
$currentOverlays.overlays['custom'] = {}; $currentOverlays.overlays['custom'] = {};
} }
$currentOverlays.overlays['custom'][layerId] = true; $currentOverlays.overlays['custom'][layerId] = true;
if (!$customOverlayOrder.includes(layerId)) { if (!$customOverlayOrder.includes(layerId)) {
$customOverlayOrder = [...$customOverlayOrder, layerId]; $customOverlayOrder = [...$customOverlayOrder, layerId];
} }
} }
} }
function tryDeleteLayer(node: any, id: string): any { function tryDeleteLayer(node: any, id: string): any {
if (node.hasOwnProperty(id)) { if (node.hasOwnProperty(id)) {
delete node[id]; delete node[id];
} }
return node; return node;
} }
function deleteLayer(layerId: string) { function deleteLayer(layerId: string) {
let layer = $customLayers[layerId]; let layer = $customLayers[layerId];
if (layer.layerType === 'basemap') { if (layer.layerType === 'basemap') {
if (layerId === $currentBasemap) { if (layerId === $currentBasemap) {
$currentBasemap = defaultBasemap; $currentBasemap = defaultBasemap;
} }
if (layerId === $previousBasemap) { if (layerId === $previousBasemap) {
$previousBasemap = defaultBasemap; $previousBasemap = defaultBasemap;
} }
$selectedBasemapTree.basemaps['custom'] = tryDeleteLayer( $selectedBasemapTree.basemaps['custom'] = tryDeleteLayer(
$selectedBasemapTree.basemaps['custom'], $selectedBasemapTree.basemaps['custom'],
layerId layerId
); );
if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) { if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) {
$selectedBasemapTree.basemaps = tryDeleteLayer( $selectedBasemapTree.basemaps = tryDeleteLayer($selectedBasemapTree.basemaps, 'custom');
$selectedBasemapTree.basemaps, }
'custom' $customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId);
); } else {
} $currentOverlays.overlays['custom'][layerId] = false;
$customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId); if ($previousOverlays.overlays['custom']) {
} else { $previousOverlays.overlays['custom'] = tryDeleteLayer(
$currentOverlays.overlays['custom'][layerId] = false; $previousOverlays.overlays['custom'],
if ($previousOverlays.overlays['custom']) { layerId
$previousOverlays.overlays['custom'] = tryDeleteLayer( );
$previousOverlays.overlays['custom'], }
layerId
);
}
$selectedOverlayTree.overlays['custom'] = tryDeleteLayer( $selectedOverlayTree.overlays['custom'] = tryDeleteLayer(
$selectedOverlayTree.overlays['custom'], $selectedOverlayTree.overlays['custom'],
layerId layerId
); );
if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) { if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) {
$selectedOverlayTree.overlays = tryDeleteLayer( $selectedOverlayTree.overlays = tryDeleteLayer($selectedOverlayTree.overlays, 'custom');
$selectedOverlayTree.overlays, }
'custom' $customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId);
);
}
$customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId);
if ( if (
$currentOverlays.overlays['custom'] && $currentOverlays.overlays['custom'] &&
$currentOverlays.overlays['custom'][layerId] && $currentOverlays.overlays['custom'][layerId] &&
$map $map
) { ) {
try { try {
$map.removeImport(layerId); $map.removeImport(layerId);
} catch (e) { } catch (e) {
// No reliable way to check if the map is ready to remove sources and layers // No reliable way to check if the map is ready to remove sources and layers
} }
} }
} }
$customLayers = tryDeleteLayer($customLayers, layerId); $customLayers = tryDeleteLayer($customLayers, layerId);
} }
let selectedLayerId: string | undefined = undefined; let selectedLayerId: string | undefined = undefined;
function setDataFromSelectedLayer() { function setDataFromSelectedLayer() {
if (selectedLayerId) { if (selectedLayerId) {
const layer = $customLayers[selectedLayerId]; const layer = $customLayers[selectedLayerId];
name = layer.name; name = layer.name;
tileUrls = layer.tileUrls; tileUrls = layer.tileUrls;
maxZoom = layer.maxZoom; maxZoom = layer.maxZoom;
layerType = layer.layerType; layerType = layer.layerType;
resourceType = layer.resourceType; resourceType = layer.resourceType;
} else { } else {
name = ''; name = '';
tileUrls = ['']; tileUrls = [''];
maxZoom = 20; maxZoom = 20;
layerType = 'basemap'; layerType = 'basemap';
resourceType = 'raster'; resourceType = 'raster';
} }
} }
$: selectedLayerId, setDataFromSelectedLayer(); $: selectedLayerId, setDataFromSelectedLayer();
</script> </script>
<div class="flex flex-col"> <div class="flex flex-col">
{#if $customBasemapOrder.length > 0} {#if $customBasemapOrder.length > 0}
<div class="flex flex-row items-center gap-1 font-semibold mb-2"> <div class="flex flex-row items-center gap-1 font-semibold mb-2">
<Map size="16" /> <Map size="16" />
{$_('layers.label.basemaps')} {$_('layers.label.basemaps')}
<div class="grow"> <div class="grow">
<Separator /> <Separator />
</div> </div>
</div> </div>
{/if} {/if}
<div <div
bind:this={basemapContainer} bind:this={basemapContainer}
class="ml-1.5 flex flex-col gap-1 {$customBasemapOrder.length > 0 ? 'mb-2' : ''}" class="ml-1.5 flex flex-col gap-1 {$customBasemapOrder.length > 0 ? 'mb-2' : ''}"
> >
{#each $customBasemapOrder as id (id)} {#each $customBasemapOrder as id (id)}
<div class="flex flex-row items-center gap-2" data-id={id}> <div class="flex flex-row items-center gap-2" data-id={id}>
<Move size="12" /> <Move size="12" />
<span class="grow">{$customLayers[id].name}</span> <span class="grow">{$customLayers[id].name}</span>
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7"> <Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
<Pencil size="16" /> <Pencil size="16" />
</Button> </Button>
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7"> <Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
<Trash2 size="16" /> <Trash2 size="16" />
</Button> </Button>
</div> </div>
{/each} {/each}
</div> </div>
{#if $customOverlayOrder.length > 0} {#if $customOverlayOrder.length > 0}
<div class="flex flex-row items-center gap-1 font-semibold mb-2"> <div class="flex flex-row items-center gap-1 font-semibold mb-2">
<Layers2 size="16" /> <Layers2 size="16" />
{$_('layers.label.overlays')} {$_('layers.label.overlays')}
<div class="grow"> <div class="grow">
<Separator /> <Separator />
</div> </div>
</div> </div>
{/if} {/if}
<div <div
bind:this={overlayContainer} bind:this={overlayContainer}
class="ml-1.5 flex flex-col gap-1 {$customOverlayOrder.length > 0 ? 'mb-2' : ''}" class="ml-1.5 flex flex-col gap-1 {$customOverlayOrder.length > 0 ? 'mb-2' : ''}"
> >
{#each $customOverlayOrder as id (id)} {#each $customOverlayOrder as id (id)}
<div class="flex flex-row items-center gap-2" data-id={id}> <div class="flex flex-row items-center gap-2" data-id={id}>
<Move size="12" /> <Move size="12" />
<span class="grow">{$customLayers[id].name}</span> <span class="grow">{$customLayers[id].name}</span>
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7"> <Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
<Pencil size="16" /> <Pencil size="16" />
</Button> </Button>
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7"> <Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
<Trash2 size="16" /> <Trash2 size="16" />
</Button> </Button>
</div> </div>
{/each} {/each}
</div> </div>
<Card.Root> <Card.Root>
<Card.Header class="p-3"> <Card.Header class="p-3">
<Card.Title class="text-base"> <Card.Title class="text-base">
{#if selectedLayerId} {#if selectedLayerId}
{$_('layers.custom_layers.edit')} {$_('layers.custom_layers.edit')}
{:else} {:else}
{$_('layers.custom_layers.new')} {$_('layers.custom_layers.new')}
{/if} {/if}
</Card.Title> </Card.Title>
</Card.Header> </Card.Header>
<Card.Content class="p-3 pt-0"> <Card.Content class="p-3 pt-0">
<fieldset class="flex flex-col gap-2"> <fieldset class="flex flex-col gap-2">
<Label for="name">{$_('menu.metadata.name')}</Label> <Label for="name">{$_('menu.metadata.name')}</Label>
<Input bind:value={name} id="name" class="h-8" /> <Input bind:value={name} id="name" class="h-8" />
<Label for="url">{$_('layers.custom_layers.urls')}</Label> <Label for="url">{$_('layers.custom_layers.urls')}</Label>
{#each tileUrls as url, i} {#each tileUrls as url, i}
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<Input <Input
bind:value={tileUrls[i]} bind:value={tileUrls[i]}
id="url" id="url"
class="h-8" class="h-8"
placeholder={$_('layers.custom_layers.url_placeholder')} placeholder={$_('layers.custom_layers.url_placeholder')}
/> />
{#if tileUrls.length > 1} {#if tileUrls.length > 1}
<Button <Button
on:click={() => on:click={() => (tileUrls = tileUrls.filter((_, index) => index !== i))}
(tileUrls = tileUrls.filter((_, index) => index !== i))} variant="outline"
variant="outline" class="p-1 h-8"
class="p-1 h-8" >
> <Minus size="16" />
<Minus size="16" /> </Button>
</Button> {/if}
{/if} {#if i === tileUrls.length - 1}
{#if i === tileUrls.length - 1} <Button
<Button on:click={() => (tileUrls = [...tileUrls, ''])}
on:click={() => (tileUrls = [...tileUrls, ''])} variant="outline"
variant="outline" class="p-1 h-8"
class="p-1 h-8" >
> <Plus size="16" />
<Plus size="16" /> </Button>
</Button> {/if}
{/if} </div>
</div> {/each}
{/each} {#if resourceType === 'raster'}
{#if resourceType === 'raster'} <Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label>
<Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label> <Input type="number" bind:value={maxZoom} id="maxZoom" min={0} max={22} class="h-8" />
<Input {/if}
type="number" <Label>{$_('layers.custom_layers.layer_type')}</Label>
bind:value={maxZoom} <RadioGroup.Root bind:value={layerType} class="flex flex-row">
id="maxZoom" <div class="flex items-center space-x-2">
min={0} <RadioGroup.Item value="basemap" id="basemap" />
max={22} <Label for="basemap">{$_('layers.custom_layers.basemap')}</Label>
class="h-8" </div>
/> <div class="flex items-center space-x-2">
{/if} <RadioGroup.Item value="overlay" id="overlay" />
<Label>{$_('layers.custom_layers.layer_type')}</Label> <Label for="overlay">{$_('layers.custom_layers.overlay')}</Label>
<RadioGroup.Root bind:value={layerType} class="flex flex-row"> </div>
<div class="flex items-center space-x-2"> </RadioGroup.Root>
<RadioGroup.Item value="basemap" id="basemap" /> {#if selectedLayerId}
<Label for="basemap">{$_('layers.custom_layers.basemap')}</Label> <div class="mt-2 flex flex-row gap-2">
</div> <Button variant="outline" on:click={createLayer} class="grow">
<div class="flex items-center space-x-2"> <Save size="16" class="mr-1" />
<RadioGroup.Item value="overlay" id="overlay" /> {$_('layers.custom_layers.update')}
<Label for="overlay">{$_('layers.custom_layers.overlay')}</Label> </Button>
</div> <Button variant="outline" on:click={() => (selectedLayerId = undefined)}>
</RadioGroup.Root> <CircleX size="16" />
{#if selectedLayerId} </Button>
<div class="mt-2 flex flex-row gap-2"> </div>
<Button variant="outline" on:click={createLayer} class="grow"> {:else}
<Save size="16" class="mr-1" /> <Button variant="outline" class="mt-2" on:click={createLayer}>
{$_('layers.custom_layers.update')} <CirclePlus size="16" class="mr-1" />
</Button> {$_('layers.custom_layers.create')}
<Button variant="outline" on:click={() => (selectedLayerId = undefined)}> </Button>
<CircleX size="16" /> {/if}
</Button> </fieldset>
</div> </Card.Content>
{:else} </Card.Root>
<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>
</div> </div>