mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-31 23:53:25 +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 { TramFront, Utensils, ShoppingBasket, Droplet, ShowerHead, Fuel, CircleParking, Fence, FerrisWheel, Bed, Mountain, Pickaxe, Store, TrainFront, Bus, Ship, Croissant, House, Tent, Wrench, Binoculars } from 'lucide-static';
|
||||||
import { type AnySourceData, type Style } from 'mapbox-gl';
|
import { type Style } from 'mapbox-gl';
|
||||||
import ignFrTopo from './custom/ign-fr-topo.json';
|
import ignFrTopo from './custom/ign-fr-topo.json';
|
||||||
import ignFrPlan from './custom/ign-fr-plan.json';
|
import ignFrPlan from './custom/ign-fr-plan.json';
|
||||||
import ignFrSatellite from './custom/ign-fr-satellite.json';
|
import ignFrSatellite from './custom/ign-fr-satellite.json';
|
||||||
|
import bikerouterGravel from './custom/bikerouter-gravel.json';
|
||||||
|
|
||||||
export const basemaps: { [key: string]: string | Style; } = {
|
export const basemaps: { [key: string]: string | Style; } = {
|
||||||
mapboxOutdoors: 'mapbox://styles/mapbox/outdoors-v12',
|
mapboxOutdoors: 'mapbox://styles/mapbox/outdoors-v12',
|
||||||
@@ -255,144 +255,309 @@ export const basemaps: { [key: string]: string | Style; } = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function extendBasemap(basemap: string | Style): string | Style {
|
export const overlays: { [key: string]: string | Style; } = {
|
||||||
if (typeof basemap === 'object') {
|
|
||||||
basemap["glyphs"] = "mapbox://fonts/mapbox/{fontstack}/{range}.pbf";
|
|
||||||
basemap["sprite"] = `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}`;
|
|
||||||
}
|
|
||||||
return basemap;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.values(basemaps).forEach(extendBasemap);
|
|
||||||
|
|
||||||
export const font: { [key: string]: string; } = {
|
|
||||||
swisstopoVector: 'Frutiger Neue Condensed Regular',
|
|
||||||
swisstopoSatellite: 'Frutiger Neue Condensed Regular',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const overlays: { [key: string]: AnySourceData; } = {
|
|
||||||
cyclOSMlite: {
|
cyclOSMlite: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://a.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png', 'https://b.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png', 'https://c.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
cyclOSMlite: {
|
||||||
maxzoom: 17,
|
type: 'raster',
|
||||||
attribution: '© <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>'
|
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: {
|
swisstopoSlope: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.hangneigung-ueber_30/default/current/3857/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoSlope: {
|
||||||
maxzoom: 17,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>',
|
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.hangneigung-ueber_30/default/current/3857/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 17,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoSlope',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoSlope',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
swisstopoHiking: {
|
swisstopoHiking: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swisstlm3d-wanderwege/default/current/3857/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoHiking: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swisstlm3d-wanderwege/default/current/3857/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoHiking',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoHiking',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
swisstopoHikingClosures: {
|
swisstopoHikingClosures: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.wanderland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoHikingClosures: {
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
type: 'raster',
|
||||||
|
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.wanderland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoHikingClosures',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoHikingClosures',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
swisstopoCycling: {
|
swisstopoCycling: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.veloland/default/current/3857/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoCycling: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.veloland/default/current/3857/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoCycling',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoCycling',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
swisstopoCyclingClosures: {
|
swisstopoCyclingClosures: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.veloland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoCyclingClosures: {
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
type: 'raster',
|
||||||
|
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.veloland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoCyclingClosures',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoCyclingClosures',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
swisstopoMountainBike: {
|
swisstopoMountainBike: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.mountainbikeland/default/current/3857/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoMountainBike: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.mountainbikeland/default/current/3857/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoMountainBike',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoMountainBike',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
swisstopoMountainBikeClosures: {
|
swisstopoMountainBikeClosures: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.mountainbikeland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoMountainBikeClosures: {
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
type: 'raster',
|
||||||
|
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.mountainbikeland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoMountainBikeClosures',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoMountainBikeClosures',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
swisstopoSkiTouring: {
|
swisstopoSkiTouring: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo-karto.skitouren/default/current/3857/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoSkiTouring: {
|
||||||
maxzoom: 17,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo-karto.skitouren/default/current/3857/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 17,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoSkiTouring',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoSkiTouring',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
ignFrCadastre: {
|
ignFrCadastre: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TILEMATRIXSET=PM&TILEMATRIX={z}&TILECOL={x}&TILEROW={y}&LAYER=CADASTRALPARCELS.PARCELS&FORMAT=image/png&STYLE=normal'],
|
sources: {
|
||||||
tileSize: 256,
|
ignFrCadastre: {
|
||||||
maxzoom: 20,
|
type: 'raster',
|
||||||
attribution: 'IGN-F/Géoportail'
|
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TILEMATRIXSET=PM&TILEMATRIX={z}&TILECOL={x}&TILEROW={y}&LAYER=CADASTRALPARCELS.PARCELS&FORMAT=image/png&STYLE=normal'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 20,
|
||||||
|
attribution: 'IGN-F/Géoportail'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'ignFrCadastre',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'ignFrCadastre',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
ignSlope: {
|
ignSlope: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TileMatrixSet=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&Layer=GEOGRAPHICALGRIDSYSTEMS.SLOPES.MOUNTAIN&FORMAT=image/png&Style=normal'],
|
sources: {
|
||||||
tileSize: 256,
|
ignSlope: {
|
||||||
maxzoom: 17,
|
type: 'raster',
|
||||||
attribution: 'IGN-F/Géoportail'
|
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TileMatrixSet=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&Layer=GEOGRAPHICALGRIDSYSTEMS.SLOPES.MOUNTAIN&FORMAT=image/png&Style=normal'],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: 'IGN-F/Géoportail'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'ignSlope',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'ignSlope',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
ignSkiTouring: {
|
ignSkiTouring: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TileMatrixSet=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&Layer=TRACES.RANDO.HIVERNALE&FORMAT=image/png&Style=normal'],
|
sources: {
|
||||||
tileSize: 256,
|
ignSkiTouring: {
|
||||||
maxzoom: 16,
|
type: 'raster',
|
||||||
attribution: 'IGN-F/Géoportail'
|
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TileMatrixSet=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&Layer=TRACES.RANDO.HIVERNALE&FORMAT=image/png&Style=normal'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 16,
|
||||||
|
attribution: 'IGN-F/Géoportail'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'ignSkiTouring',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'ignSkiTouring',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
waymarkedTrailsHiking: {
|
waymarkedTrailsHiking: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
waymarkedTrailsHiking: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
tiles: ['https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'waymarkedTrailsHiking',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsHiking',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
waymarkedTrailsCycling: {
|
waymarkedTrailsCycling: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
waymarkedTrailsCycling: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
tiles: ['https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'waymarkedTrailsCycling',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsCycling',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
waymarkedTrailsMTB: {
|
waymarkedTrailsMTB: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://tile.waymarkedtrails.org/mtb/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
waymarkedTrailsMTB: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
tiles: ['https://tile.waymarkedtrails.org/mtb/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'waymarkedTrailsMTB',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsMTB',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
waymarkedTrailsSkating: {
|
waymarkedTrailsSkating: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://tile.waymarkedtrails.org/skating/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
waymarkedTrailsSkating: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
tiles: ['https://tile.waymarkedtrails.org/skating/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'waymarkedTrailsSkating',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsSkating',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
waymarkedTrailsHorseRiding: {
|
waymarkedTrailsHorseRiding: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://tile.waymarkedtrails.org/riding/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
waymarkedTrailsHorseRiding: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
tiles: ['https://tile.waymarkedtrails.org/riding/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'waymarkedTrailsHorseRiding',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsHorseRiding',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
waymarkedTrailsWinter: {
|
waymarkedTrailsWinter: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://tile.waymarkedtrails.org/slopes/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
waymarkedTrailsWinter: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
tiles: ['https://tile.waymarkedtrails.org/slopes/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'waymarkedTrailsWinter',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsWinter',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -463,9 +628,6 @@ export const basemapTree: LayerTreeType = {
|
|||||||
export const overlayTree: LayerTreeType = {
|
export const overlayTree: LayerTreeType = {
|
||||||
overlays: {
|
overlays: {
|
||||||
world: {
|
world: {
|
||||||
cyclOSM: {
|
|
||||||
cyclOSMlite: true,
|
|
||||||
},
|
|
||||||
waymarked_trails: {
|
waymarked_trails: {
|
||||||
waymarkedTrailsHiking: true,
|
waymarkedTrailsHiking: true,
|
||||||
waymarkedTrailsCycling: true,
|
waymarkedTrailsCycling: true,
|
||||||
@@ -473,7 +635,9 @@ export const overlayTree: LayerTreeType = {
|
|||||||
waymarkedTrailsSkating: true,
|
waymarkedTrailsSkating: true,
|
||||||
waymarkedTrailsHorseRiding: true,
|
waymarkedTrailsHorseRiding: true,
|
||||||
waymarkedTrailsWinter: true,
|
waymarkedTrailsWinter: true,
|
||||||
}
|
},
|
||||||
|
cyclOSMlite: true,
|
||||||
|
bikerouterGravel: true,
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
@@ -547,9 +711,6 @@ export const defaultBasemap = 'mapboxOutdoors';
|
|||||||
export const defaultOverlays = {
|
export const defaultOverlays = {
|
||||||
overlays: {
|
overlays: {
|
||||||
world: {
|
world: {
|
||||||
cyclOSM: {
|
|
||||||
cyclOSMlite: false,
|
|
||||||
},
|
|
||||||
waymarked_trails: {
|
waymarked_trails: {
|
||||||
waymarkedTrailsHiking: false,
|
waymarkedTrailsHiking: false,
|
||||||
waymarkedTrailsCycling: false,
|
waymarkedTrailsCycling: false,
|
||||||
@@ -557,7 +718,9 @@ export const defaultOverlays = {
|
|||||||
waymarkedTrailsSkating: false,
|
waymarkedTrailsSkating: false,
|
||||||
waymarkedTrailsHorseRiding: false,
|
waymarkedTrailsHorseRiding: false,
|
||||||
waymarkedTrailsWinter: false,
|
waymarkedTrailsWinter: false,
|
||||||
}
|
},
|
||||||
|
cyclOSMlite: false,
|
||||||
|
bikerouterGravel: false,
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
@@ -683,9 +846,6 @@ export const defaultBasemapTree: LayerTreeType = {
|
|||||||
export const defaultOverlayTree: LayerTreeType = {
|
export const defaultOverlayTree: LayerTreeType = {
|
||||||
overlays: {
|
overlays: {
|
||||||
world: {
|
world: {
|
||||||
cyclOSM: {
|
|
||||||
cyclOSMlite: false,
|
|
||||||
},
|
|
||||||
waymarked_trails: {
|
waymarked_trails: {
|
||||||
waymarkedTrailsHiking: true,
|
waymarkedTrailsHiking: true,
|
||||||
waymarkedTrailsCycling: true,
|
waymarkedTrailsCycling: true,
|
||||||
@@ -693,7 +853,9 @@ export const defaultOverlayTree: LayerTreeType = {
|
|||||||
waymarkedTrailsSkating: false,
|
waymarkedTrailsSkating: false,
|
||||||
waymarkedTrailsHorseRiding: false,
|
waymarkedTrailsHorseRiding: false,
|
||||||
waymarkedTrailsWinter: false,
|
waymarkedTrailsWinter: false,
|
||||||
}
|
},
|
||||||
|
cyclOSMlite: false,
|
||||||
|
bikerouterGravel: false,
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
|
@@ -1,331 +1,355 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
|
||||||
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
|
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
|
||||||
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
|
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
|
||||||
|
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
export let accessToken = PUBLIC_MAPBOX_TOKEN;
|
export let accessToken = PUBLIC_MAPBOX_TOKEN;
|
||||||
export let geolocate = true;
|
export let geolocate = true;
|
||||||
export let geocoder = true;
|
export let geocoder = true;
|
||||||
export let hash = true;
|
export let hash = true;
|
||||||
|
|
||||||
mapboxgl.accessToken = accessToken;
|
mapboxgl.accessToken = accessToken;
|
||||||
|
|
||||||
let webgl2Supported = true;
|
let webgl2Supported = true;
|
||||||
let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
|
let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
|
||||||
maxZoom: 15,
|
maxZoom: 15,
|
||||||
linear: true,
|
linear: true,
|
||||||
easing: () => 1
|
easing: () => 1
|
||||||
};
|
};
|
||||||
|
|
||||||
const { distanceUnits, elevationProfile, verticalFileView, bottomPanelSize, rightPanelSize } =
|
const { distanceUnits, elevationProfile, verticalFileView, bottomPanelSize, rightPanelSize } =
|
||||||
settings;
|
settings;
|
||||||
let scaleControl = new mapboxgl.ScaleControl({
|
let scaleControl = new mapboxgl.ScaleControl({
|
||||||
unit: $distanceUnits
|
unit: $distanceUnits
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let gl = document.createElement('canvas').getContext('webgl2');
|
let gl = document.createElement('canvas').getContext('webgl2');
|
||||||
if (!gl) {
|
if (!gl) {
|
||||||
webgl2Supported = false;
|
webgl2Supported = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let language = $page.params.language;
|
let language = $page.params.language;
|
||||||
if (language === 'zh') {
|
if (language === 'zh') {
|
||||||
language = 'zh-Hans';
|
language = 'zh-Hans';
|
||||||
} else if (language?.includes('-')) {
|
} else if (language?.includes('-')) {
|
||||||
language = language.split('-')[0];
|
language = language.split('-')[0];
|
||||||
} else if (language === '' || language === undefined) {
|
} else if (language === '' || language === undefined) {
|
||||||
language = 'en';
|
language = 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
let newMap = new mapboxgl.Map({
|
let newMap = new mapboxgl.Map({
|
||||||
container: 'map',
|
container: 'map',
|
||||||
style: { version: 8, sources: {}, layers: [] },
|
style: {
|
||||||
zoom: 0,
|
version: 8,
|
||||||
hash: hash,
|
sources: {},
|
||||||
language,
|
layers: [],
|
||||||
attributionControl: false,
|
imports: [
|
||||||
logoPosition: 'bottom-right',
|
{
|
||||||
boxZoom: false
|
id: 'glyphs-and-sprite', // make Mapbox glyphs and sprite available to other styles
|
||||||
});
|
url: '',
|
||||||
newMap.on('load', () => {
|
data: {
|
||||||
$map = newMap; // only set the store after the map has loaded
|
version: 8,
|
||||||
scaleControl.setUnit($distanceUnits);
|
sources: {},
|
||||||
});
|
layers: [],
|
||||||
|
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
|
||||||
|
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'basemap',
|
||||||
|
url: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'overlays',
|
||||||
|
url: '',
|
||||||
|
data: {
|
||||||
|
version: 8,
|
||||||
|
sources: {},
|
||||||
|
layers: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
zoom: 0,
|
||||||
|
hash: hash,
|
||||||
|
language,
|
||||||
|
attributionControl: false,
|
||||||
|
logoPosition: 'bottom-right',
|
||||||
|
boxZoom: false
|
||||||
|
});
|
||||||
|
newMap.on('load', () => {
|
||||||
|
$map = newMap; // only set the store after the map has loaded
|
||||||
|
scaleControl.setUnit($distanceUnits);
|
||||||
|
});
|
||||||
|
|
||||||
newMap.addControl(
|
newMap.addControl(
|
||||||
new mapboxgl.AttributionControl({
|
new mapboxgl.AttributionControl({
|
||||||
compact: true
|
compact: true
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
newMap.addControl(
|
newMap.addControl(
|
||||||
new mapboxgl.NavigationControl({
|
new mapboxgl.NavigationControl({
|
||||||
visualizePitch: true
|
visualizePitch: true
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (geocoder) {
|
if (geocoder) {
|
||||||
newMap.addControl(
|
newMap.addControl(
|
||||||
new MapboxGeocoder({
|
new MapboxGeocoder({
|
||||||
accessToken: mapboxgl.accessToken,
|
accessToken: mapboxgl.accessToken,
|
||||||
mapboxgl: mapboxgl,
|
mapboxgl: mapboxgl,
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
flyTo: fitBoundsOptions,
|
flyTo: fitBoundsOptions,
|
||||||
language
|
language
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (geolocate) {
|
if (geolocate) {
|
||||||
newMap.addControl(
|
newMap.addControl(
|
||||||
new mapboxgl.GeolocateControl({
|
new mapboxgl.GeolocateControl({
|
||||||
positionOptions: {
|
positionOptions: {
|
||||||
enableHighAccuracy: true
|
enableHighAccuracy: true
|
||||||
},
|
},
|
||||||
fitBoundsOptions,
|
fitBoundsOptions,
|
||||||
trackUserLocation: true,
|
trackUserLocation: true,
|
||||||
showUserHeading: true
|
showUserHeading: true
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
newMap.addControl(scaleControl);
|
newMap.addControl(scaleControl);
|
||||||
|
|
||||||
newMap.on('style.load', () => {
|
newMap.on('style.load', () => {
|
||||||
newMap.addSource('mapbox-dem', {
|
newMap.addSource('mapbox-dem', {
|
||||||
type: 'raster-dem',
|
type: 'raster-dem',
|
||||||
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
||||||
tileSize: 512,
|
tileSize: 512,
|
||||||
maxzoom: 14
|
maxzoom: 14
|
||||||
});
|
});
|
||||||
newMap.setTerrain({
|
newMap.setTerrain({
|
||||||
source: 'mapbox-dem',
|
source: 'mapbox-dem',
|
||||||
exaggeration: newMap.getPitch() > 0 ? 1 : 0
|
exaggeration: newMap.getPitch() > 0 ? 1 : 0
|
||||||
});
|
});
|
||||||
newMap.setFog({
|
newMap.setFog({
|
||||||
color: 'rgb(186, 210, 235)',
|
color: 'rgb(186, 210, 235)',
|
||||||
'high-color': 'rgb(36, 92, 223)',
|
'high-color': 'rgb(36, 92, 223)',
|
||||||
'horizon-blend': 0.1,
|
'horizon-blend': 0.1,
|
||||||
'space-color': 'rgb(156, 240, 255)'
|
'space-color': 'rgb(156, 240, 255)'
|
||||||
});
|
});
|
||||||
newMap.on('pitch', () => {
|
newMap.on('pitch', () => {
|
||||||
if (newMap.getPitch() > 0) {
|
if (newMap.getPitch() > 0) {
|
||||||
newMap.setTerrain({
|
newMap.setTerrain({
|
||||||
source: 'mapbox-dem',
|
source: 'mapbox-dem',
|
||||||
exaggeration: 1
|
exaggeration: 1
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
newMap.setTerrain({
|
newMap.setTerrain({
|
||||||
source: 'mapbox-dem',
|
source: 'mapbox-dem',
|
||||||
exaggeration: 0
|
exaggeration: 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// add dummy layer to place the overlay layers below
|
});
|
||||||
newMap.addLayer({
|
});
|
||||||
id: 'overlays',
|
|
||||||
type: 'background',
|
|
||||||
paint: {
|
|
||||||
'background-color': 'rgba(0, 0, 0, 0)'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
$map.remove();
|
$map.remove();
|
||||||
$map = null;
|
$map = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if (
|
$: if (
|
||||||
$map &&
|
$map &&
|
||||||
(!$verticalFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)
|
(!$verticalFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)
|
||||||
) {
|
) {
|
||||||
$map.resize();
|
$map.resize();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div {...$$restProps}>
|
<div {...$$restProps}>
|
||||||
<div id="map" class="h-full {webgl2Supported ? '' : 'hidden'}"></div>
|
<div id="map" class="h-full {webgl2Supported ? '' : 'hidden'}"></div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported ? 'hidden' : ''}"
|
class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported
|
||||||
>
|
? 'hidden'
|
||||||
<p>{$_('webgl2_required')}</p>
|
: ''}"
|
||||||
<Button href="https://get.webgl.org/webgl2/" target="_blank">
|
>
|
||||||
{$_('enable_webgl2')}
|
<p>{$_('webgl2_required')}</p>
|
||||||
</Button>
|
<Button href="https://get.webgl.org/webgl2/" target="_blank">
|
||||||
</div>
|
{$_('enable_webgl2')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
div :global(.mapboxgl-map) {
|
div :global(.mapboxgl-map) {
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) {
|
div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) {
|
||||||
@apply shadow-md;
|
@apply shadow-md;
|
||||||
@apply bg-background;
|
@apply bg-background;
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-icon) {
|
div :global(.mapboxgl-ctrl-icon) {
|
||||||
@apply dark:brightness-[4.7];
|
@apply dark:brightness-[4.7];
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder) {
|
div :global(.mapboxgl-ctrl-geocoder) {
|
||||||
@apply flex;
|
@apply flex;
|
||||||
@apply flex-row;
|
@apply flex-row;
|
||||||
@apply w-fit;
|
@apply w-fit;
|
||||||
@apply min-w-fit;
|
@apply min-w-fit;
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
@apply shadow-md;
|
@apply shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.suggestions) {
|
div :global(.suggestions) {
|
||||||
@apply shadow-md;
|
@apply shadow-md;
|
||||||
@apply bg-background;
|
@apply bg-background;
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) {
|
div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) {
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
@apply hover:text-accent-foreground;
|
@apply hover:text-accent-foreground;
|
||||||
@apply hover:bg-accent;
|
@apply hover:bg-accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) {
|
div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) {
|
||||||
@apply bg-background;
|
@apply bg-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder--button) {
|
div :global(.mapboxgl-ctrl-geocoder--button) {
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
@apply hover:bg-transparent;
|
@apply hover:bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder--icon) {
|
div :global(.mapboxgl-ctrl-geocoder--icon) {
|
||||||
@apply fill-foreground;
|
@apply fill-foreground;
|
||||||
@apply hover:fill-accent-foreground;
|
@apply hover:fill-accent-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder--icon-search) {
|
div :global(.mapboxgl-ctrl-geocoder--icon-search) {
|
||||||
@apply relative;
|
@apply relative;
|
||||||
@apply top-0;
|
@apply top-0;
|
||||||
@apply left-0;
|
@apply left-0;
|
||||||
@apply my-2;
|
@apply my-2;
|
||||||
@apply w-[29px];
|
@apply w-[29px];
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder--input) {
|
div :global(.mapboxgl-ctrl-geocoder--input) {
|
||||||
@apply relative;
|
@apply relative;
|
||||||
@apply w-64;
|
@apply w-64;
|
||||||
@apply py-0;
|
@apply py-0;
|
||||||
@apply pl-2;
|
@apply pl-2;
|
||||||
@apply focus:outline-none;
|
@apply focus:outline-none;
|
||||||
@apply transition-[width];
|
@apply transition-[width];
|
||||||
@apply duration-200;
|
@apply duration-200;
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) {
|
div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) {
|
||||||
@apply w-0;
|
@apply w-0;
|
||||||
@apply p-0;
|
@apply p-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-top-right) {
|
div :global(.mapboxgl-ctrl-top-right) {
|
||||||
@apply z-40;
|
@apply z-40;
|
||||||
@apply flex;
|
@apply flex;
|
||||||
@apply flex-col;
|
@apply flex-col;
|
||||||
@apply items-end;
|
@apply items-end;
|
||||||
@apply h-full;
|
@apply h-full;
|
||||||
@apply overflow-hidden;
|
@apply overflow-hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal :global(.mapboxgl-ctrl-bottom-left) {
|
.horizontal :global(.mapboxgl-ctrl-bottom-left) {
|
||||||
@apply bottom-[42px];
|
@apply bottom-[42px];
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal :global(.mapboxgl-ctrl-bottom-right) {
|
.horizontal :global(.mapboxgl-ctrl-bottom-right) {
|
||||||
@apply bottom-[42px];
|
@apply bottom-[42px];
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-attrib) {
|
div :global(.mapboxgl-ctrl-attrib) {
|
||||||
@apply dark:bg-transparent;
|
@apply dark:bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) {
|
div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) {
|
||||||
@apply dark:bg-background;
|
@apply dark:bg-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-attrib-button) {
|
div :global(.mapboxgl-ctrl-attrib-button) {
|
||||||
@apply dark:bg-foreground;
|
@apply dark:bg-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) {
|
div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) {
|
||||||
@apply dark:bg-foreground;
|
@apply dark:bg-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-attrib a) {
|
div :global(.mapboxgl-ctrl-attrib a) {
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup) {
|
div :global(.mapboxgl-popup) {
|
||||||
@apply w-fit;
|
@apply w-fit;
|
||||||
@apply z-20;
|
@apply z-20;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-content) {
|
div :global(.mapboxgl-popup-content) {
|
||||||
@apply p-0;
|
@apply p-0;
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
@apply shadow-none;
|
@apply shadow-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) {
|
||||||
@apply border-b-background;
|
@apply border-b-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) {
|
||||||
@apply border-b-background;
|
@apply border-b-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) {
|
||||||
@apply border-b-background;
|
@apply border-b-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) {
|
||||||
@apply border-t-background;
|
@apply border-t-background;
|
||||||
@apply drop-shadow-md;
|
@apply drop-shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) {
|
||||||
@apply border-t-background;
|
@apply border-t-background;
|
||||||
@apply drop-shadow-md;
|
@apply drop-shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) {
|
||||||
@apply border-t-background;
|
@apply border-t-background;
|
||||||
@apply drop-shadow-md;
|
@apply drop-shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) {
|
||||||
@apply border-r-background;
|
@apply border-r-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) {
|
||||||
@apply border-l-background;
|
@apply border-l-background;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
|
|
||||||
import { font } from "$lib/assets/layers";
|
|
||||||
import { settings } from "$lib/db";
|
import { settings } from "$lib/db";
|
||||||
import { gpxStatistics } from "$lib/stores";
|
import { gpxStatistics } from "$lib/stores";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
const { distanceMarkers, distanceUnits, currentBasemap } = settings;
|
const { distanceMarkers, distanceUnits } = settings;
|
||||||
|
|
||||||
export class DistanceMarkers {
|
export class DistanceMarkers {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
@@ -17,7 +16,7 @@ export class DistanceMarkers {
|
|||||||
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
|
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(distanceMarkers.subscribe(this.updateBinded));
|
this.unsubscribes.push(distanceMarkers.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(distanceUnits.subscribe(this.updateBinded));
|
this.unsubscribes.push(distanceUnits.subscribe(this.updateBinded));
|
||||||
this.map.on('style.load', this.updateBinded);
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
@@ -40,7 +39,7 @@ export class DistanceMarkers {
|
|||||||
layout: {
|
layout: {
|
||||||
'text-field': ['get', 'distance'],
|
'text-field': ['get', 'distance'],
|
||||||
'text-size': 14,
|
'text-size': 14,
|
||||||
'text-font': [font[get(currentBasemap)] ?? 'Open Sans Bold'],
|
'text-font': ['Open Sans Bold'],
|
||||||
'text-padding': 20,
|
'text-padding': 20,
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
|
@@ -7,7 +7,6 @@ import { addSelectItem, selectItem, selection } from "$lib/components/file-list/
|
|||||||
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
|
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
|
||||||
import type { Waypoint } from "gpx";
|
import type { Waypoint } from "gpx";
|
||||||
import { getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils";
|
import { getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils";
|
||||||
import { font } from "$lib/assets/layers";
|
|
||||||
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
|
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
|
||||||
import { MapPin, Square } from "lucide-static";
|
import { MapPin, Square } from "lucide-static";
|
||||||
import { getSymbolKey, symbols } from "$lib/assets/symbols";
|
import { getSymbolKey, symbols } from "$lib/assets/symbols";
|
||||||
@@ -66,7 +65,7 @@ function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
|
|||||||
</svg>`;
|
</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { directionMarkers, verticalFileView, currentBasemap, defaultOpacity, defaultWeight } = settings;
|
const { directionMarkers, verticalFileView, defaultOpacity, defaultWeight } = settings;
|
||||||
|
|
||||||
export class GPXLayer {
|
export class GPXLayer {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
@@ -112,7 +111,7 @@ export class GPXLayer {
|
|||||||
}));
|
}));
|
||||||
this.draggable = get(currentTool) === Tool.WAYPOINT;
|
this.draggable = get(currentTool) === Tool.WAYPOINT;
|
||||||
|
|
||||||
this.map.on('style.load', this.updateBinded);
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
@@ -170,7 +169,7 @@ export class GPXLayer {
|
|||||||
'text-keep-upright': false,
|
'text-keep-upright': false,
|
||||||
'text-max-angle': 361,
|
'text-max-angle': 361,
|
||||||
'text-allow-overlap': true,
|
'text-allow-overlap': true,
|
||||||
'text-font': [font[get(currentBasemap)] ?? 'Open Sans Bold'],
|
'text-font': ['Open Sans Bold'],
|
||||||
'symbol-placement': 'line',
|
'symbol-placement': 'line',
|
||||||
'symbol-spacing': 20,
|
'symbol-spacing': 20,
|
||||||
},
|
},
|
||||||
@@ -294,7 +293,7 @@ export class GPXLayer {
|
|||||||
|
|
||||||
updateMap(map: mapboxgl.Map) {
|
updateMap(map: mapboxgl.Map) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.map.on('style.load', this.updateBinded);
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +302,7 @@ export class GPXLayer {
|
|||||||
this.map.off('click', this.fileId, this.layerOnClickBinded);
|
this.map.off('click', this.fileId, this.layerOnClickBinded);
|
||||||
this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
||||||
this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
this.map.off('style.load', this.updateBinded);
|
this.map.off('style.import.load', this.updateBinded);
|
||||||
|
|
||||||
if (this.map.getLayer(this.fileId + '-direction')) {
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
this.map.removeLayer(this.fileId + '-direction');
|
this.map.removeLayer(this.fileId + '-direction');
|
||||||
|
@@ -1,424 +1,427 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||||
import {
|
import {
|
||||||
CirclePlus,
|
CirclePlus,
|
||||||
CircleX,
|
CircleX,
|
||||||
Minus,
|
Minus,
|
||||||
Pencil,
|
Pencil,
|
||||||
Plus,
|
Plus,
|
||||||
Save,
|
Save,
|
||||||
Trash2,
|
Trash2,
|
||||||
Move,
|
Move,
|
||||||
Map,
|
Map,
|
||||||
Layers2
|
Layers2
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import { defaultBasemap, extendBasemap, type CustomLayer } from '$lib/assets/layers';
|
import { defaultBasemap, type CustomLayer } from '$lib/assets/layers';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import Sortable from 'sortablejs/Sortable';
|
import Sortable from 'sortablejs/Sortable';
|
||||||
import { customBasemapUpdate } from './utils';
|
import { customBasemapUpdate } from './utils';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
customLayers,
|
customLayers,
|
||||||
selectedBasemapTree,
|
selectedBasemapTree,
|
||||||
selectedOverlayTree,
|
selectedOverlayTree,
|
||||||
currentBasemap,
|
currentBasemap,
|
||||||
previousBasemap,
|
previousBasemap,
|
||||||
currentOverlays,
|
currentOverlays,
|
||||||
previousOverlays,
|
previousOverlays,
|
||||||
customBasemapOrder,
|
customBasemapOrder,
|
||||||
customOverlayOrder
|
customOverlayOrder
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
let name: string = '';
|
let name: string = '';
|
||||||
let tileUrls: string[] = [''];
|
let tileUrls: string[] = [''];
|
||||||
let maxZoom: number = 20;
|
let maxZoom: number = 20;
|
||||||
let layerType: 'basemap' | 'overlay' = 'basemap';
|
let layerType: 'basemap' | 'overlay' = 'basemap';
|
||||||
let resourceType: 'raster' | 'vector' = 'raster';
|
let resourceType: 'raster' | 'vector' = 'raster';
|
||||||
|
|
||||||
let basemapContainer: HTMLElement;
|
let basemapContainer: HTMLElement;
|
||||||
let overlayContainer: HTMLElement;
|
let overlayContainer: HTMLElement;
|
||||||
|
|
||||||
let basemapSortable: Sortable;
|
let basemapSortable: Sortable;
|
||||||
let overlaySortable: Sortable;
|
let overlaySortable: Sortable;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($customBasemapOrder.length === 0) {
|
if ($customBasemapOrder.length === 0) {
|
||||||
$customBasemapOrder = Object.keys($customLayers).filter(
|
$customBasemapOrder = Object.keys($customLayers).filter(
|
||||||
(id) => $customLayers[id].layerType === 'basemap'
|
(id) => $customLayers[id].layerType === 'basemap'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if ($customOverlayOrder.length === 0) {
|
if ($customOverlayOrder.length === 0) {
|
||||||
$customOverlayOrder = Object.keys($customLayers).filter(
|
$customOverlayOrder = Object.keys($customLayers).filter(
|
||||||
(id) => $customLayers[id].layerType === 'overlay'
|
(id) => $customLayers[id].layerType === 'overlay'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
basemapSortable = Sortable.create(basemapContainer, {
|
basemapSortable = Sortable.create(basemapContainer, {
|
||||||
onSort: (e) => {
|
onSort: (e) => {
|
||||||
$customBasemapOrder = basemapSortable.toArray();
|
$customBasemapOrder = basemapSortable.toArray();
|
||||||
$selectedBasemapTree.basemaps['custom'] = $customBasemapOrder.reduce((acc, id) => {
|
$selectedBasemapTree.basemaps['custom'] = $customBasemapOrder.reduce((acc, id) => {
|
||||||
acc[id] = true;
|
acc[id] = true;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
overlaySortable = Sortable.create(overlayContainer, {
|
overlaySortable = Sortable.create(overlayContainer, {
|
||||||
onSort: (e) => {
|
onSort: (e) => {
|
||||||
$customOverlayOrder = overlaySortable.toArray();
|
$customOverlayOrder = overlaySortable.toArray();
|
||||||
$selectedOverlayTree.overlays['custom'] = $customOverlayOrder.reduce((acc, id) => {
|
$selectedOverlayTree.overlays['custom'] = $customOverlayOrder.reduce((acc, id) => {
|
||||||
acc[id] = true;
|
acc[id] = true;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
basemapSortable.sort($customBasemapOrder);
|
basemapSortable.sort($customBasemapOrder);
|
||||||
overlaySortable.sort($customOverlayOrder);
|
overlaySortable.sort($customOverlayOrder);
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
basemapSortable.destroy();
|
basemapSortable.destroy();
|
||||||
overlaySortable.destroy();
|
overlaySortable.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if (tileUrls[0].length > 0) {
|
$: if (tileUrls[0].length > 0) {
|
||||||
if (
|
if (
|
||||||
tileUrls[0].includes('.json') ||
|
tileUrls[0].includes('.json') ||
|
||||||
(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
|
(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
|
||||||
) {
|
) {
|
||||||
resourceType = 'vector';
|
resourceType = 'vector';
|
||||||
layerType = 'basemap';
|
} else {
|
||||||
} else {
|
resourceType = 'raster';
|
||||||
resourceType = 'raster';
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function createLayer() {
|
function createLayer() {
|
||||||
if (selectedLayerId && $customLayers[selectedLayerId].layerType !== layerType) {
|
if (selectedLayerId && $customLayers[selectedLayerId].layerType !== layerType) {
|
||||||
deleteLayer(selectedLayerId);
|
deleteLayer(selectedLayerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof maxZoom === 'string') {
|
if (typeof maxZoom === 'string') {
|
||||||
maxZoom = parseInt(maxZoom);
|
maxZoom = parseInt(maxZoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
let layerId = selectedLayerId ?? getLayerId();
|
let layerId = selectedLayerId ?? getLayerId();
|
||||||
let layer: CustomLayer = {
|
let layer: CustomLayer = {
|
||||||
id: layerId,
|
id: layerId,
|
||||||
name: name,
|
name: name,
|
||||||
tileUrls: tileUrls,
|
tileUrls: tileUrls,
|
||||||
maxZoom: maxZoom,
|
maxZoom: maxZoom,
|
||||||
layerType: layerType,
|
layerType: layerType,
|
||||||
resourceType: resourceType,
|
resourceType: resourceType,
|
||||||
value: ''
|
value: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
if (resourceType === 'vector') {
|
if (resourceType === 'vector') {
|
||||||
layer.value = tileUrls[0];
|
layer.value = tileUrls[0];
|
||||||
} else {
|
} else {
|
||||||
if (layerType === 'basemap') {
|
layer.value = {
|
||||||
layer.value = extendBasemap({
|
version: 8,
|
||||||
version: 8,
|
sources: {
|
||||||
sources: {
|
[layerId]: {
|
||||||
[layerId]: {
|
type: 'raster',
|
||||||
type: 'raster',
|
tiles: tileUrls,
|
||||||
tiles: tileUrls,
|
tileSize: 256,
|
||||||
tileSize: 256,
|
maxzoom: maxZoom
|
||||||
maxzoom: maxZoom
|
}
|
||||||
}
|
},
|
||||||
},
|
layers: [
|
||||||
layers: [
|
{
|
||||||
{
|
id: layerId,
|
||||||
id: layerId,
|
type: 'raster',
|
||||||
type: 'raster',
|
source: layerId
|
||||||
source: layerId
|
}
|
||||||
}
|
]
|
||||||
]
|
};
|
||||||
});
|
}
|
||||||
} else {
|
$customLayers[layerId] = layer;
|
||||||
layer.value = {
|
addLayer(layerId);
|
||||||
type: 'raster',
|
selectedLayerId = undefined;
|
||||||
tiles: tileUrls,
|
setDataFromSelectedLayer();
|
||||||
tileSize: 256,
|
}
|
||||||
maxzoom: maxZoom
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$customLayers[layerId] = layer;
|
|
||||||
addLayer(layerId);
|
|
||||||
selectedLayerId = undefined;
|
|
||||||
setDataFromSelectedLayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLayerId() {
|
function getLayerId() {
|
||||||
for (let id = 0; ; id++) {
|
for (let id = 0; ; id++) {
|
||||||
if (!$customLayers.hasOwnProperty(`custom-${id}`)) {
|
if (!$customLayers.hasOwnProperty(`custom-${id}`)) {
|
||||||
return `custom-${id}`;
|
return `custom-${id}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLayer(layerId: string) {
|
function addLayer(layerId: string) {
|
||||||
if (layerType === 'basemap') {
|
if (layerType === 'basemap') {
|
||||||
selectedBasemapTree.update(($tree) => {
|
selectedBasemapTree.update(($tree) => {
|
||||||
if (!$tree.basemaps.hasOwnProperty('custom')) {
|
if (!$tree.basemaps.hasOwnProperty('custom')) {
|
||||||
$tree.basemaps['custom'] = {};
|
$tree.basemaps['custom'] = {};
|
||||||
}
|
}
|
||||||
$tree.basemaps['custom'][layerId] = true;
|
$tree.basemaps['custom'][layerId] = true;
|
||||||
return $tree;
|
return $tree;
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($currentBasemap === layerId) {
|
if ($currentBasemap === layerId) {
|
||||||
$customBasemapUpdate++;
|
$customBasemapUpdate++;
|
||||||
} else {
|
} else {
|
||||||
$currentBasemap = layerId;
|
$currentBasemap = layerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$customBasemapOrder.includes(layerId)) {
|
if (!$customBasemapOrder.includes(layerId)) {
|
||||||
$customBasemapOrder = [...$customBasemapOrder, layerId];
|
$customBasemapOrder = [...$customBasemapOrder, layerId];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedOverlayTree.update(($tree) => {
|
selectedOverlayTree.update(($tree) => {
|
||||||
if (!$tree.overlays.hasOwnProperty('custom')) {
|
if (!$tree.overlays.hasOwnProperty('custom')) {
|
||||||
$tree.overlays['custom'] = {};
|
$tree.overlays['custom'] = {};
|
||||||
}
|
}
|
||||||
$tree.overlays['custom'][layerId] = true;
|
$tree.overlays['custom'][layerId] = true;
|
||||||
return $tree;
|
return $tree;
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($map && $map.getSource(layerId)) {
|
if ($map) {
|
||||||
// Reset source when updating an existing layer
|
try {
|
||||||
if ($map.getLayer(layerId)) {
|
$map.removeImport(layerId);
|
||||||
$map.removeLayer(layerId);
|
} catch (e) {
|
||||||
}
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
$map.removeSource(layerId);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$currentOverlays.overlays.hasOwnProperty('custom')) {
|
if (!$currentOverlays.overlays.hasOwnProperty('custom')) {
|
||||||
$currentOverlays.overlays['custom'] = {};
|
$currentOverlays.overlays['custom'] = {};
|
||||||
}
|
}
|
||||||
$currentOverlays.overlays['custom'][layerId] = true;
|
$currentOverlays.overlays['custom'][layerId] = true;
|
||||||
|
|
||||||
if (!$customOverlayOrder.includes(layerId)) {
|
if (!$customOverlayOrder.includes(layerId)) {
|
||||||
$customOverlayOrder = [...$customOverlayOrder, layerId];
|
$customOverlayOrder = [...$customOverlayOrder, layerId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryDeleteLayer(node: any, id: string): any {
|
function tryDeleteLayer(node: any, id: string): any {
|
||||||
if (node.hasOwnProperty(id)) {
|
if (node.hasOwnProperty(id)) {
|
||||||
delete node[id];
|
delete node[id];
|
||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteLayer(layerId: string) {
|
function deleteLayer(layerId: string) {
|
||||||
let layer = $customLayers[layerId];
|
let layer = $customLayers[layerId];
|
||||||
if (layer.layerType === 'basemap') {
|
if (layer.layerType === 'basemap') {
|
||||||
if (layerId === $currentBasemap) {
|
if (layerId === $currentBasemap) {
|
||||||
$currentBasemap = defaultBasemap;
|
$currentBasemap = defaultBasemap;
|
||||||
}
|
}
|
||||||
if (layerId === $previousBasemap) {
|
if (layerId === $previousBasemap) {
|
||||||
$previousBasemap = defaultBasemap;
|
$previousBasemap = defaultBasemap;
|
||||||
}
|
}
|
||||||
|
|
||||||
$selectedBasemapTree.basemaps['custom'] = tryDeleteLayer(
|
$selectedBasemapTree.basemaps['custom'] = tryDeleteLayer(
|
||||||
$selectedBasemapTree.basemaps['custom'],
|
$selectedBasemapTree.basemaps['custom'],
|
||||||
layerId
|
layerId
|
||||||
);
|
);
|
||||||
if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) {
|
if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) {
|
||||||
$selectedBasemapTree.basemaps = tryDeleteLayer($selectedBasemapTree.basemaps, 'custom');
|
$selectedBasemapTree.basemaps = tryDeleteLayer(
|
||||||
}
|
$selectedBasemapTree.basemaps,
|
||||||
$customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId);
|
'custom'
|
||||||
} else {
|
);
|
||||||
$currentOverlays.overlays['custom'][layerId] = false;
|
}
|
||||||
if ($previousOverlays.overlays['custom']) {
|
$customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId);
|
||||||
$previousOverlays.overlays['custom'] = tryDeleteLayer(
|
} else {
|
||||||
$previousOverlays.overlays['custom'],
|
$currentOverlays.overlays['custom'][layerId] = false;
|
||||||
layerId
|
if ($previousOverlays.overlays['custom']) {
|
||||||
);
|
$previousOverlays.overlays['custom'] = tryDeleteLayer(
|
||||||
}
|
$previousOverlays.overlays['custom'],
|
||||||
|
layerId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$selectedOverlayTree.overlays['custom'] = tryDeleteLayer(
|
$selectedOverlayTree.overlays['custom'] = tryDeleteLayer(
|
||||||
$selectedOverlayTree.overlays['custom'],
|
$selectedOverlayTree.overlays['custom'],
|
||||||
layerId
|
layerId
|
||||||
);
|
);
|
||||||
if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) {
|
if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) {
|
||||||
$selectedOverlayTree.overlays = tryDeleteLayer($selectedOverlayTree.overlays, 'custom');
|
$selectedOverlayTree.overlays = tryDeleteLayer(
|
||||||
}
|
$selectedOverlayTree.overlays,
|
||||||
$customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId);
|
'custom'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId);
|
||||||
|
|
||||||
if ($map) {
|
if ($map) {
|
||||||
if ($map.getLayer(layerId)) {
|
try {
|
||||||
$map.removeLayer(layerId);
|
$map.removeImport(layerId);
|
||||||
}
|
} catch (e) {
|
||||||
if ($map.getSource(layerId)) {
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
$map.removeSource(layerId);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
$customLayers = tryDeleteLayer($customLayers, layerId);
|
||||||
$customLayers = tryDeleteLayer($customLayers, layerId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let selectedLayerId: string | undefined = undefined;
|
let selectedLayerId: string | undefined = undefined;
|
||||||
|
|
||||||
function setDataFromSelectedLayer() {
|
function setDataFromSelectedLayer() {
|
||||||
if (selectedLayerId) {
|
if (selectedLayerId) {
|
||||||
const layer = $customLayers[selectedLayerId];
|
const layer = $customLayers[selectedLayerId];
|
||||||
name = layer.name;
|
name = layer.name;
|
||||||
tileUrls = layer.tileUrls;
|
tileUrls = layer.tileUrls;
|
||||||
maxZoom = layer.maxZoom;
|
maxZoom = layer.maxZoom;
|
||||||
layerType = layer.layerType;
|
layerType = layer.layerType;
|
||||||
resourceType = layer.resourceType;
|
resourceType = layer.resourceType;
|
||||||
} else {
|
} else {
|
||||||
name = '';
|
name = '';
|
||||||
tileUrls = [''];
|
tileUrls = [''];
|
||||||
maxZoom = 20;
|
maxZoom = 20;
|
||||||
layerType = 'basemap';
|
layerType = 'basemap';
|
||||||
resourceType = 'raster';
|
resourceType = 'raster';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: selectedLayerId, setDataFromSelectedLayer();
|
$: selectedLayerId, setDataFromSelectedLayer();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
{#if $customBasemapOrder.length > 0}
|
{#if $customBasemapOrder.length > 0}
|
||||||
<div class="flex flex-row items-center gap-1 font-semibold mb-2">
|
<div class="flex flex-row items-center gap-1 font-semibold mb-2">
|
||||||
<Map size="16" />
|
<Map size="16" />
|
||||||
{$_('layers.label.basemaps')}
|
{$_('layers.label.basemaps')}
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
bind:this={basemapContainer}
|
bind:this={basemapContainer}
|
||||||
class="ml-1.5 flex flex-col gap-1 {$customBasemapOrder.length > 0 ? 'mb-2' : ''}"
|
class="ml-1.5 flex flex-col gap-1 {$customBasemapOrder.length > 0 ? 'mb-2' : ''}"
|
||||||
>
|
>
|
||||||
{#each $customBasemapOrder as id (id)}
|
{#each $customBasemapOrder as id (id)}
|
||||||
<div class="flex flex-row items-center gap-2" data-id={id}>
|
<div class="flex flex-row items-center gap-2" data-id={id}>
|
||||||
<Move size="12" />
|
<Move size="12" />
|
||||||
<span class="grow">{$customLayers[id].name}</span>
|
<span class="grow">{$customLayers[id].name}</span>
|
||||||
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
|
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
|
||||||
<Pencil size="16" />
|
<Pencil size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
|
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
|
||||||
<Trash2 size="16" />
|
<Trash2 size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if $customOverlayOrder.length > 0}
|
{#if $customOverlayOrder.length > 0}
|
||||||
<div class="flex flex-row items-center gap-1 font-semibold mb-2">
|
<div class="flex flex-row items-center gap-1 font-semibold mb-2">
|
||||||
<Layers2 size="16" />
|
<Layers2 size="16" />
|
||||||
{$_('layers.label.overlays')}
|
{$_('layers.label.overlays')}
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
bind:this={overlayContainer}
|
bind:this={overlayContainer}
|
||||||
class="ml-1.5 flex flex-col gap-1 {$customOverlayOrder.length > 0 ? 'mb-2' : ''}"
|
class="ml-1.5 flex flex-col gap-1 {$customOverlayOrder.length > 0 ? 'mb-2' : ''}"
|
||||||
>
|
>
|
||||||
{#each $customOverlayOrder as id (id)}
|
{#each $customOverlayOrder as id (id)}
|
||||||
<div class="flex flex-row items-center gap-2" data-id={id}>
|
<div class="flex flex-row items-center gap-2" data-id={id}>
|
||||||
<Move size="12" />
|
<Move size="12" />
|
||||||
<span class="grow">{$customLayers[id].name}</span>
|
<span class="grow">{$customLayers[id].name}</span>
|
||||||
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
|
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
|
||||||
<Pencil size="16" />
|
<Pencil size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
|
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
|
||||||
<Trash2 size="16" />
|
<Trash2 size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header class="p-3">
|
<Card.Header class="p-3">
|
||||||
<Card.Title class="text-base">
|
<Card.Title class="text-base">
|
||||||
{#if selectedLayerId}
|
{#if selectedLayerId}
|
||||||
{$_('layers.custom_layers.edit')}
|
{$_('layers.custom_layers.edit')}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('layers.custom_layers.new')}
|
{$_('layers.custom_layers.new')}
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content class="p-3 pt-0">
|
<Card.Content class="p-3 pt-0">
|
||||||
<fieldset class="flex flex-col gap-2">
|
<fieldset class="flex flex-col gap-2">
|
||||||
<Label for="name">{$_('menu.metadata.name')}</Label>
|
<Label for="name">{$_('menu.metadata.name')}</Label>
|
||||||
<Input bind:value={name} id="name" class="h-8" />
|
<Input bind:value={name} id="name" class="h-8" />
|
||||||
<Label for="url">{$_('layers.custom_layers.urls')}</Label>
|
<Label for="url">{$_('layers.custom_layers.urls')}</Label>
|
||||||
{#each tileUrls as url, i}
|
{#each tileUrls as url, i}
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<Input
|
<Input
|
||||||
bind:value={tileUrls[i]}
|
bind:value={tileUrls[i]}
|
||||||
id="url"
|
id="url"
|
||||||
class="h-8"
|
class="h-8"
|
||||||
placeholder={$_('layers.custom_layers.url_placeholder')}
|
placeholder={$_('layers.custom_layers.url_placeholder')}
|
||||||
/>
|
/>
|
||||||
{#if tileUrls.length > 1}
|
{#if tileUrls.length > 1}
|
||||||
<Button
|
<Button
|
||||||
on:click={() => (tileUrls = tileUrls.filter((_, index) => index !== i))}
|
on:click={() =>
|
||||||
variant="outline"
|
(tileUrls = tileUrls.filter((_, index) => index !== i))}
|
||||||
class="p-1 h-8"
|
variant="outline"
|
||||||
>
|
class="p-1 h-8"
|
||||||
<Minus size="16" />
|
>
|
||||||
</Button>
|
<Minus size="16" />
|
||||||
{/if}
|
</Button>
|
||||||
{#if i === tileUrls.length - 1}
|
{/if}
|
||||||
<Button
|
{#if i === tileUrls.length - 1}
|
||||||
on:click={() => (tileUrls = [...tileUrls, ''])}
|
<Button
|
||||||
variant="outline"
|
on:click={() => (tileUrls = [...tileUrls, ''])}
|
||||||
class="p-1 h-8"
|
variant="outline"
|
||||||
>
|
class="p-1 h-8"
|
||||||
<Plus size="16" />
|
>
|
||||||
</Button>
|
<Plus size="16" />
|
||||||
{/if}
|
</Button>
|
||||||
</div>
|
{/if}
|
||||||
{/each}
|
</div>
|
||||||
{#if resourceType === 'raster'}
|
{/each}
|
||||||
<Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label>
|
{#if resourceType === 'raster'}
|
||||||
<Input type="number" bind:value={maxZoom} id="maxZoom" min={0} max={22} class="h-8" />
|
<Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label>
|
||||||
{/if}
|
<Input
|
||||||
<Label>{$_('layers.custom_layers.layer_type')}</Label>
|
type="number"
|
||||||
<RadioGroup.Root bind:value={layerType} class="flex flex-row">
|
bind:value={maxZoom}
|
||||||
<div class="flex items-center space-x-2">
|
id="maxZoom"
|
||||||
<RadioGroup.Item value="basemap" id="basemap" />
|
min={0}
|
||||||
<Label for="basemap">{$_('layers.custom_layers.basemap')}</Label>
|
max={22}
|
||||||
</div>
|
class="h-8"
|
||||||
<div class="flex items-center space-x-2">
|
/>
|
||||||
<RadioGroup.Item value="overlay" id="overlay" disabled={resourceType === 'vector'} />
|
{/if}
|
||||||
<Label for="overlay">{$_('layers.custom_layers.overlay')}</Label>
|
<Label>{$_('layers.custom_layers.layer_type')}</Label>
|
||||||
</div>
|
<RadioGroup.Root bind:value={layerType} class="flex flex-row">
|
||||||
</RadioGroup.Root>
|
<div class="flex items-center space-x-2">
|
||||||
{#if selectedLayerId}
|
<RadioGroup.Item value="basemap" id="basemap" />
|
||||||
<div class="mt-2 flex flex-row gap-2">
|
<Label for="basemap">{$_('layers.custom_layers.basemap')}</Label>
|
||||||
<Button variant="outline" on:click={createLayer} class="grow">
|
</div>
|
||||||
<Save size="16" class="mr-1" />
|
<div class="flex items-center space-x-2">
|
||||||
{$_('layers.custom_layers.update')}
|
<RadioGroup.Item value="overlay" id="overlay" />
|
||||||
</Button>
|
<Label for="overlay">{$_('layers.custom_layers.overlay')}</Label>
|
||||||
<Button variant="outline" on:click={() => (selectedLayerId = undefined)}>
|
</div>
|
||||||
<CircleX size="16" />
|
</RadioGroup.Root>
|
||||||
</Button>
|
{#if selectedLayerId}
|
||||||
</div>
|
<div class="mt-2 flex flex-row gap-2">
|
||||||
{:else}
|
<Button variant="outline" on:click={createLayer} class="grow">
|
||||||
<Button variant="outline" class="mt-2" on:click={createLayer}>
|
<Save size="16" class="mr-1" />
|
||||||
<CirclePlus size="16" class="mr-1" />
|
{$_('layers.custom_layers.update')}
|
||||||
{$_('layers.custom_layers.create')}
|
</Button>
|
||||||
</Button>
|
<Button variant="outline" on:click={() => (selectedLayerId = undefined)}>
|
||||||
{/if}
|
<CircleX size="16" />
|
||||||
</fieldset>
|
</Button>
|
||||||
</Card.Content>
|
</div>
|
||||||
</Card.Root>
|
{:else}
|
||||||
|
<Button variant="outline" class="mt-2" on:click={createLayer}>
|
||||||
|
<CirclePlus size="16" class="mr-1" />
|
||||||
|
{$_('layers.custom_layers.create')}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</fieldset>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,203 +1,219 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
|
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
|
||||||
import LayerTree from './LayerTree.svelte';
|
import LayerTree from './LayerTree.svelte';
|
||||||
|
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
|
|
||||||
import { Layers } from 'lucide-svelte';
|
import { Layers } from 'lucide-svelte';
|
||||||
|
|
||||||
import { basemaps, defaultBasemap, overlays } from '$lib/assets/layers';
|
import { basemaps, defaultBasemap, overlays } from '$lib/assets/layers';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import { customBasemapUpdate, getLayers } from './utils';
|
import { customBasemapUpdate, getLayers } from './utils';
|
||||||
import { OverpassLayer } from './OverpassLayer';
|
import { OverpassLayer } from './OverpassLayer';
|
||||||
import OverpassPopup from './OverpassPopup.svelte';
|
import OverpassPopup from './OverpassPopup.svelte';
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let overpassLayer: OverpassLayer;
|
let overpassLayer: OverpassLayer;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentBasemap,
|
currentBasemap,
|
||||||
previousBasemap,
|
previousBasemap,
|
||||||
currentOverlays,
|
currentOverlays,
|
||||||
currentOverpassQueries,
|
currentOverpassQueries,
|
||||||
selectedBasemapTree,
|
selectedBasemapTree,
|
||||||
selectedOverlayTree,
|
selectedOverlayTree,
|
||||||
selectedOverpassTree,
|
selectedOverpassTree,
|
||||||
customLayers,
|
customLayers,
|
||||||
opacities
|
opacities
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
function setStyle() {
|
function setStyle() {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
let basemap = basemaps.hasOwnProperty($currentBasemap)
|
let basemap = basemaps.hasOwnProperty($currentBasemap)
|
||||||
? basemaps[$currentBasemap]
|
? basemaps[$currentBasemap]
|
||||||
: $customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap];
|
: $customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap];
|
||||||
$map.setStyle(basemap, {
|
$map.removeImport('basemap');
|
||||||
diff: false
|
if (typeof basemap === 'string') {
|
||||||
});
|
$map.addImport({ id: 'basemap', url: basemap }, 'overlays');
|
||||||
}
|
} else {
|
||||||
}
|
$map.addImport(
|
||||||
|
{
|
||||||
|
id: 'basemap',
|
||||||
|
data: basemap
|
||||||
|
},
|
||||||
|
'overlays'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$: if ($map && ($currentBasemap || $customBasemapUpdate)) {
|
$: if ($map && ($currentBasemap || $customBasemapUpdate)) {
|
||||||
setStyle();
|
setStyle();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($map && $currentOverlays) {
|
function addOverlay(id: string) {
|
||||||
// Add or remove overlay layers depending on the current overlays
|
try {
|
||||||
let overlayLayers = getLayers($currentOverlays);
|
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
|
||||||
Object.keys(overlayLayers).forEach((id) => {
|
if (typeof overlay === 'string') {
|
||||||
if (overlayLayers[id]) {
|
$map.addImport({ id, url: overlay });
|
||||||
if (!addOverlayLayer.hasOwnProperty(id)) {
|
} else {
|
||||||
addOverlayLayer[id] = addOverlayLayerForId(id);
|
$map.addImport({
|
||||||
}
|
id,
|
||||||
if (!$map.getLayer(id)) {
|
data: overlay
|
||||||
addOverlayLayer[id]();
|
});
|
||||||
$map.on('style.load', addOverlayLayer[id]);
|
}
|
||||||
}
|
} catch (e) {
|
||||||
} else if ($map.getLayer(id)) {
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
$map.removeLayer(id);
|
}
|
||||||
$map.off('style.load', addOverlayLayer[id]);
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if ($map) {
|
function updateOverlays() {
|
||||||
if (overpassLayer) {
|
if ($map && $currentOverlays) {
|
||||||
overpassLayer.remove();
|
let overlayLayers = getLayers($currentOverlays);
|
||||||
}
|
try {
|
||||||
overpassLayer = new OverpassLayer($map);
|
let activeOverlays = $map
|
||||||
overpassLayer.add();
|
.getStyle()
|
||||||
}
|
.imports.filter((i) => i.id !== 'basemap' && i.id !== 'overlays');
|
||||||
|
let toRemove = activeOverlays.filter((i) => !overlayLayers[i.id]);
|
||||||
|
toRemove.forEach((i) => {
|
||||||
|
$map.removeImport(i.id);
|
||||||
|
});
|
||||||
|
let toAdd = Object.entries(overlayLayers)
|
||||||
|
.filter(
|
||||||
|
([id, selected]) => selected && !activeOverlays.some((j) => j.id === id)
|
||||||
|
)
|
||||||
|
.map(([id]) => id);
|
||||||
|
toAdd.forEach((id) => {
|
||||||
|
addOverlay(id);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let selectedBasemap = writable(get(currentBasemap));
|
$: if ($map && $currentOverlays) {
|
||||||
selectedBasemap.subscribe((value) => {
|
updateOverlays();
|
||||||
// Updates coming from radio buttons
|
}
|
||||||
if (value !== get(currentBasemap)) {
|
|
||||||
previousBasemap.set(get(currentBasemap));
|
|
||||||
currentBasemap.set(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
currentBasemap.subscribe((value) => {
|
|
||||||
// Updates coming from the database, or from the user swapping basemaps
|
|
||||||
selectedBasemap.set(value);
|
|
||||||
});
|
|
||||||
|
|
||||||
let addOverlayLayer: { [key: string]: () => void } = {};
|
$: if ($map) {
|
||||||
function addOverlayLayerForId(id: string) {
|
if (overpassLayer) {
|
||||||
return () => {
|
overpassLayer.remove();
|
||||||
if ($map) {
|
}
|
||||||
try {
|
overpassLayer = new OverpassLayer($map);
|
||||||
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
|
overpassLayer.add();
|
||||||
if (!$map.getSource(id)) {
|
$map.on('style.import.load', updateOverlays);
|
||||||
$map.addSource(id, overlay);
|
}
|
||||||
}
|
|
||||||
$map.addLayer(
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
type: overlay.type === 'raster' ? 'raster' : 'line',
|
|
||||||
source: id,
|
|
||||||
paint: {
|
|
||||||
...(id in $opacities
|
|
||||||
? overlay.type === 'raster'
|
|
||||||
? { 'raster-opacity': $opacities[id] }
|
|
||||||
: { 'line-opacity': $opacities[id] }
|
|
||||||
: {})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'overlays'
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// No reliable way to check if the map is ready to add sources and layers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let open = false;
|
let selectedBasemap = writable(get(currentBasemap));
|
||||||
function openLayerControl() {
|
selectedBasemap.subscribe((value) => {
|
||||||
open = true;
|
// Updates coming from radio buttons
|
||||||
}
|
if (value !== get(currentBasemap)) {
|
||||||
function closeLayerControl() {
|
previousBasemap.set(get(currentBasemap));
|
||||||
open = false;
|
currentBasemap.set(value);
|
||||||
}
|
}
|
||||||
let cancelEvents = false;
|
});
|
||||||
|
currentBasemap.subscribe((value) => {
|
||||||
|
// Updates coming from the database, or from the user swapping basemaps
|
||||||
|
selectedBasemap.set(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
function removeOverlayLayer(id: string) {
|
||||||
|
if ($map) {
|
||||||
|
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
|
||||||
|
if (overlay.layers) {
|
||||||
|
$map.removeImport(id);
|
||||||
|
} else {
|
||||||
|
$map.removeLayer(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let open = false;
|
||||||
|
function openLayerControl() {
|
||||||
|
open = true;
|
||||||
|
}
|
||||||
|
function closeLayerControl() {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
let cancelEvents = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CustomControl class="group min-w-[29px] min-h-[29px] overflow-hidden">
|
<CustomControl class="group min-w-[29px] min-h-[29px] overflow-hidden">
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
class="h-full w-full"
|
class="h-full w-full"
|
||||||
on:mouseenter={openLayerControl}
|
on:mouseenter={openLayerControl}
|
||||||
on:mouseleave={closeLayerControl}
|
on:mouseleave={closeLayerControl}
|
||||||
on:pointerenter={() => {
|
on:pointerenter={() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
cancelEvents = true;
|
cancelEvents = true;
|
||||||
openLayerControl();
|
openLayerControl();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
cancelEvents = false;
|
cancelEvents = false;
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-row justify-center items-center delay-100 transition-[opacity] duration-0 {open
|
class="flex flex-row justify-center items-center delay-100 transition-[opacity] duration-0 {open
|
||||||
? 'opacity-0 w-0 h-0 delay-0'
|
? 'opacity-0 w-0 h-0 delay-0'
|
||||||
: 'w-[29px] h-[29px]'}"
|
: 'w-[29px] h-[29px]'}"
|
||||||
>
|
>
|
||||||
<Layers size="20" />
|
<Layers size="20" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="transition-[grid-template-rows grid-template-cols] grid grid-rows-[0fr] grid-cols-[0fr] duration-150 h-full {open
|
class="transition-[grid-template-rows grid-template-cols] grid grid-rows-[0fr] grid-cols-[0fr] duration-150 h-full {open
|
||||||
? 'grid-rows-[1fr] grid-cols-[1fr]'
|
? 'grid-rows-[1fr] grid-cols-[1fr]'
|
||||||
: ''} {cancelEvents ? 'pointer-events-none' : ''}"
|
: ''} {cancelEvents ? 'pointer-events-none' : ''}"
|
||||||
>
|
>
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<div class="h-fit">
|
<div class="h-fit">
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<LayerTree
|
<LayerTree
|
||||||
layerTree={$selectedBasemapTree}
|
layerTree={$selectedBasemapTree}
|
||||||
name="basemaps"
|
name="basemaps"
|
||||||
bind:selected={$selectedBasemap}
|
bind:selected={$selectedBasemap}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Separator class="w-full" />
|
<Separator class="w-full" />
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
{#if $currentOverlays}
|
{#if $currentOverlays}
|
||||||
<LayerTree
|
<LayerTree
|
||||||
layerTree={$selectedOverlayTree}
|
layerTree={$selectedOverlayTree}
|
||||||
name="overlays"
|
name="overlays"
|
||||||
multiple={true}
|
multiple={true}
|
||||||
bind:checked={$currentOverlays}
|
bind:checked={$currentOverlays}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Separator class="w-full" />
|
<Separator class="w-full" />
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
{#if $currentOverpassQueries}
|
{#if $currentOverpassQueries}
|
||||||
<LayerTree
|
<LayerTree
|
||||||
layerTree={$selectedOverpassTree}
|
layerTree={$selectedOverpassTree}
|
||||||
name="overpass"
|
name="overpass"
|
||||||
multiple={true}
|
multiple={true}
|
||||||
bind:checked={$currentOverpassQueries}
|
bind:checked={$currentOverpassQueries}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CustomControl>
|
</CustomControl>
|
||||||
|
|
||||||
<OverpassPopup />
|
<OverpassPopup />
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
if (open && !cancelEvents && !container.contains(e.target)) {
|
if (open && !cancelEvents && !container.contains(e.target)) {
|
||||||
closeLayerControl();
|
closeLayerControl();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@@ -50,7 +50,7 @@ export class OverpassLayer {
|
|||||||
|
|
||||||
add() {
|
add() {
|
||||||
this.map.on('moveend', this.queryIfNeededBinded);
|
this.map.on('moveend', this.queryIfNeededBinded);
|
||||||
this.map.on('style.load', this.updateBinded);
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
this.unsubscribes.push(data.subscribe(this.updateBinded));
|
this.unsubscribes.push(data.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(currentOverpassQueries.subscribe(() => {
|
this.unsubscribes.push(currentOverpassQueries.subscribe(() => {
|
||||||
this.updateBinded();
|
this.updateBinded();
|
||||||
@@ -108,15 +108,19 @@ export class OverpassLayer {
|
|||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.map.off('moveend', this.queryIfNeededBinded);
|
this.map.off('moveend', this.queryIfNeededBinded);
|
||||||
this.map.off('style.load', this.updateBinded);
|
this.map.off('style.import.load', this.updateBinded);
|
||||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
|
||||||
if (this.map.getLayer('overpass')) {
|
try {
|
||||||
this.map.removeLayer('overpass');
|
if (this.map.getLayer('overpass')) {
|
||||||
}
|
this.map.removeLayer('overpass');
|
||||||
|
}
|
||||||
|
|
||||||
if (this.map.getSource('overpass')) {
|
if (this.map.getSource('overpass')) {
|
||||||
this.map.removeSource('overpass');
|
this.map.removeSource('overpass');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -272,6 +272,7 @@
|
|||||||
"finlandTopo": "Lantmäteriverket Terrängkarta",
|
"finlandTopo": "Lantmäteriverket Terrängkarta",
|
||||||
"bgMountains": "BGMountains",
|
"bgMountains": "BGMountains",
|
||||||
"usgs": "USGS",
|
"usgs": "USGS",
|
||||||
|
"bikerouterGravel": "bikerouter.de Gravel",
|
||||||
"cyclOSMlite": "CyclOSM Lite",
|
"cyclOSMlite": "CyclOSM Lite",
|
||||||
"swisstopoSlope": "swisstopo Slope",
|
"swisstopoSlope": "swisstopo Slope",
|
||||||
"swisstopoHiking": "swisstopo Hiking",
|
"swisstopoHiking": "swisstopo Hiking",
|
||||||
|
Reference in New Issue
Block a user