diff --git a/website/src/lib/assets/layers.ts b/website/src/lib/assets/layers.ts index 8f2defa2..5d08c404 100644 --- a/website/src/lib/assets/layers.ts +++ b/website/src/lib/assets/layers.ts @@ -372,10 +372,11 @@ export const opacities: { [key: string]: number; } = { }; export type LayerTreeType = string[] | { [key: string]: LayerTreeType; }; -export type CollapsedInfoTreeType = { - self: boolean; - children: { [key: string]: CollapsedInfoTreeType; }; +export type CollapsedInfoTreeType = { + self: T; + children: { [key: string]: CollapsedInfoTreeType; }; }; +export type CheckedInfoTreeType = { [key: string]: boolean | CheckedInfoTreeType }; export const basemapTree: LayerTreeType = { basemaps: { @@ -408,7 +409,51 @@ export const overlayTree: LayerTreeType = { }, } +export const layerKeys: { [key: string]: string[]; } = { + mapboxOutdoors: ['basemaps', 'world'], + mapboxSatellite: ['basemaps', 'world'], + openStreetMap: ['basemaps', 'world'], + openTopoMap: ['basemaps', 'world'], + openHikingMap: ['basemaps', 'world'], + cyclOSM: ['basemaps', 'world'], + cyclOSMlite: ['overlays', 'world', 'cyclOSM'], + waymarkedTrailsHiking: ['overlays', 'world', 'waymarked_trails'], + waymarkedTrailsCycling: ['overlays', 'world', 'waymarked_trails'], + waymarkedTrailsMTB: ['overlays', 'world', 'waymarked_trails'], + waymarkedTrailsSkating: ['overlays', 'world', 'waymarked_trails'], + waymarkedTrailsHorseRiding: ['overlays', 'world', 'waymarked_trails'], + waymarkedTrailsWinter: ['overlays', 'world', 'waymarked_trails'], + bgMountains: ['basemaps', 'countries', 'bulgaria'], + finlandTopo: ['basemaps', 'countries', 'finland'], + ignPlanV2: ['basemaps', 'countries', 'france'], + ignFrScan25: ['basemaps', 'countries', 'france'], + ignSatellite: ['basemaps', 'countries', 'france'], + ignFrCadastre: ['overlays', 'countries', 'france'], + ignSlope: ['overlays', 'countries', 'france'], + linz: ['basemaps', 'countries', 'new_zealand'], + linzTopo: ['basemaps', 'countries', 'new_zealand'], + norwayTopo: ['basemaps', 'countries', 'norway'], + swisstopo: ['basemaps', 'countries', 'switzerland'], + swisstopoSlope: ['overlays', 'countries', 'switzerland'], + swisstopoCycling: ['overlays', 'countries', 'switzerland'], + swisstopoMountainBike: ['overlays', 'countries', 'switzerland'], + swedenTopo: ['basemaps', 'countries', 'sweden'], + ordnanceSurvey: ['basemaps', 'countries', 'united_kingdom'], + usgs: ['basemaps', 'countries', 'united_states'], +}; + export const defaultBasemap = 'mapboxOutdoors'; -export const defaultAvailableBasemaps = ['mapboxOutdoors', 'mapboxSatellite', 'openStreetMap', 'openTopoMap', 'openHikingMap', 'cyclOSM']; -export const defaultAvailableOverlays = ['cyclOSMlite', 'waymarkedTrailsHiking', 'waymarkedTrailsCycling', 'waymarkedTrailsMTB', 'waymarkedTrailsSkating', 'waymarkedTrailsHorseRiding', 'waymarkedTrailsWinter']; \ No newline at end of file +export const defaultBasemapTree: LayerTreeType = { + basemaps: { + world: ['mapboxOutdoors', 'mapboxSatellite', 'openStreetMap', 'openTopoMap', 'openHikingMap', 'cyclOSM'] + } +}; +export const defaultOverlayTree: LayerTreeType = { + overlays: { + world: { + cyclOSM: ['cyclOSMlite'], + waymarked_trails: ['waymarkedTrailsHiking', 'waymarkedTrailsCycling', 'waymarkedTrailsMTB'] + } + } +} \ No newline at end of file diff --git a/website/src/lib/components/Map.svelte b/website/src/lib/components/Map.svelte index 45395358..41b5652c 100644 --- a/website/src/lib/components/Map.svelte +++ b/website/src/lib/components/Map.svelte @@ -53,7 +53,7 @@ } onMount(() => { - $map = new mapboxgl.Map({ + let newMap = new mapboxgl.Map({ container: 'map', style: { version: 8, sources: {}, layers: [] }, projection: { name: 'mercator' }, @@ -63,16 +63,19 @@ logoPosition: 'bottom-right', boxZoom: false }); + newMap.on('load', () => { + $map = newMap; // only set the store after the map has loaded + }); - $map.addControl( + newMap.addControl( new mapboxgl.AttributionControl({ compact: true }) ); - $map.addControl(new mapboxgl.NavigationControl()); + newMap.addControl(new mapboxgl.NavigationControl()); - $map.addControl( + newMap.addControl( new MapboxGeocoder({ accessToken: mapboxgl.accessToken, mapboxgl: mapboxgl, @@ -82,7 +85,7 @@ }) ); - $map.addControl( + newMap.addControl( new mapboxgl.GeolocateControl({ positionOptions: { enableHighAccuracy: true @@ -93,10 +96,10 @@ }) ); - $map.addControl(scaleControl); + newMap.addControl(scaleControl); - $map.on('style.load', toggleTerrain); - $map.on('pitchstart', toggleTerrain); + newMap.on('style.load', toggleTerrain); + newMap.on('pitchstart', toggleTerrain); }); $: if ($map) { diff --git a/website/src/lib/components/Menu.svelte b/website/src/lib/components/Menu.svelte index ef00029b..95e6ac2f 100644 --- a/website/src/lib/components/Menu.svelte +++ b/website/src/lib/components/Menu.svelte @@ -23,7 +23,16 @@ let showDistanceMarkers = false; let showDirectionMarkers = false; - const { distanceUnits, velocityUnits, temperatureUnits, mode } = settings; + const { + distanceUnits, + velocityUnits, + temperatureUnits, + mode, + currentBasemap, + previousBasemap, + currentOverlays, + previousOverlays + } = settings; $: if ($mode === 'system') { resetMode(); } else { @@ -184,7 +193,6 @@ { - e.stopImmediatePropagation(); if (e.key === 'n' && (e.metaKey || e.ctrlKey)) { createFile(); e.preventDefault(); @@ -217,6 +225,16 @@ } else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) { $selectFiles.selectAllFiles(); e.preventDefault(); + } else if (e.key === 'F1') { + [$currentBasemap, $previousBasemap] = [$previousBasemap, $currentBasemap]; + e.preventDefault(); + } else if (e.key === 'F2') { + if ($currentOverlays.length > 0) { + [$currentOverlays, $previousOverlays] = [[], $currentOverlays]; + } else { + [$currentOverlays, $previousOverlays] = [$previousOverlays, []]; + } + e.preventDefault(); } }} /> diff --git a/website/src/lib/components/custom-control/CustomControl.svelte b/website/src/lib/components/custom-control/CustomControl.svelte index 1919f568..735569d4 100644 --- a/website/src/lib/components/custom-control/CustomControl.svelte +++ b/website/src/lib/components/custom-control/CustomControl.svelte @@ -8,15 +8,11 @@ let container: HTMLDivElement | null = null; $: if ($map && container) { - $map.on('load', () => { - if ($map && container) { - if (position.includes('right')) container.classList.add('float-right'); - else container.classList.add('float-left'); - container.classList.remove('hidden'); - let control = new CustomControl(container); - $map.addControl(control, position); - } - }); + if (position.includes('right')) container.classList.add('float-right'); + else container.classList.add('float-left'); + container.classList.remove('hidden'); + let control = new CustomControl(container); + $map.addControl(control, position); } diff --git a/website/src/lib/components/gpx-layer/GPXLayer.ts b/website/src/lib/components/gpx-layer/GPXLayer.ts index f6b4bf53..09ed2cec 100644 --- a/website/src/lib/components/gpx-layer/GPXLayer.ts +++ b/website/src/lib/components/gpx-layer/GPXLayer.ts @@ -68,35 +68,39 @@ export class GPXLayer { } let addedSource = false; - if (!this.map.getSource(this.fileId)) { - let data = this.getGeoJSON(); + try { + if (!this.map.getSource(this.fileId)) { + let data = this.getGeoJSON(); - this.map.addSource(this.fileId, { - type: 'geojson', - data - }); - addedSource = true; - } + this.map.addSource(this.fileId, { + type: 'geojson', + data + }); + addedSource = true; + } - if (!this.map.getLayer(this.fileId)) { - this.map.addLayer({ - id: this.fileId, - type: 'line', - source: this.fileId, - layout: { - 'line-join': 'round', - 'line-cap': 'round' - }, - paint: { - 'line-color': ['get', 'color'], - 'line-width': ['get', 'weight'], - 'line-opacity': ['get', 'opacity'] - } - }); + if (!this.map.getLayer(this.fileId)) { + this.map.addLayer({ + id: this.fileId, + type: 'line', + source: this.fileId, + layout: { + 'line-join': 'round', + 'line-cap': 'round' + }, + paint: { + 'line-color': ['get', 'color'], + 'line-width': ['get', 'weight'], + 'line-opacity': ['get', 'opacity'] + } + }); - this.map.on('click', this.fileId, this.selectOnClickBinded); - this.map.on('mouseenter', this.fileId, toPointerCursor); - this.map.on('mouseleave', this.fileId, toDefaultCursor); + this.map.on('click', this.fileId, this.selectOnClickBinded); + this.map.on('mouseenter', this.fileId, toPointerCursor); + this.map.on('mouseleave', this.fileId, toDefaultCursor); + } + } catch (e) { // No reliable way to check if the map is ready to add sources and layers + return; } if (!addedSource) { @@ -138,8 +142,12 @@ export class GPXLayer { this.map.off('mouseleave', this.fileId, toDefaultCursor); this.map.off('style.load', this.updateBinded); - this.map.removeLayer(this.fileId); - this.map.removeSource(this.fileId); + if (this.map.getLayer(this.fileId)) { + this.map.removeLayer(this.fileId); + } + if (this.map.getSource(this.fileId)) { + this.map.removeSource(this.fileId); + } this.markers.forEach((marker) => { marker.remove(); diff --git a/website/src/lib/components/layer-control/LayerControl.svelte b/website/src/lib/components/layer-control/LayerControl.svelte index 3caf8c6b..883ac73d 100644 --- a/website/src/lib/components/layer-control/LayerControl.svelte +++ b/website/src/lib/components/layer-control/LayerControl.svelte @@ -8,43 +8,93 @@ import { Layers } from 'lucide-svelte'; - import { - basemaps, - basemapTree, - overlays, - overlayTree, - opacities, - defaultBasemap - } from '$lib/assets/layers'; - + import { basemaps, overlays, opacities } from '$lib/assets/layers'; + import { settings } from '$lib/db'; import { map } from '$lib/stores'; + import { get, writable } from 'svelte/store'; + + const { + currentBasemap, + previousBasemap, + currentOverlays, + selectedBasemapTree, + selectedOverlayTree + } = settings; $: if ($map) { - $map.on('load', () => { - $map.setStyle(basemaps[defaultBasemap]); + // Set style depending on the current basemap + $map.setStyle(basemaps[$currentBasemap], { + diff: false }); } - let addOverlayLayer: { [key: string]: () => void } = {}; + $: if ($map) { + // Add or remove overlay layers depending on the current overlays + // Object.keys(overlays).forEach((id) => { + // if ($currentOverlays.includes(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]); + // } + // }); + console.log($currentOverlays); + } + 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); + }); + + let selectedOverlays = writable(get(currentOverlays)); + selectedOverlays.subscribe((value) => { + // Updates coming from checkboxes + if (value != get(currentOverlays)) { + currentOverlays.set(value); + } + }); + currentOverlays.subscribe((value) => { + // Updates coming from the database, or from the user toggling overlays + selectedOverlays.set(value); + }); + + let addOverlayLayer: { [key: string]: () => void } = {}; function addOverlayLayerForId(id: string) { return () => { if ($map) { - if (!$map.getSource(id)) { - $map.addSource(id, overlays[id]); - } - $map.addLayer({ - id, - type: overlays[id].type === 'raster' ? 'raster' : 'line', - source: id, - paint: { - ...(id in opacities - ? overlays[id].type === 'raster' - ? { 'raster-opacity': opacities[id] } - : { 'line-opacity': opacities[id] } - : {}) + try { + if (!$map.getSource(id)) { + $map.addSource(id, overlays[id]); } - }); + $map.addLayer({ + id, + type: overlays[id].type === 'raster' ? 'raster' : 'line', + source: id, + paint: { + ...(id in opacities + ? overlays[id].type === 'raster' + ? { 'raster-opacity': opacities[id] } + : { 'line-opacity': opacities[id] } + : {}) + } + }); + } catch (e) { + // No reliable way to check if the map is ready to add sources and layers + } } }; } @@ -63,37 +113,18 @@
{ - if ($map) { - $map.setStyle(basemaps[id], { - diff: false - }); - } - }} + bind:selected={$selectedBasemap} />
{ - if (!addOverlayLayer.hasOwnProperty(id)) { - addOverlayLayer[id] = addOverlayLayerForId(id); - } - if ($map) { - if (checked) { - addOverlayLayer[id](); - $map.on('style.load', addOverlayLayer[id]); - } else { - $map.removeLayer(id); - $map.off('style.load', addOverlayLayer[id]); - } - } - }} + bind:checked={$selectedOverlays} />
diff --git a/website/src/lib/components/layer-control/LayerControlSettings.svelte b/website/src/lib/components/layer-control/LayerControlSettings.svelte index 5f445bd1..77557cf6 100644 --- a/website/src/lib/components/layer-control/LayerControlSettings.svelte +++ b/website/src/lib/components/layer-control/LayerControlSettings.svelte @@ -9,16 +9,21 @@ import { Settings } from 'lucide-svelte'; - import { - basemaps, - basemapTree, - overlays, - overlayTree, - opacities, - defaultBasemap - } from '$lib/assets/layers'; + import { basemapTree, overlayTree, type CollapsedInfoTreeType } from '$lib/assets/layers'; + import { settings } from '$lib/db'; import { _ } from 'svelte-i18n'; + + const { selectedBasemapTree, selectedOverlayTree } = settings; + + let checkedBasemaps: CollapsedInfoTreeType<{ [key: string]: boolean }> = { + self: {}, + children: {} + }; + let checkedOverlays: CollapsedInfoTreeType<{ [key: string]: boolean }> = { + self: {}, + children: {} + }; @@ -43,9 +48,10 @@ layerTree={basemapTree} name="basemapSettings" multiple={true} - onValueChange={(id) => { - // TODO + onValueChange={(id, checked) => { + console.log('basemap', id, checked); }} + bind:checked={checkedBasemaps} /> @@ -55,8 +61,9 @@ name="overlaySettings" multiple={true} onValueChange={(id, checked) => { - // TODO + console.log('overlay', id, checked); }} + bind:checked={checkedOverlays} /> diff --git a/website/src/lib/components/layer-control/LayerTree.svelte b/website/src/lib/components/layer-control/LayerTree.svelte index 48428ae3..9c38d87f 100644 --- a/website/src/lib/components/layer-control/LayerTree.svelte +++ b/website/src/lib/components/layer-control/LayerTree.svelte @@ -1,19 +1,25 @@ -
- -
+
+
+ +
+
diff --git a/website/src/lib/components/layer-control/LayerTreeNode.svelte b/website/src/lib/components/layer-control/LayerTreeNode.svelte index 6a3fe383..f180ba4c 100644 --- a/website/src/lib/components/layer-control/LayerTreeNode.svelte +++ b/website/src/lib/components/layer-control/LayerTreeNode.svelte @@ -6,24 +6,20 @@ import { ChevronDown, ChevronUp } from 'lucide-svelte'; - import { type CollapsedInfoTreeType, type LayerTreeType } from '$lib/assets/layers'; + import { + type CheckedInfoTreeType, + type CollapsedInfoTreeType, + type LayerTreeType + } from '$lib/assets/layers'; import { _ } from 'svelte-i18n'; export let name: string; export let node: LayerTreeType; + export let selected: string | undefined = undefined; export let multiple: boolean = false; - export let onValueChange: (id: string, checked: boolean) => void; - - let checked: { [key: string]: boolean } = {}; - if (multiple && Array.isArray(node)) { - node.forEach((id) => { - checked[id] = false; - }); - } - - export let open: CollapsedInfoTreeType; + export let open: CollapsedInfoTreeType; if (!Array.isArray(node)) { Object.keys(node).forEach((id) => { if (!open.children.hasOwnProperty(id)) { @@ -34,6 +30,23 @@ } }); } + + export let checked: CheckedInfoTreeType; + if (Array.isArray(node)) { + if (multiple) { + node.forEach((id) => { + if (!checked.hasOwnProperty(id)) { + checked[id] = false; + } + }); + } + } else { + Object.keys(node).forEach((id) => { + if (!checked.hasOwnProperty(id)) { + checked[id] = {}; + } + }); + } {#if Array.isArray(node)} @@ -42,27 +55,18 @@
{#if multiple} { - onValueChange(id, !checked[id]); - }} class="scale-90" /> {:else} - { - onValueChange(id, true); - }} - /> + {/if} - +
{/each}
@@ -87,9 +91,10 @@ diff --git a/website/src/lib/db.ts b/website/src/lib/db.ts index 7cd417dc..75dc0c20 100644 --- a/website/src/lib/db.ts +++ b/website/src/lib/db.ts @@ -4,6 +4,7 @@ import { type FreezedObject, type Patch, produceWithPatches, applyPatches } from import { writable, get, derived, type Readable, type Writable } from 'svelte/store'; import { fileOrder, selectedFiles } from './stores'; import { mode } from 'mode-watcher'; +import { defaultBasemap, defaultBasemapTree, defaultOverlayTree } from './assets/layers'; class Database extends Dexie { @@ -266,6 +267,7 @@ function dexieSettingStore(setting: string, initial: any): Writable { let store = writable(initial); liveQuery(() => db.settings.get(setting)).subscribe(value => { if (value !== undefined) { + console.log('setting', setting, 'changed to', value); store.set(value); } }); @@ -293,4 +295,10 @@ export const settings = { routing: dexieSettingStore('routing', true), routingProfile: dexieSettingStore('routingProfile', 'bike'), privateRoads: dexieSettingStore('privateRoads', false), + currentBasemap: dexieSettingStore('currentBasemap', defaultBasemap), + previousBasemap: dexieSettingStore('previousBasemap', defaultBasemap), + selectedBasemapTree: dexieSettingStore('selectedBasemapTree', defaultBasemapTree), + currentOverlays: dexieSettingStore('currentOverlays', {}), + previousOverlays: dexieSettingStore('previousOverlays', {}), + selectedOverlayTree: dexieSettingStore('selectedOverlayTree', defaultOverlayTree), }; \ No newline at end of file