use style imports instead of layers to allow stacking mapbox styles, closes #32

This commit is contained in:
vcoppe
2024-08-31 15:57:58 +02:00
parent 33f3b6cc32
commit 0cb781176e
9 changed files with 3053 additions and 981 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import { TramFront, Utensils, ShoppingBasket, Droplet, ShowerHead, Fuel, CircleParking, Fence, FerrisWheel, Bed, Mountain, Pickaxe, Store, TrainFront, Bus, Ship, Croissant, House, Tent, Wrench, Binoculars } from 'lucide-static'; import { TramFront, Utensils, ShoppingBasket, Droplet, ShowerHead, Fuel, CircleParking, Fence, FerrisWheel, Bed, Mountain, Pickaxe, Store, TrainFront, Bus, Ship, Croissant, House, Tent, Wrench, Binoculars } from 'lucide-static';
import { type AnySourceData, type Style } from 'mapbox-gl'; import { type Style } from 'mapbox-gl';
import ignFrTopo from './custom/ign-fr-topo.json'; import ignFrTopo from './custom/ign-fr-topo.json';
import ignFrPlan from './custom/ign-fr-plan.json'; import ignFrPlan from './custom/ign-fr-plan.json';
import ignFrSatellite from './custom/ign-fr-satellite.json'; import ignFrSatellite from './custom/ign-fr-satellite.json';
import bikerouterGravel from './custom/bikerouter-gravel.json';
export const basemaps: { [key: string]: string | Style; } = { export const basemaps: { [key: string]: string | Style; } = {
mapboxOutdoors: 'mapbox://styles/mapbox/outdoors-v12', mapboxOutdoors: 'mapbox://styles/mapbox/outdoors-v12',
@@ -255,144 +255,309 @@ export const basemaps: { [key: string]: string | Style; } = {
}, },
}; };
export function extendBasemap(basemap: string | Style): string | Style { export const overlays: { [key: string]: string | Style; } = {
if (typeof basemap === 'object') {
basemap["glyphs"] = "mapbox://fonts/mapbox/{fontstack}/{range}.pbf";
basemap["sprite"] = `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}`;
}
return basemap;
}
Object.values(basemaps).forEach(extendBasemap);
export const font: { [key: string]: string; } = {
swisstopoVector: 'Frutiger Neue Condensed Regular',
swisstopoSatellite: 'Frutiger Neue Condensed Regular',
};
export const overlays: { [key: string]: AnySourceData; } = {
cyclOSMlite: { cyclOSMlite: {
type: 'raster', version: 8,
tiles: ['https://a.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png', 'https://b.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png', 'https://c.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png'], sources: {
tileSize: 256, cyclOSMlite: {
maxzoom: 17, type: 'raster',
attribution: '&copy; <a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>' tiles: ['https://a.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png', 'https://b.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png', 'https://c.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 17,
attribution: '&copy; <a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}
},
layers: [{
id: 'cyclOSMlite',
type: 'raster',
source: 'cyclOSMlite',
}],
}, },
bikerouterGravel: bikerouterGravel,
swisstopoSlope: { swisstopoSlope: {
type: 'raster', version: 8,
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.hangneigung-ueber_30/default/current/3857/{z}/{x}/{y}.png'], sources: {
tileSize: 256, swisstopoSlope: {
maxzoom: 17, type: 'raster',
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>', tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.hangneigung-ueber_30/default/current/3857/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 17,
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>',
},
},
layers: [{
id: 'swisstopoSlope',
type: 'raster',
source: 'swisstopoSlope',
}],
}, },
swisstopoHiking: { swisstopoHiking: {
type: 'raster', version: 8,
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swisstlm3d-wanderwege/default/current/3857/{z}/{x}/{y}.png'], sources: {
tileSize: 256, swisstopoHiking: {
maxzoom: 18, type: 'raster',
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>' tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swisstlm3d-wanderwege/default/current/3857/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 18,
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
},
},
layers: [{
id: 'swisstopoHiking',
type: 'raster',
source: 'swisstopoHiking',
}],
}, },
swisstopoHikingClosures: { swisstopoHikingClosures: {
type: 'raster', version: 8,
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.wanderland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'], sources: {
tileSize: 256, swisstopoHikingClosures: {
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>' type: 'raster',
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.wanderland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
tileSize: 256,
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
},
},
layers: [{
id: 'swisstopoHikingClosures',
type: 'raster',
source: 'swisstopoHikingClosures',
}],
}, },
swisstopoCycling: { swisstopoCycling: {
type: 'raster', version: 8,
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.veloland/default/current/3857/{z}/{x}/{y}.png'], sources: {
tileSize: 256, swisstopoCycling: {
maxzoom: 18, type: 'raster',
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>' tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.veloland/default/current/3857/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 18,
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
}
},
layers: [{
id: 'swisstopoCycling',
type: 'raster',
source: 'swisstopoCycling',
}],
}, },
swisstopoCyclingClosures: { swisstopoCyclingClosures: {
type: 'raster', version: 8,
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.veloland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'], sources: {
tileSize: 256, swisstopoCyclingClosures: {
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>' type: 'raster',
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.veloland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
tileSize: 256,
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
}
},
layers: [{
id: 'swisstopoCyclingClosures',
type: 'raster',
source: 'swisstopoCyclingClosures',
}],
}, },
swisstopoMountainBike: { swisstopoMountainBike: {
type: 'raster', version: 8,
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.mountainbikeland/default/current/3857/{z}/{x}/{y}.png'], sources: {
tileSize: 256, swisstopoMountainBike: {
maxzoom: 18, type: 'raster',
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>' tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.mountainbikeland/default/current/3857/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 18,
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
}
},
layers: [{
id: 'swisstopoMountainBike',
type: 'raster',
source: 'swisstopoMountainBike',
}],
}, },
swisstopoMountainBikeClosures: { swisstopoMountainBikeClosures: {
type: 'raster', version: 8,
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.mountainbikeland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'], sources: {
tileSize: 256, swisstopoMountainBikeClosures: {
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>' type: 'raster',
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.mountainbikeland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
tileSize: 256,
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
}
},
layers: [{
id: 'swisstopoMountainBikeClosures',
type: 'raster',
source: 'swisstopoMountainBikeClosures',
}],
}, },
swisstopoSkiTouring: { swisstopoSkiTouring: {
type: 'raster', version: 8,
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo-karto.skitouren/default/current/3857/{z}/{x}/{y}.png'], sources: {
tileSize: 256, swisstopoSkiTouring: {
maxzoom: 17, type: 'raster',
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>' tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo-karto.skitouren/default/current/3857/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 17,
attribution: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
}
},
layers: [{
id: 'swisstopoSkiTouring',
type: 'raster',
source: 'swisstopoSkiTouring',
}],
}, },
ignFrCadastre: { ignFrCadastre: {
type: 'raster', version: 8,
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TILEMATRIXSET=PM&TILEMATRIX={z}&TILECOL={x}&TILEROW={y}&LAYER=CADASTRALPARCELS.PARCELS&FORMAT=image/png&STYLE=normal'], sources: {
tileSize: 256, ignFrCadastre: {
maxzoom: 20, type: 'raster',
attribution: 'IGN-F/Géoportail' tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TILEMATRIXSET=PM&TILEMATRIX={z}&TILECOL={x}&TILEROW={y}&LAYER=CADASTRALPARCELS.PARCELS&FORMAT=image/png&STYLE=normal'],
tileSize: 256,
maxzoom: 20,
attribution: 'IGN-F/Géoportail'
}
},
layers: [{
id: 'ignFrCadastre',
type: 'raster',
source: 'ignFrCadastre',
}],
}, },
ignSlope: { ignSlope: {
type: 'raster', version: 8,
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TileMatrixSet=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&Layer=GEOGRAPHICALGRIDSYSTEMS.SLOPES.MOUNTAIN&FORMAT=image/png&Style=normal'], sources: {
tileSize: 256, ignSlope: {
maxzoom: 17, type: 'raster',
attribution: 'IGN-F/Géoportail' tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TileMatrixSet=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&Layer=GEOGRAPHICALGRIDSYSTEMS.SLOPES.MOUNTAIN&FORMAT=image/png&Style=normal'],
tileSize: 256,
attribution: 'IGN-F/Géoportail'
}
},
layers: [{
id: 'ignSlope',
type: 'raster',
source: 'ignSlope',
}],
}, },
ignSkiTouring: { ignSkiTouring: {
type: 'raster', version: 8,
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TileMatrixSet=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&Layer=TRACES.RANDO.HIVERNALE&FORMAT=image/png&Style=normal'], sources: {
tileSize: 256, ignSkiTouring: {
maxzoom: 16, type: 'raster',
attribution: 'IGN-F/Géoportail' tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TileMatrixSet=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&Layer=TRACES.RANDO.HIVERNALE&FORMAT=image/png&Style=normal'],
tileSize: 256,
maxzoom: 16,
attribution: 'IGN-F/Géoportail'
},
},
layers: [{
id: 'ignSkiTouring',
type: 'raster',
source: 'ignSkiTouring',
}],
}, },
waymarkedTrailsHiking: { waymarkedTrailsHiking: {
type: 'raster', version: 8,
tiles: ['https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png'], sources: {
tileSize: 256, waymarkedTrailsHiking: {
maxzoom: 18, type: 'raster',
attribution: '&copy; <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>' tiles: ['https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 18,
attribution: '&copy; <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
}
},
layers: [{
id: 'waymarkedTrailsHiking',
type: 'raster',
source: 'waymarkedTrailsHiking',
}],
}, },
waymarkedTrailsCycling: { waymarkedTrailsCycling: {
type: 'raster', version: 8,
tiles: ['https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png'], sources: {
tileSize: 256, waymarkedTrailsCycling: {
maxzoom: 18, type: 'raster',
attribution: '&copy; <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>' tiles: ['https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 18,
attribution: '&copy; <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
}
},
layers: [{
id: 'waymarkedTrailsCycling',
type: 'raster',
source: 'waymarkedTrailsCycling',
}],
}, },
waymarkedTrailsMTB: { waymarkedTrailsMTB: {
type: 'raster', version: 8,
tiles: ['https://tile.waymarkedtrails.org/mtb/{z}/{x}/{y}.png'], sources: {
tileSize: 256, waymarkedTrailsMTB: {
maxzoom: 18, type: 'raster',
attribution: '&copy; <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>' tiles: ['https://tile.waymarkedtrails.org/mtb/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 18,
attribution: '&copy; <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
}
},
layers: [{
id: 'waymarkedTrailsMTB',
type: 'raster',
source: 'waymarkedTrailsMTB',
}],
}, },
waymarkedTrailsSkating: { waymarkedTrailsSkating: {
type: 'raster', version: 8,
tiles: ['https://tile.waymarkedtrails.org/skating/{z}/{x}/{y}.png'], sources: {
tileSize: 256, waymarkedTrailsSkating: {
maxzoom: 18, type: 'raster',
attribution: '&copy; <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>' tiles: ['https://tile.waymarkedtrails.org/skating/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 18,
attribution: '&copy; <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
}
},
layers: [{
id: 'waymarkedTrailsSkating',
type: 'raster',
source: 'waymarkedTrailsSkating',
}],
}, },
waymarkedTrailsHorseRiding: { waymarkedTrailsHorseRiding: {
type: 'raster', version: 8,
tiles: ['https://tile.waymarkedtrails.org/riding/{z}/{x}/{y}.png'], sources: {
tileSize: 256, waymarkedTrailsHorseRiding: {
maxzoom: 18, type: 'raster',
attribution: '&copy; <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>' tiles: ['https://tile.waymarkedtrails.org/riding/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 18,
attribution: '&copy; <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
}
},
layers: [{
id: 'waymarkedTrailsHorseRiding',
type: 'raster',
source: 'waymarkedTrailsHorseRiding',
}],
}, },
waymarkedTrailsWinter: { waymarkedTrailsWinter: {
type: 'raster', version: 8,
tiles: ['https://tile.waymarkedtrails.org/slopes/{z}/{x}/{y}.png'], sources: {
tileSize: 256, waymarkedTrailsWinter: {
maxzoom: 18, type: 'raster',
attribution: '&copy; <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>' tiles: ['https://tile.waymarkedtrails.org/slopes/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 18,
attribution: '&copy; <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
}
},
layers: [{
id: 'waymarkedTrailsWinter',
type: 'raster',
source: 'waymarkedTrailsWinter',
}],
}, },
}; };
@@ -463,9 +628,6 @@ export const basemapTree: LayerTreeType = {
export const overlayTree: LayerTreeType = { export const overlayTree: LayerTreeType = {
overlays: { overlays: {
world: { world: {
cyclOSM: {
cyclOSMlite: true,
},
waymarked_trails: { waymarked_trails: {
waymarkedTrailsHiking: true, waymarkedTrailsHiking: true,
waymarkedTrailsCycling: true, waymarkedTrailsCycling: true,
@@ -473,7 +635,9 @@ export const overlayTree: LayerTreeType = {
waymarkedTrailsSkating: true, waymarkedTrailsSkating: true,
waymarkedTrailsHorseRiding: true, waymarkedTrailsHorseRiding: true,
waymarkedTrailsWinter: true, waymarkedTrailsWinter: true,
} },
cyclOSMlite: true,
bikerouterGravel: true,
}, },
countries: { countries: {
france: { france: {
@@ -547,9 +711,6 @@ export const defaultBasemap = 'mapboxOutdoors';
export const defaultOverlays = { export const defaultOverlays = {
overlays: { overlays: {
world: { world: {
cyclOSM: {
cyclOSMlite: false,
},
waymarked_trails: { waymarked_trails: {
waymarkedTrailsHiking: false, waymarkedTrailsHiking: false,
waymarkedTrailsCycling: false, waymarkedTrailsCycling: false,
@@ -557,7 +718,9 @@ export const defaultOverlays = {
waymarkedTrailsSkating: false, waymarkedTrailsSkating: false,
waymarkedTrailsHorseRiding: false, waymarkedTrailsHorseRiding: false,
waymarkedTrailsWinter: false, waymarkedTrailsWinter: false,
} },
cyclOSMlite: false,
bikerouterGravel: false,
}, },
countries: { countries: {
france: { france: {
@@ -683,9 +846,6 @@ export const defaultBasemapTree: LayerTreeType = {
export const defaultOverlayTree: LayerTreeType = { export const defaultOverlayTree: LayerTreeType = {
overlays: { overlays: {
world: { world: {
cyclOSM: {
cyclOSMlite: false,
},
waymarked_trails: { waymarked_trails: {
waymarkedTrailsHiking: true, waymarkedTrailsHiking: true,
waymarkedTrailsCycling: true, waymarkedTrailsCycling: true,
@@ -693,7 +853,9 @@ export const defaultOverlayTree: LayerTreeType = {
waymarkedTrailsSkating: false, waymarkedTrailsSkating: false,
waymarkedTrailsHorseRiding: false, waymarkedTrailsHorseRiding: false,
waymarkedTrailsWinter: false, waymarkedTrailsWinter: false,
} },
cyclOSMlite: false,
bikerouterGravel: false,
}, },
countries: { countries: {
france: { france: {

View File

@@ -1,331 +1,355 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css'; import 'mapbox-gl/dist/mapbox-gl.css';
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'; import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'; import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { map } from '$lib/stores'; import { map } from '$lib/stores';
import { settings } from '$lib/db'; import { settings } from '$lib/db';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public'; import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import { page } from '$app/stores'; import { page } from '$app/stores';
export let accessToken = PUBLIC_MAPBOX_TOKEN; export let accessToken = PUBLIC_MAPBOX_TOKEN;
export let geolocate = true; export let geolocate = true;
export let geocoder = true; export let geocoder = true;
export let hash = true; export let hash = true;
mapboxgl.accessToken = accessToken; mapboxgl.accessToken = accessToken;
let webgl2Supported = true; let webgl2Supported = true;
let fitBoundsOptions: mapboxgl.FitBoundsOptions = { let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
maxZoom: 15, maxZoom: 15,
linear: true, linear: true,
easing: () => 1 easing: () => 1
}; };
const { distanceUnits, elevationProfile, verticalFileView, bottomPanelSize, rightPanelSize } = const { distanceUnits, elevationProfile, verticalFileView, bottomPanelSize, rightPanelSize } =
settings; settings;
let scaleControl = new mapboxgl.ScaleControl({ let scaleControl = new mapboxgl.ScaleControl({
unit: $distanceUnits unit: $distanceUnits
}); });
onMount(() => { onMount(() => {
let gl = document.createElement('canvas').getContext('webgl2'); let gl = document.createElement('canvas').getContext('webgl2');
if (!gl) { if (!gl) {
webgl2Supported = false; webgl2Supported = false;
return; return;
} }
let language = $page.params.language; let language = $page.params.language;
if (language === 'zh') { if (language === 'zh') {
language = 'zh-Hans'; language = 'zh-Hans';
} else if (language?.includes('-')) { } else if (language?.includes('-')) {
language = language.split('-')[0]; language = language.split('-')[0];
} else if (language === '' || language === undefined) { } else if (language === '' || language === undefined) {
language = 'en'; language = 'en';
} }
let newMap = new mapboxgl.Map({ let newMap = new mapboxgl.Map({
container: 'map', container: 'map',
style: { version: 8, sources: {}, layers: [] }, style: {
zoom: 0, version: 8,
hash: hash, sources: {},
language, layers: [],
attributionControl: false, imports: [
logoPosition: 'bottom-right', {
boxZoom: false id: 'glyphs-and-sprite', // make Mapbox glyphs and sprite available to other styles
}); url: '',
newMap.on('load', () => { data: {
$map = newMap; // only set the store after the map has loaded version: 8,
scaleControl.setUnit($distanceUnits); sources: {},
}); layers: [],
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}`
}
},
{
id: 'basemap',
url: ''
},
{
id: 'overlays',
url: '',
data: {
version: 8,
sources: {},
layers: []
}
}
]
},
zoom: 0,
hash: hash,
language,
attributionControl: false,
logoPosition: 'bottom-right',
boxZoom: false
});
newMap.on('load', () => {
$map = newMap; // only set the store after the map has loaded
scaleControl.setUnit($distanceUnits);
});
newMap.addControl( newMap.addControl(
new mapboxgl.AttributionControl({ new mapboxgl.AttributionControl({
compact: true compact: true
}) })
); );
newMap.addControl( newMap.addControl(
new mapboxgl.NavigationControl({ new mapboxgl.NavigationControl({
visualizePitch: true visualizePitch: true
}) })
); );
if (geocoder) { if (geocoder) {
newMap.addControl( newMap.addControl(
new MapboxGeocoder({ new MapboxGeocoder({
accessToken: mapboxgl.accessToken, accessToken: mapboxgl.accessToken,
mapboxgl: mapboxgl, mapboxgl: mapboxgl,
collapsed: true, collapsed: true,
flyTo: fitBoundsOptions, flyTo: fitBoundsOptions,
language language
}) })
); );
} }
if (geolocate) { if (geolocate) {
newMap.addControl( newMap.addControl(
new mapboxgl.GeolocateControl({ new mapboxgl.GeolocateControl({
positionOptions: { positionOptions: {
enableHighAccuracy: true enableHighAccuracy: true
}, },
fitBoundsOptions, fitBoundsOptions,
trackUserLocation: true, trackUserLocation: true,
showUserHeading: true showUserHeading: true
}) })
); );
} }
newMap.addControl(scaleControl); newMap.addControl(scaleControl);
newMap.on('style.load', () => { newMap.on('style.load', () => {
newMap.addSource('mapbox-dem', { newMap.addSource('mapbox-dem', {
type: 'raster-dem', type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1', url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512, tileSize: 512,
maxzoom: 14 maxzoom: 14
}); });
newMap.setTerrain({ newMap.setTerrain({
source: 'mapbox-dem', source: 'mapbox-dem',
exaggeration: newMap.getPitch() > 0 ? 1 : 0 exaggeration: newMap.getPitch() > 0 ? 1 : 0
}); });
newMap.setFog({ newMap.setFog({
color: 'rgb(186, 210, 235)', color: 'rgb(186, 210, 235)',
'high-color': 'rgb(36, 92, 223)', 'high-color': 'rgb(36, 92, 223)',
'horizon-blend': 0.1, 'horizon-blend': 0.1,
'space-color': 'rgb(156, 240, 255)' 'space-color': 'rgb(156, 240, 255)'
}); });
newMap.on('pitch', () => { newMap.on('pitch', () => {
if (newMap.getPitch() > 0) { if (newMap.getPitch() > 0) {
newMap.setTerrain({ newMap.setTerrain({
source: 'mapbox-dem', source: 'mapbox-dem',
exaggeration: 1 exaggeration: 1
}); });
} else { } else {
newMap.setTerrain({ newMap.setTerrain({
source: 'mapbox-dem', source: 'mapbox-dem',
exaggeration: 0 exaggeration: 0
}); });
} }
}); });
// add dummy layer to place the overlay layers below });
newMap.addLayer({ });
id: 'overlays',
type: 'background',
paint: {
'background-color': 'rgba(0, 0, 0, 0)'
}
});
});
});
onDestroy(() => { onDestroy(() => {
if ($map) { if ($map) {
$map.remove(); $map.remove();
$map = null; $map = null;
} }
}); });
$: if ( $: if (
$map && $map &&
(!$verticalFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize) (!$verticalFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)
) { ) {
$map.resize(); $map.resize();
} }
</script> </script>
<div {...$$restProps}> <div {...$$restProps}>
<div id="map" class="h-full {webgl2Supported ? '' : 'hidden'}"></div> <div id="map" class="h-full {webgl2Supported ? '' : 'hidden'}"></div>
<div <div
class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported ? 'hidden' : ''}" class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported
> ? 'hidden'
<p>{$_('webgl2_required')}</p> : ''}"
<Button href="https://get.webgl.org/webgl2/" target="_blank"> >
{$_('enable_webgl2')} <p>{$_('webgl2_required')}</p>
</Button> <Button href="https://get.webgl.org/webgl2/" target="_blank">
</div> {$_('enable_webgl2')}
</Button>
</div>
</div> </div>
<style lang="postcss"> <style lang="postcss">
div :global(.mapboxgl-map) { div :global(.mapboxgl-map) {
@apply font-sans; @apply font-sans;
} }
div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) { div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) {
@apply shadow-md; @apply shadow-md;
@apply bg-background; @apply bg-background;
@apply text-foreground; @apply text-foreground;
} }
div :global(.mapboxgl-ctrl-icon) { div :global(.mapboxgl-ctrl-icon) {
@apply dark:brightness-[4.7]; @apply dark:brightness-[4.7];
} }
div :global(.mapboxgl-ctrl-geocoder) { div :global(.mapboxgl-ctrl-geocoder) {
@apply flex; @apply flex;
@apply flex-row; @apply flex-row;
@apply w-fit; @apply w-fit;
@apply min-w-fit; @apply min-w-fit;
@apply items-center; @apply items-center;
@apply shadow-md; @apply shadow-md;
} }
div :global(.suggestions) { div :global(.suggestions) {
@apply shadow-md; @apply shadow-md;
@apply bg-background; @apply bg-background;
@apply text-foreground; @apply text-foreground;
} }
div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) { div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) {
@apply text-foreground; @apply text-foreground;
@apply hover:text-accent-foreground; @apply hover:text-accent-foreground;
@apply hover:bg-accent; @apply hover:bg-accent;
} }
div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) { div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) {
@apply bg-background; @apply bg-background;
} }
div :global(.mapboxgl-ctrl-geocoder--button) { div :global(.mapboxgl-ctrl-geocoder--button) {
@apply bg-transparent; @apply bg-transparent;
@apply hover:bg-transparent; @apply hover:bg-transparent;
} }
div :global(.mapboxgl-ctrl-geocoder--icon) { div :global(.mapboxgl-ctrl-geocoder--icon) {
@apply fill-foreground; @apply fill-foreground;
@apply hover:fill-accent-foreground; @apply hover:fill-accent-foreground;
} }
div :global(.mapboxgl-ctrl-geocoder--icon-search) { div :global(.mapboxgl-ctrl-geocoder--icon-search) {
@apply relative; @apply relative;
@apply top-0; @apply top-0;
@apply left-0; @apply left-0;
@apply my-2; @apply my-2;
@apply w-[29px]; @apply w-[29px];
} }
div :global(.mapboxgl-ctrl-geocoder--input) { div :global(.mapboxgl-ctrl-geocoder--input) {
@apply relative; @apply relative;
@apply w-64; @apply w-64;
@apply py-0; @apply py-0;
@apply pl-2; @apply pl-2;
@apply focus:outline-none; @apply focus:outline-none;
@apply transition-[width]; @apply transition-[width];
@apply duration-200; @apply duration-200;
@apply text-foreground; @apply text-foreground;
} }
div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) { div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) {
@apply w-0; @apply w-0;
@apply p-0; @apply p-0;
} }
div :global(.mapboxgl-ctrl-top-right) { div :global(.mapboxgl-ctrl-top-right) {
@apply z-40; @apply z-40;
@apply flex; @apply flex;
@apply flex-col; @apply flex-col;
@apply items-end; @apply items-end;
@apply h-full; @apply h-full;
@apply overflow-hidden; @apply overflow-hidden;
} }
.horizontal :global(.mapboxgl-ctrl-bottom-left) { .horizontal :global(.mapboxgl-ctrl-bottom-left) {
@apply bottom-[42px]; @apply bottom-[42px];
} }
.horizontal :global(.mapboxgl-ctrl-bottom-right) { .horizontal :global(.mapboxgl-ctrl-bottom-right) {
@apply bottom-[42px]; @apply bottom-[42px];
} }
div :global(.mapboxgl-ctrl-attrib) { div :global(.mapboxgl-ctrl-attrib) {
@apply dark:bg-transparent; @apply dark:bg-transparent;
} }
div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) { div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) {
@apply dark:bg-background; @apply dark:bg-background;
} }
div :global(.mapboxgl-ctrl-attrib-button) { div :global(.mapboxgl-ctrl-attrib-button) {
@apply dark:bg-foreground; @apply dark:bg-foreground;
} }
div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) { div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) {
@apply dark:bg-foreground; @apply dark:bg-foreground;
} }
div :global(.mapboxgl-ctrl-attrib a) { div :global(.mapboxgl-ctrl-attrib a) {
@apply text-foreground; @apply text-foreground;
} }
div :global(.mapboxgl-popup) { div :global(.mapboxgl-popup) {
@apply w-fit; @apply w-fit;
@apply z-20; @apply z-20;
} }
div :global(.mapboxgl-popup-content) { div :global(.mapboxgl-popup-content) {
@apply p-0; @apply p-0;
@apply bg-transparent; @apply bg-transparent;
@apply shadow-none; @apply shadow-none;
} }
div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) { div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) {
@apply border-b-background; @apply border-b-background;
} }
div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) { div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) {
@apply border-b-background; @apply border-b-background;
} }
div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) { div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) {
@apply border-b-background; @apply border-b-background;
} }
div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) { div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) {
@apply border-t-background; @apply border-t-background;
@apply drop-shadow-md; @apply drop-shadow-md;
} }
div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) { div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) {
@apply border-t-background; @apply border-t-background;
@apply drop-shadow-md; @apply drop-shadow-md;
} }
div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) { div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) {
@apply border-t-background; @apply border-t-background;
@apply drop-shadow-md; @apply drop-shadow-md;
} }
div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) { div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) {
@apply border-r-background; @apply border-r-background;
} }
div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) { div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) {
@apply border-l-background; @apply border-l-background;
} }
</style> </style>

View File

@@ -1,10 +1,9 @@
import { font } from "$lib/assets/layers";
import { settings } from "$lib/db"; import { settings } from "$lib/db";
import { gpxStatistics } from "$lib/stores"; import { gpxStatistics } from "$lib/stores";
import { get } from "svelte/store"; import { get } from "svelte/store";
const { distanceMarkers, distanceUnits, currentBasemap } = settings; const { distanceMarkers, distanceUnits } = settings;
export class DistanceMarkers { export class DistanceMarkers {
map: mapboxgl.Map; map: mapboxgl.Map;
@@ -17,7 +16,7 @@ export class DistanceMarkers {
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded)); this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
this.unsubscribes.push(distanceMarkers.subscribe(this.updateBinded)); this.unsubscribes.push(distanceMarkers.subscribe(this.updateBinded));
this.unsubscribes.push(distanceUnits.subscribe(this.updateBinded)); this.unsubscribes.push(distanceUnits.subscribe(this.updateBinded));
this.map.on('style.load', this.updateBinded); this.map.on('style.import.load', this.updateBinded);
} }
update() { update() {
@@ -40,7 +39,7 @@ export class DistanceMarkers {
layout: { layout: {
'text-field': ['get', 'distance'], 'text-field': ['get', 'distance'],
'text-size': 14, 'text-size': 14,
'text-font': [font[get(currentBasemap)] ?? 'Open Sans Bold'], 'text-font': ['Open Sans Bold'],
'text-padding': 20, 'text-padding': 20,
}, },
paint: { paint: {

View File

@@ -7,7 +7,6 @@ import { addSelectItem, selectItem, selection } from "$lib/components/file-list/
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList"; import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
import type { Waypoint } from "gpx"; import type { Waypoint } from "gpx";
import { getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils"; import { getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils";
import { font } from "$lib/assets/layers";
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte"; import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
import { MapPin, Square } from "lucide-static"; import { MapPin, Square } from "lucide-static";
import { getSymbolKey, symbols } from "$lib/assets/symbols"; import { getSymbolKey, symbols } from "$lib/assets/symbols";
@@ -66,7 +65,7 @@ function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
</svg>`; </svg>`;
} }
const { directionMarkers, verticalFileView, currentBasemap, defaultOpacity, defaultWeight } = settings; const { directionMarkers, verticalFileView, defaultOpacity, defaultWeight } = settings;
export class GPXLayer { export class GPXLayer {
map: mapboxgl.Map; map: mapboxgl.Map;
@@ -112,7 +111,7 @@ export class GPXLayer {
})); }));
this.draggable = get(currentTool) === Tool.WAYPOINT; this.draggable = get(currentTool) === Tool.WAYPOINT;
this.map.on('style.load', this.updateBinded); this.map.on('style.import.load', this.updateBinded);
} }
update() { update() {
@@ -170,7 +169,7 @@ export class GPXLayer {
'text-keep-upright': false, 'text-keep-upright': false,
'text-max-angle': 361, 'text-max-angle': 361,
'text-allow-overlap': true, 'text-allow-overlap': true,
'text-font': [font[get(currentBasemap)] ?? 'Open Sans Bold'], 'text-font': ['Open Sans Bold'],
'symbol-placement': 'line', 'symbol-placement': 'line',
'symbol-spacing': 20, 'symbol-spacing': 20,
}, },
@@ -294,7 +293,7 @@ export class GPXLayer {
updateMap(map: mapboxgl.Map) { updateMap(map: mapboxgl.Map) {
this.map = map; this.map = map;
this.map.on('style.load', this.updateBinded); this.map.on('style.import.load', this.updateBinded);
this.update(); this.update();
} }
@@ -303,7 +302,7 @@ export class GPXLayer {
this.map.off('click', this.fileId, this.layerOnClickBinded); this.map.off('click', this.fileId, this.layerOnClickBinded);
this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded); this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded); this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
this.map.off('style.load', this.updateBinded); this.map.off('style.import.load', this.updateBinded);
if (this.map.getLayer(this.fileId + '-direction')) { if (this.map.getLayer(this.fileId + '-direction')) {
this.map.removeLayer(this.fileId + '-direction'); this.map.removeLayer(this.fileId + '-direction');

View File

@@ -1,424 +1,427 @@
<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, extendBasemap, 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';
layerType = 'basemap'; } 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 layerId = selectedLayerId ?? getLayerId(); let layerId = selectedLayerId ?? getLayerId();
let layer: CustomLayer = { let layer: CustomLayer = {
id: layerId, id: layerId,
name: name, name: name,
tileUrls: tileUrls, tileUrls: tileUrls,
maxZoom: maxZoom, maxZoom: maxZoom,
layerType: layerType, layerType: layerType,
resourceType: resourceType, resourceType: resourceType,
value: '' value: ''
}; };
if (resourceType === 'vector') { if (resourceType === 'vector') {
layer.value = tileUrls[0]; layer.value = tileUrls[0];
} else { } else {
if (layerType === 'basemap') { layer.value = {
layer.value = extendBasemap({ version: 8,
version: 8, sources: {
sources: { [layerId]: {
[layerId]: { type: 'raster',
type: 'raster', tiles: tileUrls,
tiles: tileUrls, tileSize: 256,
tileSize: 256, maxzoom: maxZoom
maxzoom: maxZoom }
} },
}, layers: [
layers: [ {
{ id: layerId,
id: layerId, type: 'raster',
type: 'raster', source: layerId
source: layerId }
} ]
] };
}); }
} else { $customLayers[layerId] = layer;
layer.value = { addLayer(layerId);
type: 'raster', selectedLayerId = undefined;
tiles: tileUrls, setDataFromSelectedLayer();
tileSize: 256, }
maxzoom: maxZoom
};
}
}
$customLayers[layerId] = layer;
addLayer(layerId);
selectedLayerId = undefined;
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 ($map && $map.getSource(layerId)) { if ($map) {
// Reset source when updating an existing layer try {
if ($map.getLayer(layerId)) { $map.removeImport(layerId);
$map.removeLayer(layerId); } catch (e) {
} // No reliable way to check if the map is ready to remove sources and layers
$map.removeSource(layerId); }
} }
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, 'custom'); $selectedBasemapTree.basemaps = tryDeleteLayer(
} $selectedBasemapTree.basemaps,
$customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId); 'custom'
} else { );
$currentOverlays.overlays['custom'][layerId] = false; }
if ($previousOverlays.overlays['custom']) { $customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId);
$previousOverlays.overlays['custom'] = tryDeleteLayer( } else {
$previousOverlays.overlays['custom'], $currentOverlays.overlays['custom'][layerId] = false;
layerId if ($previousOverlays.overlays['custom']) {
); $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, 'custom'); $selectedOverlayTree.overlays = tryDeleteLayer(
} $selectedOverlayTree.overlays,
$customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId); 'custom'
);
}
$customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId);
if ($map) { if ($map) {
if ($map.getLayer(layerId)) { try {
$map.removeLayer(layerId); $map.removeImport(layerId);
} } catch (e) {
if ($map.getSource(layerId)) { // No reliable way to check if the map is ready to remove sources and layers
$map.removeSource(layerId); }
} }
} }
} $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={() => (tileUrls = tileUrls.filter((_, index) => index !== i))} on:click={() =>
variant="outline" (tileUrls = tileUrls.filter((_, index) => index !== i))}
class="p-1 h-8" variant="outline"
> class="p-1 h-8"
<Minus size="16" /> >
</Button> <Minus size="16" />
{/if} </Button>
{#if i === tileUrls.length - 1} {/if}
<Button {#if i === tileUrls.length - 1}
on:click={() => (tileUrls = [...tileUrls, ''])} <Button
variant="outline" on:click={() => (tileUrls = [...tileUrls, ''])}
class="p-1 h-8" variant="outline"
> class="p-1 h-8"
<Plus size="16" /> >
</Button> <Plus size="16" />
{/if} </Button>
</div> {/if}
{/each} </div>
{#if resourceType === 'raster'} {/each}
<Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label> {#if resourceType === 'raster'}
<Input type="number" bind:value={maxZoom} id="maxZoom" min={0} max={22} class="h-8" /> <Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label>
{/if} <Input
<Label>{$_('layers.custom_layers.layer_type')}</Label> type="number"
<RadioGroup.Root bind:value={layerType} class="flex flex-row"> bind:value={maxZoom}
<div class="flex items-center space-x-2"> id="maxZoom"
<RadioGroup.Item value="basemap" id="basemap" /> min={0}
<Label for="basemap">{$_('layers.custom_layers.basemap')}</Label> max={22}
</div> class="h-8"
<div class="flex items-center space-x-2"> />
<RadioGroup.Item value="overlay" id="overlay" disabled={resourceType === 'vector'} /> {/if}
<Label for="overlay">{$_('layers.custom_layers.overlay')}</Label> <Label>{$_('layers.custom_layers.layer_type')}</Label>
</div> <RadioGroup.Root bind:value={layerType} class="flex flex-row">
</RadioGroup.Root> <div class="flex items-center space-x-2">
{#if selectedLayerId} <RadioGroup.Item value="basemap" id="basemap" />
<div class="mt-2 flex flex-row gap-2"> <Label for="basemap">{$_('layers.custom_layers.basemap')}</Label>
<Button variant="outline" on:click={createLayer} class="grow"> </div>
<Save size="16" class="mr-1" /> <div class="flex items-center space-x-2">
{$_('layers.custom_layers.update')} <RadioGroup.Item value="overlay" id="overlay" />
</Button> <Label for="overlay">{$_('layers.custom_layers.overlay')}</Label>
<Button variant="outline" on:click={() => (selectedLayerId = undefined)}> </div>
<CircleX size="16" /> </RadioGroup.Root>
</Button> {#if selectedLayerId}
</div> <div class="mt-2 flex flex-row gap-2">
{:else} <Button variant="outline" on:click={createLayer} class="grow">
<Button variant="outline" class="mt-2" on:click={createLayer}> <Save size="16" class="mr-1" />
<CirclePlus size="16" class="mr-1" /> {$_('layers.custom_layers.update')}
{$_('layers.custom_layers.create')} </Button>
</Button> <Button variant="outline" on:click={() => (selectedLayerId = undefined)}>
{/if} <CircleX size="16" />
</fieldset> </Button>
</Card.Content> </div>
</Card.Root> {: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>
</div> </div>

View File

@@ -1,203 +1,219 @@
<script lang="ts"> <script lang="ts">
import CustomControl from '$lib/components/custom-control/CustomControl.svelte'; import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
import LayerTree from './LayerTree.svelte'; import LayerTree from './LayerTree.svelte';
import { Separator } from '$lib/components/ui/separator'; import { Separator } from '$lib/components/ui/separator';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js'; import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import { Layers } from 'lucide-svelte'; import { Layers } from 'lucide-svelte';
import { basemaps, defaultBasemap, overlays } from '$lib/assets/layers'; import { basemaps, defaultBasemap, overlays } from '$lib/assets/layers';
import { settings } from '$lib/db'; import { settings } from '$lib/db';
import { map } from '$lib/stores'; import { map } from '$lib/stores';
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
import { customBasemapUpdate, getLayers } from './utils'; import { customBasemapUpdate, getLayers } from './utils';
import { OverpassLayer } from './OverpassLayer'; import { OverpassLayer } from './OverpassLayer';
import OverpassPopup from './OverpassPopup.svelte'; import OverpassPopup from './OverpassPopup.svelte';
let container: HTMLDivElement; let container: HTMLDivElement;
let overpassLayer: OverpassLayer; let overpassLayer: OverpassLayer;
const { const {
currentBasemap, currentBasemap,
previousBasemap, previousBasemap,
currentOverlays, currentOverlays,
currentOverpassQueries, currentOverpassQueries,
selectedBasemapTree, selectedBasemapTree,
selectedOverlayTree, selectedOverlayTree,
selectedOverpassTree, selectedOverpassTree,
customLayers, customLayers,
opacities opacities
} = settings; } = settings;
function setStyle() { function setStyle() {
if ($map) { if ($map) {
let basemap = basemaps.hasOwnProperty($currentBasemap) let basemap = basemaps.hasOwnProperty($currentBasemap)
? basemaps[$currentBasemap] ? basemaps[$currentBasemap]
: $customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap]; : $customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap];
$map.setStyle(basemap, { $map.removeImport('basemap');
diff: false if (typeof basemap === 'string') {
}); $map.addImport({ id: 'basemap', url: basemap }, 'overlays');
} } else {
} $map.addImport(
{
id: 'basemap',
data: basemap
},
'overlays'
);
}
}
}
$: if ($map && ($currentBasemap || $customBasemapUpdate)) { $: if ($map && ($currentBasemap || $customBasemapUpdate)) {
setStyle(); setStyle();
} }
$: if ($map && $currentOverlays) { function addOverlay(id: string) {
// Add or remove overlay layers depending on the current overlays try {
let overlayLayers = getLayers($currentOverlays); let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
Object.keys(overlayLayers).forEach((id) => { if (typeof overlay === 'string') {
if (overlayLayers[id]) { $map.addImport({ id, url: overlay });
if (!addOverlayLayer.hasOwnProperty(id)) { } else {
addOverlayLayer[id] = addOverlayLayerForId(id); $map.addImport({
} id,
if (!$map.getLayer(id)) { data: overlay
addOverlayLayer[id](); });
$map.on('style.load', addOverlayLayer[id]); }
} } catch (e) {
} else if ($map.getLayer(id)) { // No reliable way to check if the map is ready to add sources and layers
$map.removeLayer(id); }
$map.off('style.load', addOverlayLayer[id]); }
}
});
}
$: if ($map) { function updateOverlays() {
if (overpassLayer) { if ($map && $currentOverlays) {
overpassLayer.remove(); let overlayLayers = getLayers($currentOverlays);
} try {
overpassLayer = new OverpassLayer($map); let activeOverlays = $map
overpassLayer.add(); .getStyle()
} .imports.filter((i) => i.id !== 'basemap' && i.id !== 'overlays');
let toRemove = activeOverlays.filter((i) => !overlayLayers[i.id]);
toRemove.forEach((i) => {
$map.removeImport(i.id);
});
let toAdd = Object.entries(overlayLayers)
.filter(
([id, selected]) => selected && !activeOverlays.some((j) => j.id === id)
)
.map(([id]) => id);
toAdd.forEach((id) => {
addOverlay(id);
});
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
}
}
}
let selectedBasemap = writable(get(currentBasemap)); $: if ($map && $currentOverlays) {
selectedBasemap.subscribe((value) => { updateOverlays();
// Updates coming from radio buttons }
if (value !== get(currentBasemap)) {
previousBasemap.set(get(currentBasemap));
currentBasemap.set(value);
}
});
currentBasemap.subscribe((value) => {
// Updates coming from the database, or from the user swapping basemaps
selectedBasemap.set(value);
});
let addOverlayLayer: { [key: string]: () => void } = {}; $: if ($map) {
function addOverlayLayerForId(id: string) { if (overpassLayer) {
return () => { overpassLayer.remove();
if ($map) { }
try { overpassLayer = new OverpassLayer($map);
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id]; overpassLayer.add();
if (!$map.getSource(id)) { $map.on('style.import.load', updateOverlays);
$map.addSource(id, overlay); }
}
$map.addLayer(
{
id,
type: overlay.type === 'raster' ? 'raster' : 'line',
source: id,
paint: {
...(id in $opacities
? overlay.type === 'raster'
? { 'raster-opacity': $opacities[id] }
: { 'line-opacity': $opacities[id] }
: {})
}
},
'overlays'
);
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
}
}
};
}
let open = false; let selectedBasemap = writable(get(currentBasemap));
function openLayerControl() { selectedBasemap.subscribe((value) => {
open = true; // Updates coming from radio buttons
} if (value !== get(currentBasemap)) {
function closeLayerControl() { previousBasemap.set(get(currentBasemap));
open = false; currentBasemap.set(value);
} }
let cancelEvents = false; });
currentBasemap.subscribe((value) => {
// Updates coming from the database, or from the user swapping basemaps
selectedBasemap.set(value);
});
function removeOverlayLayer(id: string) {
if ($map) {
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
if (overlay.layers) {
$map.removeImport(id);
} else {
$map.removeLayer(id);
}
}
}
let open = false;
function openLayerControl() {
open = true;
}
function closeLayerControl() {
open = false;
}
let cancelEvents = false;
</script> </script>
<CustomControl class="group min-w-[29px] min-h-[29px] overflow-hidden"> <CustomControl class="group min-w-[29px] min-h-[29px] overflow-hidden">
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
bind:this={container} bind:this={container}
class="h-full w-full" class="h-full w-full"
on:mouseenter={openLayerControl} on:mouseenter={openLayerControl}
on:mouseleave={closeLayerControl} on:mouseleave={closeLayerControl}
on:pointerenter={() => { on:pointerenter={() => {
if (!open) { if (!open) {
cancelEvents = true; cancelEvents = true;
openLayerControl(); openLayerControl();
setTimeout(() => { setTimeout(() => {
cancelEvents = false; cancelEvents = false;
}, 500); }, 500);
} }
}} }}
> >
<div <div
class="flex flex-row justify-center items-center delay-100 transition-[opacity] duration-0 {open class="flex flex-row justify-center items-center delay-100 transition-[opacity] duration-0 {open
? 'opacity-0 w-0 h-0 delay-0' ? 'opacity-0 w-0 h-0 delay-0'
: 'w-[29px] h-[29px]'}" : 'w-[29px] h-[29px]'}"
> >
<Layers size="20" /> <Layers size="20" />
</div> </div>
<div <div
class="transition-[grid-template-rows grid-template-cols] grid grid-rows-[0fr] grid-cols-[0fr] duration-150 h-full {open class="transition-[grid-template-rows grid-template-cols] grid grid-rows-[0fr] grid-cols-[0fr] duration-150 h-full {open
? 'grid-rows-[1fr] grid-cols-[1fr]' ? 'grid-rows-[1fr] grid-cols-[1fr]'
: ''} {cancelEvents ? 'pointer-events-none' : ''}" : ''} {cancelEvents ? 'pointer-events-none' : ''}"
> >
<ScrollArea> <ScrollArea>
<div class="h-fit"> <div class="h-fit">
<div class="p-2"> <div class="p-2">
<LayerTree <LayerTree
layerTree={$selectedBasemapTree} layerTree={$selectedBasemapTree}
name="basemaps" name="basemaps"
bind:selected={$selectedBasemap} bind:selected={$selectedBasemap}
/> />
</div> </div>
<Separator class="w-full" /> <Separator class="w-full" />
<div class="p-2"> <div class="p-2">
{#if $currentOverlays} {#if $currentOverlays}
<LayerTree <LayerTree
layerTree={$selectedOverlayTree} layerTree={$selectedOverlayTree}
name="overlays" name="overlays"
multiple={true} multiple={true}
bind:checked={$currentOverlays} bind:checked={$currentOverlays}
/> />
{/if} {/if}
</div> </div>
<Separator class="w-full" /> <Separator class="w-full" />
<div class="p-2"> <div class="p-2">
{#if $currentOverpassQueries} {#if $currentOverpassQueries}
<LayerTree <LayerTree
layerTree={$selectedOverpassTree} layerTree={$selectedOverpassTree}
name="overpass" name="overpass"
multiple={true} multiple={true}
bind:checked={$currentOverpassQueries} bind:checked={$currentOverpassQueries}
/> />
{/if} {/if}
</div> </div>
</div> </div>
</ScrollArea> </ScrollArea>
</div> </div>
</div> </div>
</CustomControl> </CustomControl>
<OverpassPopup /> <OverpassPopup />
<svelte:window <svelte:window
on:click={(e) => { on:click={(e) => {
if (open && !cancelEvents && !container.contains(e.target)) { if (open && !cancelEvents && !container.contains(e.target)) {
closeLayerControl(); closeLayerControl();
} }
}} }}
/> />

View File

@@ -50,7 +50,7 @@ export class OverpassLayer {
add() { add() {
this.map.on('moveend', this.queryIfNeededBinded); this.map.on('moveend', this.queryIfNeededBinded);
this.map.on('style.load', this.updateBinded); this.map.on('style.import.load', this.updateBinded);
this.unsubscribes.push(data.subscribe(this.updateBinded)); this.unsubscribes.push(data.subscribe(this.updateBinded));
this.unsubscribes.push(currentOverpassQueries.subscribe(() => { this.unsubscribes.push(currentOverpassQueries.subscribe(() => {
this.updateBinded(); this.updateBinded();
@@ -108,15 +108,19 @@ export class OverpassLayer {
remove() { remove() {
this.map.off('moveend', this.queryIfNeededBinded); this.map.off('moveend', this.queryIfNeededBinded);
this.map.off('style.load', this.updateBinded); this.map.off('style.import.load', this.updateBinded);
this.unsubscribes.forEach((unsubscribe) => unsubscribe()); this.unsubscribes.forEach((unsubscribe) => unsubscribe());
if (this.map.getLayer('overpass')) { try {
this.map.removeLayer('overpass'); if (this.map.getLayer('overpass')) {
} this.map.removeLayer('overpass');
}
if (this.map.getSource('overpass')) { if (this.map.getSource('overpass')) {
this.map.removeSource('overpass'); this.map.removeSource('overpass');
}
} catch (e) {
// No reliable way to check if the map is ready to remove sources and layers
} }
} }

View File

@@ -272,6 +272,7 @@
"finlandTopo": "Lantmäteriverket Terrängkarta", "finlandTopo": "Lantmäteriverket Terrängkarta",
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite", "cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",