mirror of
				https://github.com/gpxstudio/gpx.studio.git
				synced 2025-11-04 13:31:13 +00:00 
			
		
		
		
	use style imports instead of layers to allow stacking mapbox styles, closes #32
This commit is contained in:
		
							
								
								
									
										1864
									
								
								website/src/lib/assets/custom/bikerouter-gravel.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1864
									
								
								website/src/lib/assets/custom/bikerouter-gravel.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -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 { type AnySourceData, type Style } from 'mapbox-gl';
 | 
			
		||||
import { type Style } from 'mapbox-gl';
 | 
			
		||||
import ignFrTopo from './custom/ign-fr-topo.json';
 | 
			
		||||
import ignFrPlan from './custom/ign-fr-plan.json';
 | 
			
		||||
import ignFrSatellite from './custom/ign-fr-satellite.json';
 | 
			
		||||
import bikerouterGravel from './custom/bikerouter-gravel.json';
 | 
			
		||||
 | 
			
		||||
export const basemaps: { [key: string]: string | Style; } = {
 | 
			
		||||
    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 {
 | 
			
		||||
    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; } = {
 | 
			
		||||
export const overlays: { [key: string]: string | Style; } = {
 | 
			
		||||
    cyclOSMlite: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        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: '© <a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            cyclOSMlite: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                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: '© <a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        layers: [{
 | 
			
		||||
            id: 'cyclOSMlite',
 | 
			
		||||
            type: 'raster',
 | 
			
		||||
            source: 'cyclOSMlite',
 | 
			
		||||
        }],
 | 
			
		||||
    },
 | 
			
		||||
    bikerouterGravel: bikerouterGravel,
 | 
			
		||||
    swisstopoSlope: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        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: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>',
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            swisstopoSlope: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                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: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        layers: [{
 | 
			
		||||
            id: 'swisstopoSlope',
 | 
			
		||||
            type: 'raster',
 | 
			
		||||
            source: 'swisstopoSlope',
 | 
			
		||||
        }],
 | 
			
		||||
    },
 | 
			
		||||
    swisstopoHiking: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        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: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            swisstopoHiking: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                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: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        layers: [{
 | 
			
		||||
            id: 'swisstopoHiking',
 | 
			
		||||
            type: 'raster',
 | 
			
		||||
            source: 'swisstopoHiking',
 | 
			
		||||
        }],
 | 
			
		||||
    },
 | 
			
		||||
    swisstopoHikingClosures: {
 | 
			
		||||
        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: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            swisstopoHikingClosures: {
 | 
			
		||||
                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: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        layers: [{
 | 
			
		||||
            id: 'swisstopoHikingClosures',
 | 
			
		||||
            type: 'raster',
 | 
			
		||||
            source: 'swisstopoHikingClosures',
 | 
			
		||||
        }],
 | 
			
		||||
    },
 | 
			
		||||
    swisstopoCycling: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.veloland/default/current/3857/{z}/{x}/{y}.png'],
 | 
			
		||||
        tileSize: 256,
 | 
			
		||||
        maxzoom: 18,
 | 
			
		||||
        attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            swisstopoCycling: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.veloland/default/current/3857/{z}/{x}/{y}.png'],
 | 
			
		||||
                tileSize: 256,
 | 
			
		||||
                maxzoom: 18,
 | 
			
		||||
                attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        layers: [{
 | 
			
		||||
            id: 'swisstopoCycling',
 | 
			
		||||
            type: 'raster',
 | 
			
		||||
            source: 'swisstopoCycling',
 | 
			
		||||
        }],
 | 
			
		||||
    },
 | 
			
		||||
    swisstopoCyclingClosures: {
 | 
			
		||||
        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: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            swisstopoCyclingClosures: {
 | 
			
		||||
                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: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        layers: [{
 | 
			
		||||
            id: 'swisstopoCyclingClosures',
 | 
			
		||||
            type: 'raster',
 | 
			
		||||
            source: 'swisstopoCyclingClosures',
 | 
			
		||||
        }],
 | 
			
		||||
    },
 | 
			
		||||
    swisstopoMountainBike: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.mountainbikeland/default/current/3857/{z}/{x}/{y}.png'],
 | 
			
		||||
        tileSize: 256,
 | 
			
		||||
        maxzoom: 18,
 | 
			
		||||
        attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            swisstopoMountainBike: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.mountainbikeland/default/current/3857/{z}/{x}/{y}.png'],
 | 
			
		||||
                tileSize: 256,
 | 
			
		||||
                maxzoom: 18,
 | 
			
		||||
                attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        layers: [{
 | 
			
		||||
            id: 'swisstopoMountainBike',
 | 
			
		||||
            type: 'raster',
 | 
			
		||||
            source: 'swisstopoMountainBike',
 | 
			
		||||
        }],
 | 
			
		||||
    },
 | 
			
		||||
    swisstopoMountainBikeClosures: {
 | 
			
		||||
        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: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            swisstopoMountainBikeClosures: {
 | 
			
		||||
                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: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        layers: [{
 | 
			
		||||
            id: 'swisstopoMountainBikeClosures',
 | 
			
		||||
            type: 'raster',
 | 
			
		||||
            source: 'swisstopoMountainBikeClosures',
 | 
			
		||||
        }],
 | 
			
		||||
    },
 | 
			
		||||
    swisstopoSkiTouring: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        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: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            swisstopoSkiTouring: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                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: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        layers: [{
 | 
			
		||||
            id: 'swisstopoSkiTouring',
 | 
			
		||||
            type: 'raster',
 | 
			
		||||
            source: 'swisstopoSkiTouring',
 | 
			
		||||
        }],
 | 
			
		||||
    },
 | 
			
		||||
    ignFrCadastre: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        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'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            ignFrCadastre: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                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: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        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,
 | 
			
		||||
        maxzoom: 17,
 | 
			
		||||
        attribution: 'IGN-F/Géoportail'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            ignSlope: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                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: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        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'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            ignSkiTouring: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                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: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        tiles: ['https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png'],
 | 
			
		||||
        tileSize: 256,
 | 
			
		||||
        maxzoom: 18,
 | 
			
		||||
        attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            waymarkedTrailsHiking: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                tiles: ['https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png'],
 | 
			
		||||
                tileSize: 256,
 | 
			
		||||
                maxzoom: 18,
 | 
			
		||||
                attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        layers: [{
 | 
			
		||||
            id: 'waymarkedTrailsHiking',
 | 
			
		||||
            type: 'raster',
 | 
			
		||||
            source: 'waymarkedTrailsHiking',
 | 
			
		||||
        }],
 | 
			
		||||
    },
 | 
			
		||||
    waymarkedTrailsCycling: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        tiles: ['https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png'],
 | 
			
		||||
        tileSize: 256,
 | 
			
		||||
        maxzoom: 18,
 | 
			
		||||
        attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            waymarkedTrailsCycling: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                tiles: ['https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png'],
 | 
			
		||||
                tileSize: 256,
 | 
			
		||||
                maxzoom: 18,
 | 
			
		||||
                attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        layers: [{
 | 
			
		||||
            id: 'waymarkedTrailsCycling',
 | 
			
		||||
            type: 'raster',
 | 
			
		||||
            source: 'waymarkedTrailsCycling',
 | 
			
		||||
        }],
 | 
			
		||||
    },
 | 
			
		||||
    waymarkedTrailsMTB: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        tiles: ['https://tile.waymarkedtrails.org/mtb/{z}/{x}/{y}.png'],
 | 
			
		||||
        tileSize: 256,
 | 
			
		||||
        maxzoom: 18,
 | 
			
		||||
        attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            waymarkedTrailsMTB: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                tiles: ['https://tile.waymarkedtrails.org/mtb/{z}/{x}/{y}.png'],
 | 
			
		||||
                tileSize: 256,
 | 
			
		||||
                maxzoom: 18,
 | 
			
		||||
                attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        layers: [{
 | 
			
		||||
            id: 'waymarkedTrailsMTB',
 | 
			
		||||
            type: 'raster',
 | 
			
		||||
            source: 'waymarkedTrailsMTB',
 | 
			
		||||
        }],
 | 
			
		||||
    },
 | 
			
		||||
    waymarkedTrailsSkating: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        tiles: ['https://tile.waymarkedtrails.org/skating/{z}/{x}/{y}.png'],
 | 
			
		||||
        tileSize: 256,
 | 
			
		||||
        maxzoom: 18,
 | 
			
		||||
        attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            waymarkedTrailsSkating: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                tiles: ['https://tile.waymarkedtrails.org/skating/{z}/{x}/{y}.png'],
 | 
			
		||||
                tileSize: 256,
 | 
			
		||||
                maxzoom: 18,
 | 
			
		||||
                attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        layers: [{
 | 
			
		||||
            id: 'waymarkedTrailsSkating',
 | 
			
		||||
            type: 'raster',
 | 
			
		||||
            source: 'waymarkedTrailsSkating',
 | 
			
		||||
        }],
 | 
			
		||||
    },
 | 
			
		||||
    waymarkedTrailsHorseRiding: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        tiles: ['https://tile.waymarkedtrails.org/riding/{z}/{x}/{y}.png'],
 | 
			
		||||
        tileSize: 256,
 | 
			
		||||
        maxzoom: 18,
 | 
			
		||||
        attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            waymarkedTrailsHorseRiding: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                tiles: ['https://tile.waymarkedtrails.org/riding/{z}/{x}/{y}.png'],
 | 
			
		||||
                tileSize: 256,
 | 
			
		||||
                maxzoom: 18,
 | 
			
		||||
                attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        layers: [{
 | 
			
		||||
            id: 'waymarkedTrailsHorseRiding',
 | 
			
		||||
            type: 'raster',
 | 
			
		||||
            source: 'waymarkedTrailsHorseRiding',
 | 
			
		||||
        }],
 | 
			
		||||
    },
 | 
			
		||||
    waymarkedTrailsWinter: {
 | 
			
		||||
        type: 'raster',
 | 
			
		||||
        tiles: ['https://tile.waymarkedtrails.org/slopes/{z}/{x}/{y}.png'],
 | 
			
		||||
        tileSize: 256,
 | 
			
		||||
        maxzoom: 18,
 | 
			
		||||
        attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
 | 
			
		||||
        version: 8,
 | 
			
		||||
        sources: {
 | 
			
		||||
            waymarkedTrailsWinter: {
 | 
			
		||||
                type: 'raster',
 | 
			
		||||
                tiles: ['https://tile.waymarkedtrails.org/slopes/{z}/{x}/{y}.png'],
 | 
			
		||||
                tileSize: 256,
 | 
			
		||||
                maxzoom: 18,
 | 
			
		||||
                attribution: '© <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 = {
 | 
			
		||||
    overlays: {
 | 
			
		||||
        world: {
 | 
			
		||||
            cyclOSM: {
 | 
			
		||||
                cyclOSMlite: true,
 | 
			
		||||
            },
 | 
			
		||||
            waymarked_trails: {
 | 
			
		||||
                waymarkedTrailsHiking: true,
 | 
			
		||||
                waymarkedTrailsCycling: true,
 | 
			
		||||
@@ -473,7 +635,9 @@ export const overlayTree: LayerTreeType = {
 | 
			
		||||
                waymarkedTrailsSkating: true,
 | 
			
		||||
                waymarkedTrailsHorseRiding: true,
 | 
			
		||||
                waymarkedTrailsWinter: true,
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
            cyclOSMlite: true,
 | 
			
		||||
            bikerouterGravel: true,
 | 
			
		||||
        },
 | 
			
		||||
        countries: {
 | 
			
		||||
            france: {
 | 
			
		||||
@@ -547,9 +711,6 @@ export const defaultBasemap = 'mapboxOutdoors';
 | 
			
		||||
export const defaultOverlays = {
 | 
			
		||||
    overlays: {
 | 
			
		||||
        world: {
 | 
			
		||||
            cyclOSM: {
 | 
			
		||||
                cyclOSMlite: false,
 | 
			
		||||
            },
 | 
			
		||||
            waymarked_trails: {
 | 
			
		||||
                waymarkedTrailsHiking: false,
 | 
			
		||||
                waymarkedTrailsCycling: false,
 | 
			
		||||
@@ -557,7 +718,9 @@ export const defaultOverlays = {
 | 
			
		||||
                waymarkedTrailsSkating: false,
 | 
			
		||||
                waymarkedTrailsHorseRiding: false,
 | 
			
		||||
                waymarkedTrailsWinter: false,
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
            cyclOSMlite: false,
 | 
			
		||||
            bikerouterGravel: false,
 | 
			
		||||
        },
 | 
			
		||||
        countries: {
 | 
			
		||||
            france: {
 | 
			
		||||
@@ -683,9 +846,6 @@ export const defaultBasemapTree: LayerTreeType = {
 | 
			
		||||
export const defaultOverlayTree: LayerTreeType = {
 | 
			
		||||
    overlays: {
 | 
			
		||||
        world: {
 | 
			
		||||
            cyclOSM: {
 | 
			
		||||
                cyclOSMlite: false,
 | 
			
		||||
            },
 | 
			
		||||
            waymarked_trails: {
 | 
			
		||||
                waymarkedTrailsHiking: true,
 | 
			
		||||
                waymarkedTrailsCycling: true,
 | 
			
		||||
@@ -693,7 +853,9 @@ export const defaultOverlayTree: LayerTreeType = {
 | 
			
		||||
                waymarkedTrailsSkating: false,
 | 
			
		||||
                waymarkedTrailsHorseRiding: false,
 | 
			
		||||
                waymarkedTrailsWinter: false,
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
            cyclOSMlite: false,
 | 
			
		||||
            bikerouterGravel: false,
 | 
			
		||||
        },
 | 
			
		||||
        countries: {
 | 
			
		||||
            france: {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,331 +1,355 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
    import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
 | 
			
		||||
	import mapboxgl from 'mapbox-gl';
 | 
			
		||||
	import 'mapbox-gl/dist/mapbox-gl.css';
 | 
			
		||||
    import mapboxgl from 'mapbox-gl';
 | 
			
		||||
    import 'mapbox-gl/dist/mapbox-gl.css';
 | 
			
		||||
 | 
			
		||||
	import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
 | 
			
		||||
	import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
 | 
			
		||||
    import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
 | 
			
		||||
    import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
 | 
			
		||||
 | 
			
		||||
	import { Button } from '$lib/components/ui/button';
 | 
			
		||||
	import { map } from '$lib/stores';
 | 
			
		||||
	import { settings } from '$lib/db';
 | 
			
		||||
	import { _ } from 'svelte-i18n';
 | 
			
		||||
	import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
 | 
			
		||||
	import { page } from '$app/stores';
 | 
			
		||||
    import { Button } from '$lib/components/ui/button';
 | 
			
		||||
    import { map } from '$lib/stores';
 | 
			
		||||
    import { settings } from '$lib/db';
 | 
			
		||||
    import { _ } from 'svelte-i18n';
 | 
			
		||||
    import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
 | 
			
		||||
    import { page } from '$app/stores';
 | 
			
		||||
 | 
			
		||||
	export let accessToken = PUBLIC_MAPBOX_TOKEN;
 | 
			
		||||
	export let geolocate = true;
 | 
			
		||||
	export let geocoder = true;
 | 
			
		||||
	export let hash = true;
 | 
			
		||||
    export let accessToken = PUBLIC_MAPBOX_TOKEN;
 | 
			
		||||
    export let geolocate = true;
 | 
			
		||||
    export let geocoder = true;
 | 
			
		||||
    export let hash = true;
 | 
			
		||||
 | 
			
		||||
	mapboxgl.accessToken = accessToken;
 | 
			
		||||
    mapboxgl.accessToken = accessToken;
 | 
			
		||||
 | 
			
		||||
	let webgl2Supported = true;
 | 
			
		||||
	let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
 | 
			
		||||
		maxZoom: 15,
 | 
			
		||||
		linear: true,
 | 
			
		||||
		easing: () => 1
 | 
			
		||||
	};
 | 
			
		||||
    let webgl2Supported = true;
 | 
			
		||||
    let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
 | 
			
		||||
        maxZoom: 15,
 | 
			
		||||
        linear: true,
 | 
			
		||||
        easing: () => 1
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
	const { distanceUnits, elevationProfile, verticalFileView, bottomPanelSize, rightPanelSize } =
 | 
			
		||||
		settings;
 | 
			
		||||
	let scaleControl = new mapboxgl.ScaleControl({
 | 
			
		||||
		unit: $distanceUnits
 | 
			
		||||
	});
 | 
			
		||||
    const { distanceUnits, elevationProfile, verticalFileView, bottomPanelSize, rightPanelSize } =
 | 
			
		||||
        settings;
 | 
			
		||||
    let scaleControl = new mapboxgl.ScaleControl({
 | 
			
		||||
        unit: $distanceUnits
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		let gl = document.createElement('canvas').getContext('webgl2');
 | 
			
		||||
		if (!gl) {
 | 
			
		||||
			webgl2Supported = false;
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
    onMount(() => {
 | 
			
		||||
        let gl = document.createElement('canvas').getContext('webgl2');
 | 
			
		||||
        if (!gl) {
 | 
			
		||||
            webgl2Supported = false;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		let language = $page.params.language;
 | 
			
		||||
		if (language === 'zh') {
 | 
			
		||||
			language = 'zh-Hans';
 | 
			
		||||
		} else if (language?.includes('-')) {
 | 
			
		||||
			language = language.split('-')[0];
 | 
			
		||||
		} else if (language === '' || language === undefined) {
 | 
			
		||||
			language = 'en';
 | 
			
		||||
		}
 | 
			
		||||
        let language = $page.params.language;
 | 
			
		||||
        if (language === 'zh') {
 | 
			
		||||
            language = 'zh-Hans';
 | 
			
		||||
        } else if (language?.includes('-')) {
 | 
			
		||||
            language = language.split('-')[0];
 | 
			
		||||
        } else if (language === '' || language === undefined) {
 | 
			
		||||
            language = 'en';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		let newMap = new mapboxgl.Map({
 | 
			
		||||
			container: 'map',
 | 
			
		||||
			style: { 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);
 | 
			
		||||
		});
 | 
			
		||||
        let newMap = new mapboxgl.Map({
 | 
			
		||||
            container: 'map',
 | 
			
		||||
            style: {
 | 
			
		||||
                version: 8,
 | 
			
		||||
                sources: {},
 | 
			
		||||
                layers: [],
 | 
			
		||||
                imports: [
 | 
			
		||||
                    {
 | 
			
		||||
                        id: 'glyphs-and-sprite', // make Mapbox glyphs and sprite available to other styles
 | 
			
		||||
                        url: '',
 | 
			
		||||
                        data: {
 | 
			
		||||
                            version: 8,
 | 
			
		||||
                            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(
 | 
			
		||||
			new mapboxgl.AttributionControl({
 | 
			
		||||
				compact: true
 | 
			
		||||
			})
 | 
			
		||||
		);
 | 
			
		||||
        newMap.addControl(
 | 
			
		||||
            new mapboxgl.AttributionControl({
 | 
			
		||||
                compact: true
 | 
			
		||||
            })
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
		newMap.addControl(
 | 
			
		||||
			new mapboxgl.NavigationControl({
 | 
			
		||||
				visualizePitch: true
 | 
			
		||||
			})
 | 
			
		||||
		);
 | 
			
		||||
        newMap.addControl(
 | 
			
		||||
            new mapboxgl.NavigationControl({
 | 
			
		||||
                visualizePitch: true
 | 
			
		||||
            })
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
		if (geocoder) {
 | 
			
		||||
			newMap.addControl(
 | 
			
		||||
				new MapboxGeocoder({
 | 
			
		||||
					accessToken: mapboxgl.accessToken,
 | 
			
		||||
					mapboxgl: mapboxgl,
 | 
			
		||||
					collapsed: true,
 | 
			
		||||
					flyTo: fitBoundsOptions,
 | 
			
		||||
					language
 | 
			
		||||
				})
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
        if (geocoder) {
 | 
			
		||||
            newMap.addControl(
 | 
			
		||||
                new MapboxGeocoder({
 | 
			
		||||
                    accessToken: mapboxgl.accessToken,
 | 
			
		||||
                    mapboxgl: mapboxgl,
 | 
			
		||||
                    collapsed: true,
 | 
			
		||||
                    flyTo: fitBoundsOptions,
 | 
			
		||||
                    language
 | 
			
		||||
                })
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		if (geolocate) {
 | 
			
		||||
			newMap.addControl(
 | 
			
		||||
				new mapboxgl.GeolocateControl({
 | 
			
		||||
					positionOptions: {
 | 
			
		||||
						enableHighAccuracy: true
 | 
			
		||||
					},
 | 
			
		||||
					fitBoundsOptions,
 | 
			
		||||
					trackUserLocation: true,
 | 
			
		||||
					showUserHeading: true
 | 
			
		||||
				})
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
        if (geolocate) {
 | 
			
		||||
            newMap.addControl(
 | 
			
		||||
                new mapboxgl.GeolocateControl({
 | 
			
		||||
                    positionOptions: {
 | 
			
		||||
                        enableHighAccuracy: true
 | 
			
		||||
                    },
 | 
			
		||||
                    fitBoundsOptions,
 | 
			
		||||
                    trackUserLocation: true,
 | 
			
		||||
                    showUserHeading: true
 | 
			
		||||
                })
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		newMap.addControl(scaleControl);
 | 
			
		||||
        newMap.addControl(scaleControl);
 | 
			
		||||
 | 
			
		||||
		newMap.on('style.load', () => {
 | 
			
		||||
			newMap.addSource('mapbox-dem', {
 | 
			
		||||
				type: 'raster-dem',
 | 
			
		||||
				url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
 | 
			
		||||
				tileSize: 512,
 | 
			
		||||
				maxzoom: 14
 | 
			
		||||
			});
 | 
			
		||||
			newMap.setTerrain({
 | 
			
		||||
				source: 'mapbox-dem',
 | 
			
		||||
				exaggeration: newMap.getPitch() > 0 ? 1 : 0
 | 
			
		||||
			});
 | 
			
		||||
			newMap.setFog({
 | 
			
		||||
				color: 'rgb(186, 210, 235)',
 | 
			
		||||
				'high-color': 'rgb(36, 92, 223)',
 | 
			
		||||
				'horizon-blend': 0.1,
 | 
			
		||||
				'space-color': 'rgb(156, 240, 255)'
 | 
			
		||||
			});
 | 
			
		||||
			newMap.on('pitch', () => {
 | 
			
		||||
				if (newMap.getPitch() > 0) {
 | 
			
		||||
					newMap.setTerrain({
 | 
			
		||||
						source: 'mapbox-dem',
 | 
			
		||||
						exaggeration: 1
 | 
			
		||||
					});
 | 
			
		||||
				} else {
 | 
			
		||||
					newMap.setTerrain({
 | 
			
		||||
						source: 'mapbox-dem',
 | 
			
		||||
						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)'
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
        newMap.on('style.load', () => {
 | 
			
		||||
            newMap.addSource('mapbox-dem', {
 | 
			
		||||
                type: 'raster-dem',
 | 
			
		||||
                url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
 | 
			
		||||
                tileSize: 512,
 | 
			
		||||
                maxzoom: 14
 | 
			
		||||
            });
 | 
			
		||||
            newMap.setTerrain({
 | 
			
		||||
                source: 'mapbox-dem',
 | 
			
		||||
                exaggeration: newMap.getPitch() > 0 ? 1 : 0
 | 
			
		||||
            });
 | 
			
		||||
            newMap.setFog({
 | 
			
		||||
                color: 'rgb(186, 210, 235)',
 | 
			
		||||
                'high-color': 'rgb(36, 92, 223)',
 | 
			
		||||
                'horizon-blend': 0.1,
 | 
			
		||||
                'space-color': 'rgb(156, 240, 255)'
 | 
			
		||||
            });
 | 
			
		||||
            newMap.on('pitch', () => {
 | 
			
		||||
                if (newMap.getPitch() > 0) {
 | 
			
		||||
                    newMap.setTerrain({
 | 
			
		||||
                        source: 'mapbox-dem',
 | 
			
		||||
                        exaggeration: 1
 | 
			
		||||
                    });
 | 
			
		||||
                } else {
 | 
			
		||||
                    newMap.setTerrain({
 | 
			
		||||
                        source: 'mapbox-dem',
 | 
			
		||||
                        exaggeration: 0
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
	onDestroy(() => {
 | 
			
		||||
		if ($map) {
 | 
			
		||||
			$map.remove();
 | 
			
		||||
			$map = null;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
    onDestroy(() => {
 | 
			
		||||
        if ($map) {
 | 
			
		||||
            $map.remove();
 | 
			
		||||
            $map = null;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
	$: if (
 | 
			
		||||
		$map &&
 | 
			
		||||
		(!$verticalFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)
 | 
			
		||||
	) {
 | 
			
		||||
		$map.resize();
 | 
			
		||||
	}
 | 
			
		||||
    $: if (
 | 
			
		||||
        $map &&
 | 
			
		||||
        (!$verticalFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)
 | 
			
		||||
    ) {
 | 
			
		||||
        $map.resize();
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div {...$$restProps}>
 | 
			
		||||
	<div id="map" class="h-full {webgl2Supported ? '' : 'hidden'}"></div>
 | 
			
		||||
	<div
 | 
			
		||||
		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')}
 | 
			
		||||
		</Button>
 | 
			
		||||
	</div>
 | 
			
		||||
    <div id="map" class="h-full {webgl2Supported ? '' : 'hidden'}"></div>
 | 
			
		||||
    <div
 | 
			
		||||
        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')}
 | 
			
		||||
        </Button>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style lang="postcss">
 | 
			
		||||
	div :global(.mapboxgl-map) {
 | 
			
		||||
		@apply font-sans;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-map) {
 | 
			
		||||
        @apply font-sans;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) {
 | 
			
		||||
		@apply shadow-md;
 | 
			
		||||
		@apply bg-background;
 | 
			
		||||
		@apply text-foreground;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) {
 | 
			
		||||
        @apply shadow-md;
 | 
			
		||||
        @apply bg-background;
 | 
			
		||||
        @apply text-foreground;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-ctrl-icon) {
 | 
			
		||||
		@apply dark:brightness-[4.7];
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-ctrl-icon) {
 | 
			
		||||
        @apply dark:brightness-[4.7];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-ctrl-geocoder) {
 | 
			
		||||
		@apply flex;
 | 
			
		||||
		@apply flex-row;
 | 
			
		||||
		@apply w-fit;
 | 
			
		||||
		@apply min-w-fit;
 | 
			
		||||
		@apply items-center;
 | 
			
		||||
		@apply shadow-md;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-ctrl-geocoder) {
 | 
			
		||||
        @apply flex;
 | 
			
		||||
        @apply flex-row;
 | 
			
		||||
        @apply w-fit;
 | 
			
		||||
        @apply min-w-fit;
 | 
			
		||||
        @apply items-center;
 | 
			
		||||
        @apply shadow-md;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.suggestions) {
 | 
			
		||||
		@apply shadow-md;
 | 
			
		||||
		@apply bg-background;
 | 
			
		||||
		@apply text-foreground;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.suggestions) {
 | 
			
		||||
        @apply shadow-md;
 | 
			
		||||
        @apply bg-background;
 | 
			
		||||
        @apply text-foreground;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) {
 | 
			
		||||
		@apply text-foreground;
 | 
			
		||||
		@apply hover:text-accent-foreground;
 | 
			
		||||
		@apply hover:bg-accent;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) {
 | 
			
		||||
        @apply text-foreground;
 | 
			
		||||
        @apply hover:text-accent-foreground;
 | 
			
		||||
        @apply hover:bg-accent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) {
 | 
			
		||||
		@apply bg-background;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) {
 | 
			
		||||
        @apply bg-background;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-ctrl-geocoder--button) {
 | 
			
		||||
		@apply bg-transparent;
 | 
			
		||||
		@apply hover:bg-transparent;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-ctrl-geocoder--button) {
 | 
			
		||||
        @apply bg-transparent;
 | 
			
		||||
        @apply hover:bg-transparent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-ctrl-geocoder--icon) {
 | 
			
		||||
		@apply fill-foreground;
 | 
			
		||||
		@apply hover:fill-accent-foreground;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-ctrl-geocoder--icon) {
 | 
			
		||||
        @apply fill-foreground;
 | 
			
		||||
        @apply hover:fill-accent-foreground;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-ctrl-geocoder--icon-search) {
 | 
			
		||||
		@apply relative;
 | 
			
		||||
		@apply top-0;
 | 
			
		||||
		@apply left-0;
 | 
			
		||||
		@apply my-2;
 | 
			
		||||
		@apply w-[29px];
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-ctrl-geocoder--icon-search) {
 | 
			
		||||
        @apply relative;
 | 
			
		||||
        @apply top-0;
 | 
			
		||||
        @apply left-0;
 | 
			
		||||
        @apply my-2;
 | 
			
		||||
        @apply w-[29px];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-ctrl-geocoder--input) {
 | 
			
		||||
		@apply relative;
 | 
			
		||||
		@apply w-64;
 | 
			
		||||
		@apply py-0;
 | 
			
		||||
		@apply pl-2;
 | 
			
		||||
		@apply focus:outline-none;
 | 
			
		||||
		@apply transition-[width];
 | 
			
		||||
		@apply duration-200;
 | 
			
		||||
		@apply text-foreground;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-ctrl-geocoder--input) {
 | 
			
		||||
        @apply relative;
 | 
			
		||||
        @apply w-64;
 | 
			
		||||
        @apply py-0;
 | 
			
		||||
        @apply pl-2;
 | 
			
		||||
        @apply focus:outline-none;
 | 
			
		||||
        @apply transition-[width];
 | 
			
		||||
        @apply duration-200;
 | 
			
		||||
        @apply text-foreground;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) {
 | 
			
		||||
		@apply w-0;
 | 
			
		||||
		@apply p-0;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) {
 | 
			
		||||
        @apply w-0;
 | 
			
		||||
        @apply p-0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-ctrl-top-right) {
 | 
			
		||||
		@apply z-40;
 | 
			
		||||
		@apply flex;
 | 
			
		||||
		@apply flex-col;
 | 
			
		||||
		@apply items-end;
 | 
			
		||||
		@apply h-full;
 | 
			
		||||
		@apply overflow-hidden;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-ctrl-top-right) {
 | 
			
		||||
        @apply z-40;
 | 
			
		||||
        @apply flex;
 | 
			
		||||
        @apply flex-col;
 | 
			
		||||
        @apply items-end;
 | 
			
		||||
        @apply h-full;
 | 
			
		||||
        @apply overflow-hidden;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	.horizontal :global(.mapboxgl-ctrl-bottom-left) {
 | 
			
		||||
		@apply bottom-[42px];
 | 
			
		||||
	}
 | 
			
		||||
    .horizontal :global(.mapboxgl-ctrl-bottom-left) {
 | 
			
		||||
        @apply bottom-[42px];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	.horizontal :global(.mapboxgl-ctrl-bottom-right) {
 | 
			
		||||
		@apply bottom-[42px];
 | 
			
		||||
	}
 | 
			
		||||
    .horizontal :global(.mapboxgl-ctrl-bottom-right) {
 | 
			
		||||
        @apply bottom-[42px];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-ctrl-attrib) {
 | 
			
		||||
		@apply dark:bg-transparent;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-ctrl-attrib) {
 | 
			
		||||
        @apply dark:bg-transparent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) {
 | 
			
		||||
		@apply dark:bg-background;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) {
 | 
			
		||||
        @apply dark:bg-background;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-ctrl-attrib-button) {
 | 
			
		||||
		@apply dark:bg-foreground;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-ctrl-attrib-button) {
 | 
			
		||||
        @apply dark:bg-foreground;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) {
 | 
			
		||||
		@apply dark:bg-foreground;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) {
 | 
			
		||||
        @apply dark:bg-foreground;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-ctrl-attrib a) {
 | 
			
		||||
		@apply text-foreground;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-ctrl-attrib a) {
 | 
			
		||||
        @apply text-foreground;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-popup) {
 | 
			
		||||
		@apply w-fit;
 | 
			
		||||
		@apply z-20;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-popup) {
 | 
			
		||||
        @apply w-fit;
 | 
			
		||||
        @apply z-20;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-popup-content) {
 | 
			
		||||
		@apply p-0;
 | 
			
		||||
		@apply bg-transparent;
 | 
			
		||||
		@apply shadow-none;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-popup-content) {
 | 
			
		||||
        @apply p-0;
 | 
			
		||||
        @apply bg-transparent;
 | 
			
		||||
        @apply shadow-none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) {
 | 
			
		||||
		@apply border-b-background;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) {
 | 
			
		||||
        @apply border-b-background;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) {
 | 
			
		||||
		@apply border-b-background;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) {
 | 
			
		||||
        @apply border-b-background;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) {
 | 
			
		||||
		@apply border-b-background;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) {
 | 
			
		||||
        @apply border-b-background;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) {
 | 
			
		||||
		@apply border-t-background;
 | 
			
		||||
		@apply drop-shadow-md;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) {
 | 
			
		||||
        @apply border-t-background;
 | 
			
		||||
        @apply drop-shadow-md;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) {
 | 
			
		||||
		@apply border-t-background;
 | 
			
		||||
		@apply drop-shadow-md;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) {
 | 
			
		||||
        @apply border-t-background;
 | 
			
		||||
        @apply drop-shadow-md;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) {
 | 
			
		||||
		@apply border-t-background;
 | 
			
		||||
		@apply drop-shadow-md;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) {
 | 
			
		||||
        @apply border-t-background;
 | 
			
		||||
        @apply drop-shadow-md;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) {
 | 
			
		||||
		@apply border-r-background;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) {
 | 
			
		||||
        @apply border-r-background;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) {
 | 
			
		||||
		@apply border-l-background;
 | 
			
		||||
	}
 | 
			
		||||
    div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) {
 | 
			
		||||
        @apply border-l-background;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
 | 
			
		||||
import { font } from "$lib/assets/layers";
 | 
			
		||||
import { settings } from "$lib/db";
 | 
			
		||||
import { gpxStatistics } from "$lib/stores";
 | 
			
		||||
import { get } from "svelte/store";
 | 
			
		||||
 | 
			
		||||
const { distanceMarkers, distanceUnits, currentBasemap } = settings;
 | 
			
		||||
const { distanceMarkers, distanceUnits } = settings;
 | 
			
		||||
 | 
			
		||||
export class DistanceMarkers {
 | 
			
		||||
    map: mapboxgl.Map;
 | 
			
		||||
@@ -17,7 +16,7 @@ export class DistanceMarkers {
 | 
			
		||||
        this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
 | 
			
		||||
        this.unsubscribes.push(distanceMarkers.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() {
 | 
			
		||||
@@ -40,7 +39,7 @@ export class DistanceMarkers {
 | 
			
		||||
                        layout: {
 | 
			
		||||
                            'text-field': ['get', 'distance'],
 | 
			
		||||
                            'text-size': 14,
 | 
			
		||||
                            'text-font': [font[get(currentBasemap)] ?? 'Open Sans Bold'],
 | 
			
		||||
                            'text-font': ['Open Sans Bold'],
 | 
			
		||||
                            'text-padding': 20,
 | 
			
		||||
                        },
 | 
			
		||||
                        paint: {
 | 
			
		||||
 
 | 
			
		||||
@@ -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 type { Waypoint } from "gpx";
 | 
			
		||||
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 { MapPin, Square } from "lucide-static";
 | 
			
		||||
import { getSymbolKey, symbols } from "$lib/assets/symbols";
 | 
			
		||||
@@ -66,7 +65,7 @@ function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
 | 
			
		||||
    </svg>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const { directionMarkers, verticalFileView, currentBasemap, defaultOpacity, defaultWeight } = settings;
 | 
			
		||||
const { directionMarkers, verticalFileView, defaultOpacity, defaultWeight } = settings;
 | 
			
		||||
 | 
			
		||||
export class GPXLayer {
 | 
			
		||||
    map: mapboxgl.Map;
 | 
			
		||||
@@ -112,7 +111,7 @@ export class GPXLayer {
 | 
			
		||||
        }));
 | 
			
		||||
        this.draggable = get(currentTool) === Tool.WAYPOINT;
 | 
			
		||||
 | 
			
		||||
        this.map.on('style.load', this.updateBinded);
 | 
			
		||||
        this.map.on('style.import.load', this.updateBinded);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    update() {
 | 
			
		||||
@@ -170,7 +169,7 @@ export class GPXLayer {
 | 
			
		||||
                            'text-keep-upright': false,
 | 
			
		||||
                            'text-max-angle': 361,
 | 
			
		||||
                            'text-allow-overlap': true,
 | 
			
		||||
                            'text-font': [font[get(currentBasemap)] ?? 'Open Sans Bold'],
 | 
			
		||||
                            'text-font': ['Open Sans Bold'],
 | 
			
		||||
                            'symbol-placement': 'line',
 | 
			
		||||
                            'symbol-spacing': 20,
 | 
			
		||||
                        },
 | 
			
		||||
@@ -294,7 +293,7 @@ export class GPXLayer {
 | 
			
		||||
 | 
			
		||||
    updateMap(map: mapboxgl.Map) {
 | 
			
		||||
        this.map = map;
 | 
			
		||||
        this.map.on('style.load', this.updateBinded);
 | 
			
		||||
        this.map.on('style.import.load', this.updateBinded);
 | 
			
		||||
        this.update();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -303,7 +302,7 @@ export class GPXLayer {
 | 
			
		||||
            this.map.off('click', this.fileId, this.layerOnClickBinded);
 | 
			
		||||
            this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
 | 
			
		||||
            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')) {
 | 
			
		||||
                this.map.removeLayer(this.fileId + '-direction');
 | 
			
		||||
 
 | 
			
		||||
@@ -1,424 +1,427 @@
 | 
			
		||||
<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 { Separator } from '$lib/components/ui/separator';
 | 
			
		||||
	import * as RadioGroup from '$lib/components/ui/radio-group';
 | 
			
		||||
	import {
 | 
			
		||||
		CirclePlus,
 | 
			
		||||
		CircleX,
 | 
			
		||||
		Minus,
 | 
			
		||||
		Pencil,
 | 
			
		||||
		Plus,
 | 
			
		||||
		Save,
 | 
			
		||||
		Trash2,
 | 
			
		||||
		Move,
 | 
			
		||||
		Map,
 | 
			
		||||
		Layers2
 | 
			
		||||
	} 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';
 | 
			
		||||
	import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
	import Sortable from 'sortablejs/Sortable';
 | 
			
		||||
	import { customBasemapUpdate } from './utils';
 | 
			
		||||
    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 { Separator } from '$lib/components/ui/separator';
 | 
			
		||||
    import * as RadioGroup from '$lib/components/ui/radio-group';
 | 
			
		||||
    import {
 | 
			
		||||
        CirclePlus,
 | 
			
		||||
        CircleX,
 | 
			
		||||
        Minus,
 | 
			
		||||
        Pencil,
 | 
			
		||||
        Plus,
 | 
			
		||||
        Save,
 | 
			
		||||
        Trash2,
 | 
			
		||||
        Move,
 | 
			
		||||
        Map,
 | 
			
		||||
        Layers2
 | 
			
		||||
    } from 'lucide-svelte';
 | 
			
		||||
    import { _ } from 'svelte-i18n';
 | 
			
		||||
    import { settings } from '$lib/db';
 | 
			
		||||
    import { defaultBasemap, type CustomLayer } from '$lib/assets/layers';
 | 
			
		||||
    import { map } from '$lib/stores';
 | 
			
		||||
    import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
    import Sortable from 'sortablejs/Sortable';
 | 
			
		||||
    import { customBasemapUpdate } from './utils';
 | 
			
		||||
 | 
			
		||||
	const {
 | 
			
		||||
		customLayers,
 | 
			
		||||
		selectedBasemapTree,
 | 
			
		||||
		selectedOverlayTree,
 | 
			
		||||
		currentBasemap,
 | 
			
		||||
		previousBasemap,
 | 
			
		||||
		currentOverlays,
 | 
			
		||||
		previousOverlays,
 | 
			
		||||
		customBasemapOrder,
 | 
			
		||||
		customOverlayOrder
 | 
			
		||||
	} = settings;
 | 
			
		||||
    const {
 | 
			
		||||
        customLayers,
 | 
			
		||||
        selectedBasemapTree,
 | 
			
		||||
        selectedOverlayTree,
 | 
			
		||||
        currentBasemap,
 | 
			
		||||
        previousBasemap,
 | 
			
		||||
        currentOverlays,
 | 
			
		||||
        previousOverlays,
 | 
			
		||||
        customBasemapOrder,
 | 
			
		||||
        customOverlayOrder
 | 
			
		||||
    } = settings;
 | 
			
		||||
 | 
			
		||||
	let name: string = '';
 | 
			
		||||
	let tileUrls: string[] = [''];
 | 
			
		||||
	let maxZoom: number = 20;
 | 
			
		||||
	let layerType: 'basemap' | 'overlay' = 'basemap';
 | 
			
		||||
	let resourceType: 'raster' | 'vector' = 'raster';
 | 
			
		||||
    let name: string = '';
 | 
			
		||||
    let tileUrls: string[] = [''];
 | 
			
		||||
    let maxZoom: number = 20;
 | 
			
		||||
    let layerType: 'basemap' | 'overlay' = 'basemap';
 | 
			
		||||
    let resourceType: 'raster' | 'vector' = 'raster';
 | 
			
		||||
 | 
			
		||||
	let basemapContainer: HTMLElement;
 | 
			
		||||
	let overlayContainer: HTMLElement;
 | 
			
		||||
    let basemapContainer: HTMLElement;
 | 
			
		||||
    let overlayContainer: HTMLElement;
 | 
			
		||||
 | 
			
		||||
	let basemapSortable: Sortable;
 | 
			
		||||
	let overlaySortable: Sortable;
 | 
			
		||||
    let basemapSortable: Sortable;
 | 
			
		||||
    let overlaySortable: Sortable;
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		if ($customBasemapOrder.length === 0) {
 | 
			
		||||
			$customBasemapOrder = Object.keys($customLayers).filter(
 | 
			
		||||
				(id) => $customLayers[id].layerType === 'basemap'
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
		if ($customOverlayOrder.length === 0) {
 | 
			
		||||
			$customOverlayOrder = Object.keys($customLayers).filter(
 | 
			
		||||
				(id) => $customLayers[id].layerType === 'overlay'
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
    onMount(() => {
 | 
			
		||||
        if ($customBasemapOrder.length === 0) {
 | 
			
		||||
            $customBasemapOrder = Object.keys($customLayers).filter(
 | 
			
		||||
                (id) => $customLayers[id].layerType === 'basemap'
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        if ($customOverlayOrder.length === 0) {
 | 
			
		||||
            $customOverlayOrder = Object.keys($customLayers).filter(
 | 
			
		||||
                (id) => $customLayers[id].layerType === 'overlay'
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		basemapSortable = Sortable.create(basemapContainer, {
 | 
			
		||||
			onSort: (e) => {
 | 
			
		||||
				$customBasemapOrder = basemapSortable.toArray();
 | 
			
		||||
				$selectedBasemapTree.basemaps['custom'] = $customBasemapOrder.reduce((acc, id) => {
 | 
			
		||||
					acc[id] = true;
 | 
			
		||||
					return acc;
 | 
			
		||||
				}, {});
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
		overlaySortable = Sortable.create(overlayContainer, {
 | 
			
		||||
			onSort: (e) => {
 | 
			
		||||
				$customOverlayOrder = overlaySortable.toArray();
 | 
			
		||||
				$selectedOverlayTree.overlays['custom'] = $customOverlayOrder.reduce((acc, id) => {
 | 
			
		||||
					acc[id] = true;
 | 
			
		||||
					return acc;
 | 
			
		||||
				}, {});
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
        basemapSortable = Sortable.create(basemapContainer, {
 | 
			
		||||
            onSort: (e) => {
 | 
			
		||||
                $customBasemapOrder = basemapSortable.toArray();
 | 
			
		||||
                $selectedBasemapTree.basemaps['custom'] = $customBasemapOrder.reduce((acc, id) => {
 | 
			
		||||
                    acc[id] = true;
 | 
			
		||||
                    return acc;
 | 
			
		||||
                }, {});
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        overlaySortable = Sortable.create(overlayContainer, {
 | 
			
		||||
            onSort: (e) => {
 | 
			
		||||
                $customOverlayOrder = overlaySortable.toArray();
 | 
			
		||||
                $selectedOverlayTree.overlays['custom'] = $customOverlayOrder.reduce((acc, id) => {
 | 
			
		||||
                    acc[id] = true;
 | 
			
		||||
                    return acc;
 | 
			
		||||
                }, {});
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
		basemapSortable.sort($customBasemapOrder);
 | 
			
		||||
		overlaySortable.sort($customOverlayOrder);
 | 
			
		||||
	});
 | 
			
		||||
        basemapSortable.sort($customBasemapOrder);
 | 
			
		||||
        overlaySortable.sort($customOverlayOrder);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
	onDestroy(() => {
 | 
			
		||||
		basemapSortable.destroy();
 | 
			
		||||
		overlaySortable.destroy();
 | 
			
		||||
	});
 | 
			
		||||
    onDestroy(() => {
 | 
			
		||||
        basemapSortable.destroy();
 | 
			
		||||
        overlaySortable.destroy();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
	$: if (tileUrls[0].length > 0) {
 | 
			
		||||
		if (
 | 
			
		||||
			tileUrls[0].includes('.json') ||
 | 
			
		||||
			(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
 | 
			
		||||
		) {
 | 
			
		||||
			resourceType = 'vector';
 | 
			
		||||
			layerType = 'basemap';
 | 
			
		||||
		} else {
 | 
			
		||||
			resourceType = 'raster';
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
    $: if (tileUrls[0].length > 0) {
 | 
			
		||||
        if (
 | 
			
		||||
            tileUrls[0].includes('.json') ||
 | 
			
		||||
            (tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
 | 
			
		||||
        ) {
 | 
			
		||||
            resourceType = 'vector';
 | 
			
		||||
        } else {
 | 
			
		||||
            resourceType = 'raster';
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	function createLayer() {
 | 
			
		||||
		if (selectedLayerId && $customLayers[selectedLayerId].layerType !== layerType) {
 | 
			
		||||
			deleteLayer(selectedLayerId);
 | 
			
		||||
		}
 | 
			
		||||
    function createLayer() {
 | 
			
		||||
        if (selectedLayerId && $customLayers[selectedLayerId].layerType !== layerType) {
 | 
			
		||||
            deleteLayer(selectedLayerId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		if (typeof maxZoom === 'string') {
 | 
			
		||||
			maxZoom = parseInt(maxZoom);
 | 
			
		||||
		}
 | 
			
		||||
        if (typeof maxZoom === 'string') {
 | 
			
		||||
            maxZoom = parseInt(maxZoom);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		let layerId = selectedLayerId ?? getLayerId();
 | 
			
		||||
		let layer: CustomLayer = {
 | 
			
		||||
			id: layerId,
 | 
			
		||||
			name: name,
 | 
			
		||||
			tileUrls: tileUrls,
 | 
			
		||||
			maxZoom: maxZoom,
 | 
			
		||||
			layerType: layerType,
 | 
			
		||||
			resourceType: resourceType,
 | 
			
		||||
			value: ''
 | 
			
		||||
		};
 | 
			
		||||
        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,
 | 
			
		||||
							tileSize: 256,
 | 
			
		||||
							maxzoom: maxZoom
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					layers: [
 | 
			
		||||
						{
 | 
			
		||||
							id: layerId,
 | 
			
		||||
							type: 'raster',
 | 
			
		||||
							source: layerId
 | 
			
		||||
						}
 | 
			
		||||
					]
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				layer.value = {
 | 
			
		||||
					type: 'raster',
 | 
			
		||||
					tiles: tileUrls,
 | 
			
		||||
					tileSize: 256,
 | 
			
		||||
					maxzoom: maxZoom
 | 
			
		||||
				};
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		$customLayers[layerId] = layer;
 | 
			
		||||
		addLayer(layerId);
 | 
			
		||||
		selectedLayerId = undefined;
 | 
			
		||||
		setDataFromSelectedLayer();
 | 
			
		||||
	}
 | 
			
		||||
        if (resourceType === 'vector') {
 | 
			
		||||
            layer.value = tileUrls[0];
 | 
			
		||||
        } else {
 | 
			
		||||
            layer.value = {
 | 
			
		||||
                version: 8,
 | 
			
		||||
                sources: {
 | 
			
		||||
                    [layerId]: {
 | 
			
		||||
                        type: 'raster',
 | 
			
		||||
                        tiles: tileUrls,
 | 
			
		||||
                        tileSize: 256,
 | 
			
		||||
                        maxzoom: maxZoom
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                layers: [
 | 
			
		||||
                    {
 | 
			
		||||
                        id: layerId,
 | 
			
		||||
                        type: 'raster',
 | 
			
		||||
                        source: layerId
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        $customLayers[layerId] = layer;
 | 
			
		||||
        addLayer(layerId);
 | 
			
		||||
        selectedLayerId = undefined;
 | 
			
		||||
        setDataFromSelectedLayer();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	function getLayerId() {
 | 
			
		||||
		for (let id = 0; ; id++) {
 | 
			
		||||
			if (!$customLayers.hasOwnProperty(`custom-${id}`)) {
 | 
			
		||||
				return `custom-${id}`;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
    function getLayerId() {
 | 
			
		||||
        for (let id = 0; ; id++) {
 | 
			
		||||
            if (!$customLayers.hasOwnProperty(`custom-${id}`)) {
 | 
			
		||||
                return `custom-${id}`;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	function addLayer(layerId: string) {
 | 
			
		||||
		if (layerType === 'basemap') {
 | 
			
		||||
			selectedBasemapTree.update(($tree) => {
 | 
			
		||||
				if (!$tree.basemaps.hasOwnProperty('custom')) {
 | 
			
		||||
					$tree.basemaps['custom'] = {};
 | 
			
		||||
				}
 | 
			
		||||
				$tree.basemaps['custom'][layerId] = true;
 | 
			
		||||
				return $tree;
 | 
			
		||||
			});
 | 
			
		||||
    function addLayer(layerId: string) {
 | 
			
		||||
        if (layerType === 'basemap') {
 | 
			
		||||
            selectedBasemapTree.update(($tree) => {
 | 
			
		||||
                if (!$tree.basemaps.hasOwnProperty('custom')) {
 | 
			
		||||
                    $tree.basemaps['custom'] = {};
 | 
			
		||||
                }
 | 
			
		||||
                $tree.basemaps['custom'][layerId] = true;
 | 
			
		||||
                return $tree;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
			if ($currentBasemap === layerId) {
 | 
			
		||||
				$customBasemapUpdate++;
 | 
			
		||||
			} else {
 | 
			
		||||
				$currentBasemap = layerId;
 | 
			
		||||
			}
 | 
			
		||||
            if ($currentBasemap === layerId) {
 | 
			
		||||
                $customBasemapUpdate++;
 | 
			
		||||
            } else {
 | 
			
		||||
                $currentBasemap = layerId;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
			if (!$customBasemapOrder.includes(layerId)) {
 | 
			
		||||
				$customBasemapOrder = [...$customBasemapOrder, layerId];
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			selectedOverlayTree.update(($tree) => {
 | 
			
		||||
				if (!$tree.overlays.hasOwnProperty('custom')) {
 | 
			
		||||
					$tree.overlays['custom'] = {};
 | 
			
		||||
				}
 | 
			
		||||
				$tree.overlays['custom'][layerId] = true;
 | 
			
		||||
				return $tree;
 | 
			
		||||
			});
 | 
			
		||||
            if (!$customBasemapOrder.includes(layerId)) {
 | 
			
		||||
                $customBasemapOrder = [...$customBasemapOrder, layerId];
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            selectedOverlayTree.update(($tree) => {
 | 
			
		||||
                if (!$tree.overlays.hasOwnProperty('custom')) {
 | 
			
		||||
                    $tree.overlays['custom'] = {};
 | 
			
		||||
                }
 | 
			
		||||
                $tree.overlays['custom'][layerId] = true;
 | 
			
		||||
                return $tree;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
			if ($map && $map.getSource(layerId)) {
 | 
			
		||||
				// Reset source when updating an existing layer
 | 
			
		||||
				if ($map.getLayer(layerId)) {
 | 
			
		||||
					$map.removeLayer(layerId);
 | 
			
		||||
				}
 | 
			
		||||
				$map.removeSource(layerId);
 | 
			
		||||
			}
 | 
			
		||||
            if ($map) {
 | 
			
		||||
                try {
 | 
			
		||||
                    $map.removeImport(layerId);
 | 
			
		||||
                } catch (e) {
 | 
			
		||||
                    // No reliable way to check if the map is ready to remove sources and layers
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
			if (!$currentOverlays.overlays.hasOwnProperty('custom')) {
 | 
			
		||||
				$currentOverlays.overlays['custom'] = {};
 | 
			
		||||
			}
 | 
			
		||||
			$currentOverlays.overlays['custom'][layerId] = true;
 | 
			
		||||
            if (!$currentOverlays.overlays.hasOwnProperty('custom')) {
 | 
			
		||||
                $currentOverlays.overlays['custom'] = {};
 | 
			
		||||
            }
 | 
			
		||||
            $currentOverlays.overlays['custom'][layerId] = true;
 | 
			
		||||
 | 
			
		||||
			if (!$customOverlayOrder.includes(layerId)) {
 | 
			
		||||
				$customOverlayOrder = [...$customOverlayOrder, layerId];
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
            if (!$customOverlayOrder.includes(layerId)) {
 | 
			
		||||
                $customOverlayOrder = [...$customOverlayOrder, layerId];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	function tryDeleteLayer(node: any, id: string): any {
 | 
			
		||||
		if (node.hasOwnProperty(id)) {
 | 
			
		||||
			delete node[id];
 | 
			
		||||
		}
 | 
			
		||||
		return node;
 | 
			
		||||
	}
 | 
			
		||||
    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;
 | 
			
		||||
			}
 | 
			
		||||
    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');
 | 
			
		||||
			}
 | 
			
		||||
			$customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId);
 | 
			
		||||
		} else {
 | 
			
		||||
			$currentOverlays.overlays['custom'][layerId] = false;
 | 
			
		||||
			if ($previousOverlays.overlays['custom']) {
 | 
			
		||||
				$previousOverlays.overlays['custom'] = tryDeleteLayer(
 | 
			
		||||
					$previousOverlays.overlays['custom'],
 | 
			
		||||
					layerId
 | 
			
		||||
				);
 | 
			
		||||
			}
 | 
			
		||||
            $selectedBasemapTree.basemaps['custom'] = tryDeleteLayer(
 | 
			
		||||
                $selectedBasemapTree.basemaps['custom'],
 | 
			
		||||
                layerId
 | 
			
		||||
            );
 | 
			
		||||
            if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) {
 | 
			
		||||
                $selectedBasemapTree.basemaps = tryDeleteLayer(
 | 
			
		||||
                    $selectedBasemapTree.basemaps,
 | 
			
		||||
                    'custom'
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            $customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId);
 | 
			
		||||
        } else {
 | 
			
		||||
            $currentOverlays.overlays['custom'][layerId] = false;
 | 
			
		||||
            if ($previousOverlays.overlays['custom']) {
 | 
			
		||||
                $previousOverlays.overlays['custom'] = tryDeleteLayer(
 | 
			
		||||
                    $previousOverlays.overlays['custom'],
 | 
			
		||||
                    layerId
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
			$selectedOverlayTree.overlays['custom'] = tryDeleteLayer(
 | 
			
		||||
				$selectedOverlayTree.overlays['custom'],
 | 
			
		||||
				layerId
 | 
			
		||||
			);
 | 
			
		||||
			if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) {
 | 
			
		||||
				$selectedOverlayTree.overlays = tryDeleteLayer($selectedOverlayTree.overlays, 'custom');
 | 
			
		||||
			}
 | 
			
		||||
			$customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId);
 | 
			
		||||
            $selectedOverlayTree.overlays['custom'] = tryDeleteLayer(
 | 
			
		||||
                $selectedOverlayTree.overlays['custom'],
 | 
			
		||||
                layerId
 | 
			
		||||
            );
 | 
			
		||||
            if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) {
 | 
			
		||||
                $selectedOverlayTree.overlays = tryDeleteLayer(
 | 
			
		||||
                    $selectedOverlayTree.overlays,
 | 
			
		||||
                    'custom'
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            $customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId);
 | 
			
		||||
 | 
			
		||||
			if ($map) {
 | 
			
		||||
				if ($map.getLayer(layerId)) {
 | 
			
		||||
					$map.removeLayer(layerId);
 | 
			
		||||
				}
 | 
			
		||||
				if ($map.getSource(layerId)) {
 | 
			
		||||
					$map.removeSource(layerId);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		$customLayers = tryDeleteLayer($customLayers, layerId);
 | 
			
		||||
	}
 | 
			
		||||
            if ($map) {
 | 
			
		||||
                try {
 | 
			
		||||
                    $map.removeImport(layerId);
 | 
			
		||||
                } catch (e) {
 | 
			
		||||
                    // No reliable way to check if the map is ready to remove sources and layers
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        $customLayers = tryDeleteLayer($customLayers, layerId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	let selectedLayerId: string | undefined = undefined;
 | 
			
		||||
    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';
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
    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();
 | 
			
		||||
    $: selectedLayerId, setDataFromSelectedLayer();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col">
 | 
			
		||||
	{#if $customBasemapOrder.length > 0}
 | 
			
		||||
		<div class="flex flex-row items-center gap-1 font-semibold mb-2">
 | 
			
		||||
			<Map size="16" />
 | 
			
		||||
			{$_('layers.label.basemaps')}
 | 
			
		||||
			<div class="grow">
 | 
			
		||||
				<Separator />
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	{/if}
 | 
			
		||||
	<div
 | 
			
		||||
		bind:this={basemapContainer}
 | 
			
		||||
		class="ml-1.5 flex flex-col gap-1 {$customBasemapOrder.length > 0 ? 'mb-2' : ''}"
 | 
			
		||||
	>
 | 
			
		||||
		{#each $customBasemapOrder as id (id)}
 | 
			
		||||
			<div class="flex flex-row items-center gap-2" data-id={id}>
 | 
			
		||||
				<Move size="12" />
 | 
			
		||||
				<span class="grow">{$customLayers[id].name}</span>
 | 
			
		||||
				<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
 | 
			
		||||
					<Pencil size="16" />
 | 
			
		||||
				</Button>
 | 
			
		||||
				<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
 | 
			
		||||
					<Trash2 size="16" />
 | 
			
		||||
				</Button>
 | 
			
		||||
			</div>
 | 
			
		||||
		{/each}
 | 
			
		||||
	</div>
 | 
			
		||||
	{#if $customOverlayOrder.length > 0}
 | 
			
		||||
		<div class="flex flex-row items-center gap-1 font-semibold mb-2">
 | 
			
		||||
			<Layers2 size="16" />
 | 
			
		||||
			{$_('layers.label.overlays')}
 | 
			
		||||
			<div class="grow">
 | 
			
		||||
				<Separator />
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	{/if}
 | 
			
		||||
	<div
 | 
			
		||||
		bind:this={overlayContainer}
 | 
			
		||||
		class="ml-1.5 flex flex-col gap-1 {$customOverlayOrder.length > 0 ? 'mb-2' : ''}"
 | 
			
		||||
	>
 | 
			
		||||
		{#each $customOverlayOrder as id (id)}
 | 
			
		||||
			<div class="flex flex-row items-center gap-2" data-id={id}>
 | 
			
		||||
				<Move size="12" />
 | 
			
		||||
				<span class="grow">{$customLayers[id].name}</span>
 | 
			
		||||
				<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
 | 
			
		||||
					<Pencil size="16" />
 | 
			
		||||
				</Button>
 | 
			
		||||
				<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
 | 
			
		||||
					<Trash2 size="16" />
 | 
			
		||||
				</Button>
 | 
			
		||||
			</div>
 | 
			
		||||
		{/each}
 | 
			
		||||
	</div>
 | 
			
		||||
    {#if $customBasemapOrder.length > 0}
 | 
			
		||||
        <div class="flex flex-row items-center gap-1 font-semibold mb-2">
 | 
			
		||||
            <Map size="16" />
 | 
			
		||||
            {$_('layers.label.basemaps')}
 | 
			
		||||
            <div class="grow">
 | 
			
		||||
                <Separator />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    {/if}
 | 
			
		||||
    <div
 | 
			
		||||
        bind:this={basemapContainer}
 | 
			
		||||
        class="ml-1.5 flex flex-col gap-1 {$customBasemapOrder.length > 0 ? 'mb-2' : ''}"
 | 
			
		||||
    >
 | 
			
		||||
        {#each $customBasemapOrder as id (id)}
 | 
			
		||||
            <div class="flex flex-row items-center gap-2" data-id={id}>
 | 
			
		||||
                <Move size="12" />
 | 
			
		||||
                <span class="grow">{$customLayers[id].name}</span>
 | 
			
		||||
                <Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
 | 
			
		||||
                    <Pencil size="16" />
 | 
			
		||||
                </Button>
 | 
			
		||||
                <Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
 | 
			
		||||
                    <Trash2 size="16" />
 | 
			
		||||
                </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
        {/each}
 | 
			
		||||
    </div>
 | 
			
		||||
    {#if $customOverlayOrder.length > 0}
 | 
			
		||||
        <div class="flex flex-row items-center gap-1 font-semibold mb-2">
 | 
			
		||||
            <Layers2 size="16" />
 | 
			
		||||
            {$_('layers.label.overlays')}
 | 
			
		||||
            <div class="grow">
 | 
			
		||||
                <Separator />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    {/if}
 | 
			
		||||
    <div
 | 
			
		||||
        bind:this={overlayContainer}
 | 
			
		||||
        class="ml-1.5 flex flex-col gap-1 {$customOverlayOrder.length > 0 ? 'mb-2' : ''}"
 | 
			
		||||
    >
 | 
			
		||||
        {#each $customOverlayOrder as id (id)}
 | 
			
		||||
            <div class="flex flex-row items-center gap-2" data-id={id}>
 | 
			
		||||
                <Move size="12" />
 | 
			
		||||
                <span class="grow">{$customLayers[id].name}</span>
 | 
			
		||||
                <Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
 | 
			
		||||
                    <Pencil size="16" />
 | 
			
		||||
                </Button>
 | 
			
		||||
                <Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
 | 
			
		||||
                    <Trash2 size="16" />
 | 
			
		||||
                </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
        {/each}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
	<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"
 | 
			
		||||
							placeholder={$_('layers.custom_layers.url_placeholder')}
 | 
			
		||||
						/>
 | 
			
		||||
						{#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>
 | 
			
		||||
    <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"
 | 
			
		||||
                            placeholder={$_('layers.custom_layers.url_placeholder')}
 | 
			
		||||
                        />
 | 
			
		||||
                        {#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" />
 | 
			
		||||
                        <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>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,203 +1,219 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
 | 
			
		||||
	import LayerTree from './LayerTree.svelte';
 | 
			
		||||
    import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
 | 
			
		||||
    import LayerTree from './LayerTree.svelte';
 | 
			
		||||
 | 
			
		||||
	import { Separator } from '$lib/components/ui/separator';
 | 
			
		||||
	import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
 | 
			
		||||
    import { Separator } from '$lib/components/ui/separator';
 | 
			
		||||
    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 { settings } from '$lib/db';
 | 
			
		||||
	import { map } from '$lib/stores';
 | 
			
		||||
	import { get, writable } from 'svelte/store';
 | 
			
		||||
	import { customBasemapUpdate, getLayers } from './utils';
 | 
			
		||||
	import { OverpassLayer } from './OverpassLayer';
 | 
			
		||||
	import OverpassPopup from './OverpassPopup.svelte';
 | 
			
		||||
    import { basemaps, defaultBasemap, overlays } from '$lib/assets/layers';
 | 
			
		||||
    import { settings } from '$lib/db';
 | 
			
		||||
    import { map } from '$lib/stores';
 | 
			
		||||
    import { get, writable } from 'svelte/store';
 | 
			
		||||
    import { customBasemapUpdate, getLayers } from './utils';
 | 
			
		||||
    import { OverpassLayer } from './OverpassLayer';
 | 
			
		||||
    import OverpassPopup from './OverpassPopup.svelte';
 | 
			
		||||
 | 
			
		||||
	let container: HTMLDivElement;
 | 
			
		||||
	let overpassLayer: OverpassLayer;
 | 
			
		||||
    let container: HTMLDivElement;
 | 
			
		||||
    let overpassLayer: OverpassLayer;
 | 
			
		||||
 | 
			
		||||
	const {
 | 
			
		||||
		currentBasemap,
 | 
			
		||||
		previousBasemap,
 | 
			
		||||
		currentOverlays,
 | 
			
		||||
		currentOverpassQueries,
 | 
			
		||||
		selectedBasemapTree,
 | 
			
		||||
		selectedOverlayTree,
 | 
			
		||||
		selectedOverpassTree,
 | 
			
		||||
		customLayers,
 | 
			
		||||
		opacities
 | 
			
		||||
	} = settings;
 | 
			
		||||
    const {
 | 
			
		||||
        currentBasemap,
 | 
			
		||||
        previousBasemap,
 | 
			
		||||
        currentOverlays,
 | 
			
		||||
        currentOverpassQueries,
 | 
			
		||||
        selectedBasemapTree,
 | 
			
		||||
        selectedOverlayTree,
 | 
			
		||||
        selectedOverpassTree,
 | 
			
		||||
        customLayers,
 | 
			
		||||
        opacities
 | 
			
		||||
    } = settings;
 | 
			
		||||
 | 
			
		||||
	function setStyle() {
 | 
			
		||||
		if ($map) {
 | 
			
		||||
			let basemap = basemaps.hasOwnProperty($currentBasemap)
 | 
			
		||||
				? basemaps[$currentBasemap]
 | 
			
		||||
				: $customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap];
 | 
			
		||||
			$map.setStyle(basemap, {
 | 
			
		||||
				diff: false
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
    function setStyle() {
 | 
			
		||||
        if ($map) {
 | 
			
		||||
            let basemap = basemaps.hasOwnProperty($currentBasemap)
 | 
			
		||||
                ? basemaps[$currentBasemap]
 | 
			
		||||
                : $customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap];
 | 
			
		||||
            $map.removeImport('basemap');
 | 
			
		||||
            if (typeof basemap === 'string') {
 | 
			
		||||
                $map.addImport({ id: 'basemap', url: basemap }, 'overlays');
 | 
			
		||||
            } else {
 | 
			
		||||
                $map.addImport(
 | 
			
		||||
                    {
 | 
			
		||||
                        id: 'basemap',
 | 
			
		||||
                        data: basemap
 | 
			
		||||
                    },
 | 
			
		||||
                    'overlays'
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	$: if ($map && ($currentBasemap || $customBasemapUpdate)) {
 | 
			
		||||
		setStyle();
 | 
			
		||||
	}
 | 
			
		||||
    $: if ($map && ($currentBasemap || $customBasemapUpdate)) {
 | 
			
		||||
        setStyle();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	$: if ($map && $currentOverlays) {
 | 
			
		||||
		// Add or remove overlay layers depending on the current overlays
 | 
			
		||||
		let overlayLayers = getLayers($currentOverlays);
 | 
			
		||||
		Object.keys(overlayLayers).forEach((id) => {
 | 
			
		||||
			if (overlayLayers[id]) {
 | 
			
		||||
				if (!addOverlayLayer.hasOwnProperty(id)) {
 | 
			
		||||
					addOverlayLayer[id] = addOverlayLayerForId(id);
 | 
			
		||||
				}
 | 
			
		||||
				if (!$map.getLayer(id)) {
 | 
			
		||||
					addOverlayLayer[id]();
 | 
			
		||||
					$map.on('style.load', addOverlayLayer[id]);
 | 
			
		||||
				}
 | 
			
		||||
			} else if ($map.getLayer(id)) {
 | 
			
		||||
				$map.removeLayer(id);
 | 
			
		||||
				$map.off('style.load', addOverlayLayer[id]);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
    function addOverlay(id: string) {
 | 
			
		||||
        try {
 | 
			
		||||
            let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
 | 
			
		||||
            if (typeof overlay === 'string') {
 | 
			
		||||
                $map.addImport({ id, url: overlay });
 | 
			
		||||
            } else {
 | 
			
		||||
                $map.addImport({
 | 
			
		||||
                    id,
 | 
			
		||||
                    data: overlay
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            // No reliable way to check if the map is ready to add sources and layers
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	$: if ($map) {
 | 
			
		||||
		if (overpassLayer) {
 | 
			
		||||
			overpassLayer.remove();
 | 
			
		||||
		}
 | 
			
		||||
		overpassLayer = new OverpassLayer($map);
 | 
			
		||||
		overpassLayer.add();
 | 
			
		||||
	}
 | 
			
		||||
    function updateOverlays() {
 | 
			
		||||
        if ($map && $currentOverlays) {
 | 
			
		||||
            let overlayLayers = getLayers($currentOverlays);
 | 
			
		||||
            try {
 | 
			
		||||
                let activeOverlays = $map
 | 
			
		||||
                    .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));
 | 
			
		||||
	selectedBasemap.subscribe((value) => {
 | 
			
		||||
		// 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);
 | 
			
		||||
	});
 | 
			
		||||
    $: if ($map && $currentOverlays) {
 | 
			
		||||
        updateOverlays();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	let addOverlayLayer: { [key: string]: () => void } = {};
 | 
			
		||||
	function addOverlayLayerForId(id: string) {
 | 
			
		||||
		return () => {
 | 
			
		||||
			if ($map) {
 | 
			
		||||
				try {
 | 
			
		||||
					let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
 | 
			
		||||
					if (!$map.getSource(id)) {
 | 
			
		||||
						$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
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
    $: if ($map) {
 | 
			
		||||
        if (overpassLayer) {
 | 
			
		||||
            overpassLayer.remove();
 | 
			
		||||
        }
 | 
			
		||||
        overpassLayer = new OverpassLayer($map);
 | 
			
		||||
        overpassLayer.add();
 | 
			
		||||
        $map.on('style.import.load', updateOverlays);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	let open = false;
 | 
			
		||||
	function openLayerControl() {
 | 
			
		||||
		open = true;
 | 
			
		||||
	}
 | 
			
		||||
	function closeLayerControl() {
 | 
			
		||||
		open = false;
 | 
			
		||||
	}
 | 
			
		||||
	let cancelEvents = false;
 | 
			
		||||
    let selectedBasemap = writable(get(currentBasemap));
 | 
			
		||||
    selectedBasemap.subscribe((value) => {
 | 
			
		||||
        // 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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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>
 | 
			
		||||
 | 
			
		||||
<CustomControl class="group min-w-[29px] min-h-[29px] overflow-hidden">
 | 
			
		||||
	<!-- svelte-ignore a11y-no-static-element-interactions -->
 | 
			
		||||
	<div
 | 
			
		||||
		bind:this={container}
 | 
			
		||||
		class="h-full w-full"
 | 
			
		||||
		on:mouseenter={openLayerControl}
 | 
			
		||||
		on:mouseleave={closeLayerControl}
 | 
			
		||||
		on:pointerenter={() => {
 | 
			
		||||
			if (!open) {
 | 
			
		||||
				cancelEvents = true;
 | 
			
		||||
				openLayerControl();
 | 
			
		||||
				setTimeout(() => {
 | 
			
		||||
					cancelEvents = false;
 | 
			
		||||
				}, 500);
 | 
			
		||||
			}
 | 
			
		||||
		}}
 | 
			
		||||
	>
 | 
			
		||||
		<div
 | 
			
		||||
			class="flex flex-row justify-center items-center delay-100 transition-[opacity] duration-0 {open
 | 
			
		||||
				? 'opacity-0 w-0 h-0 delay-0'
 | 
			
		||||
				: 'w-[29px] h-[29px]'}"
 | 
			
		||||
		>
 | 
			
		||||
			<Layers size="20" />
 | 
			
		||||
		</div>
 | 
			
		||||
		<div
 | 
			
		||||
			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]'
 | 
			
		||||
				: ''} {cancelEvents ? 'pointer-events-none' : ''}"
 | 
			
		||||
		>
 | 
			
		||||
			<ScrollArea>
 | 
			
		||||
				<div class="h-fit">
 | 
			
		||||
					<div class="p-2">
 | 
			
		||||
						<LayerTree
 | 
			
		||||
							layerTree={$selectedBasemapTree}
 | 
			
		||||
							name="basemaps"
 | 
			
		||||
							bind:selected={$selectedBasemap}
 | 
			
		||||
						/>
 | 
			
		||||
					</div>
 | 
			
		||||
					<Separator class="w-full" />
 | 
			
		||||
					<div class="p-2">
 | 
			
		||||
						{#if $currentOverlays}
 | 
			
		||||
							<LayerTree
 | 
			
		||||
								layerTree={$selectedOverlayTree}
 | 
			
		||||
								name="overlays"
 | 
			
		||||
								multiple={true}
 | 
			
		||||
								bind:checked={$currentOverlays}
 | 
			
		||||
							/>
 | 
			
		||||
						{/if}
 | 
			
		||||
					</div>
 | 
			
		||||
					<Separator class="w-full" />
 | 
			
		||||
					<div class="p-2">
 | 
			
		||||
						{#if $currentOverpassQueries}
 | 
			
		||||
							<LayerTree
 | 
			
		||||
								layerTree={$selectedOverpassTree}
 | 
			
		||||
								name="overpass"
 | 
			
		||||
								multiple={true}
 | 
			
		||||
								bind:checked={$currentOverpassQueries}
 | 
			
		||||
							/>
 | 
			
		||||
						{/if}
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</ScrollArea>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
    <!-- svelte-ignore a11y-no-static-element-interactions -->
 | 
			
		||||
    <div
 | 
			
		||||
        bind:this={container}
 | 
			
		||||
        class="h-full w-full"
 | 
			
		||||
        on:mouseenter={openLayerControl}
 | 
			
		||||
        on:mouseleave={closeLayerControl}
 | 
			
		||||
        on:pointerenter={() => {
 | 
			
		||||
            if (!open) {
 | 
			
		||||
                cancelEvents = true;
 | 
			
		||||
                openLayerControl();
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    cancelEvents = false;
 | 
			
		||||
                }, 500);
 | 
			
		||||
            }
 | 
			
		||||
        }}
 | 
			
		||||
    >
 | 
			
		||||
        <div
 | 
			
		||||
            class="flex flex-row justify-center items-center delay-100 transition-[opacity] duration-0 {open
 | 
			
		||||
                ? 'opacity-0 w-0 h-0 delay-0'
 | 
			
		||||
                : 'w-[29px] h-[29px]'}"
 | 
			
		||||
        >
 | 
			
		||||
            <Layers size="20" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div
 | 
			
		||||
            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]'
 | 
			
		||||
                : ''} {cancelEvents ? 'pointer-events-none' : ''}"
 | 
			
		||||
        >
 | 
			
		||||
            <ScrollArea>
 | 
			
		||||
                <div class="h-fit">
 | 
			
		||||
                    <div class="p-2">
 | 
			
		||||
                        <LayerTree
 | 
			
		||||
                            layerTree={$selectedBasemapTree}
 | 
			
		||||
                            name="basemaps"
 | 
			
		||||
                            bind:selected={$selectedBasemap}
 | 
			
		||||
                        />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <Separator class="w-full" />
 | 
			
		||||
                    <div class="p-2">
 | 
			
		||||
                        {#if $currentOverlays}
 | 
			
		||||
                            <LayerTree
 | 
			
		||||
                                layerTree={$selectedOverlayTree}
 | 
			
		||||
                                name="overlays"
 | 
			
		||||
                                multiple={true}
 | 
			
		||||
                                bind:checked={$currentOverlays}
 | 
			
		||||
                            />
 | 
			
		||||
                        {/if}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <Separator class="w-full" />
 | 
			
		||||
                    <div class="p-2">
 | 
			
		||||
                        {#if $currentOverpassQueries}
 | 
			
		||||
                            <LayerTree
 | 
			
		||||
                                layerTree={$selectedOverpassTree}
 | 
			
		||||
                                name="overpass"
 | 
			
		||||
                                multiple={true}
 | 
			
		||||
                                bind:checked={$currentOverpassQueries}
 | 
			
		||||
                            />
 | 
			
		||||
                        {/if}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ScrollArea>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</CustomControl>
 | 
			
		||||
 | 
			
		||||
<OverpassPopup />
 | 
			
		||||
 | 
			
		||||
<svelte:window
 | 
			
		||||
	on:click={(e) => {
 | 
			
		||||
		if (open && !cancelEvents && !container.contains(e.target)) {
 | 
			
		||||
			closeLayerControl();
 | 
			
		||||
		}
 | 
			
		||||
	}}
 | 
			
		||||
    on:click={(e) => {
 | 
			
		||||
        if (open && !cancelEvents && !container.contains(e.target)) {
 | 
			
		||||
            closeLayerControl();
 | 
			
		||||
        }
 | 
			
		||||
    }}
 | 
			
		||||
/>
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@ export class OverpassLayer {
 | 
			
		||||
 | 
			
		||||
    add() {
 | 
			
		||||
        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(currentOverpassQueries.subscribe(() => {
 | 
			
		||||
            this.updateBinded();
 | 
			
		||||
@@ -108,15 +108,19 @@ export class OverpassLayer {
 | 
			
		||||
 | 
			
		||||
    remove() {
 | 
			
		||||
        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());
 | 
			
		||||
 | 
			
		||||
        if (this.map.getLayer('overpass')) {
 | 
			
		||||
            this.map.removeLayer('overpass');
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
            if (this.map.getLayer('overpass')) {
 | 
			
		||||
                this.map.removeLayer('overpass');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        if (this.map.getSource('overpass')) {
 | 
			
		||||
            this.map.removeSource('overpass');
 | 
			
		||||
            if (this.map.getSource('overpass')) {
 | 
			
		||||
                this.map.removeSource('overpass');
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            // No reliable way to check if the map is ready to remove sources and layers
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -272,6 +272,7 @@
 | 
			
		||||
            "finlandTopo": "Lantmäteriverket Terrängkarta",
 | 
			
		||||
            "bgMountains": "BGMountains",
 | 
			
		||||
            "usgs": "USGS",
 | 
			
		||||
            "bikerouterGravel": "bikerouter.de Gravel",
 | 
			
		||||
            "cyclOSMlite": "CyclOSM Lite",
 | 
			
		||||
            "swisstopoSlope": "swisstopo Slope",
 | 
			
		||||
            "swisstopoHiking": "swisstopo Hiking",
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user