add overlay layers
This commit is contained in:
81
web/src/lib/components/map/style_switcher.svelte
Normal file
81
web/src/lib/components/map/style_switcher.svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import type { StyleSwitcherControlOptions } from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
|
||||
import RadioGroup from "../base/radio_group.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
interface Props {
|
||||
settings: StyleSwitcherControlOptions;
|
||||
}
|
||||
|
||||
let { settings }: Props = $props();
|
||||
|
||||
let container: HTMLDivElement;
|
||||
|
||||
const mapStyles = $derived(settings.styles);
|
||||
|
||||
let showSwitcher: boolean = $state(false);
|
||||
|
||||
function toggleSwitcher() {
|
||||
showSwitcher = !showSwitcher;
|
||||
}
|
||||
|
||||
function hideSwitcher() {
|
||||
showSwitcher = false;
|
||||
}
|
||||
|
||||
export function getElement() {
|
||||
return container;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={container}
|
||||
class="maplibregl-ctrl maplibregl-ctrl-group relative z-10"
|
||||
>
|
||||
<button onclick={toggleSwitcher} aria-label="toggle style switcher">
|
||||
<i class="fa fa-layer-group text-black"></i>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="absolute bg-menu-background rounded-lg px-4 py-2 mt-2 shadow-xl space-y-3"
|
||||
class:hidden={!showSwitcher}
|
||||
style="transform: translateX(calc(-100% + 29px))"
|
||||
>
|
||||
<div class="flex items-center gap-x-12">
|
||||
<span class="font-semibold whitespace-nowrap text-base"
|
||||
>Map style</span
|
||||
>
|
||||
<button
|
||||
class="fa fa-close !rounded-full"
|
||||
onclick={hideSwitcher}
|
||||
aria-label="close style switcher"
|
||||
></button>
|
||||
</div>
|
||||
<RadioGroup
|
||||
name="completed"
|
||||
items={mapStyles}
|
||||
bind:selected={settings.state.selectedStyle}
|
||||
onchange={(style) => settings.onMapStyleSwitch(settings.styles.indexOf(style), style)}
|
||||
></RadioGroup>
|
||||
<hr class="border-input-border" />
|
||||
<span class="font-semibold whitespace-nowrap text-base">Layers</span>
|
||||
{#each settings.layers as l, i}
|
||||
<div class="flex items-center mt-2 mb-4">
|
||||
<input
|
||||
id="{l.text}-layer-checkbox"
|
||||
type="checkbox"
|
||||
checked={settings.state.selectedLayers[l.text]}
|
||||
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
|
||||
onchange={(e) => {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
settings.onLayerChange(checked, l);
|
||||
}}
|
||||
/>
|
||||
|
||||
<label for="{l.text}-layer-checkbox" class="ms-2 text-sm"
|
||||
>{$_(l.text)}</label
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -9,18 +9,24 @@
|
||||
import { findStartAndEndPoints } from "$lib/util/geojson_util";
|
||||
import { toGeoJson } from "$lib/util/gpx_util";
|
||||
import {
|
||||
baseMapStyles,
|
||||
createMarkerFromWaypoint,
|
||||
createPopupFromTrail,
|
||||
FontawesomeMarker,
|
||||
overlayLayers,
|
||||
} from "$lib/util/maplibre_util";
|
||||
import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
|
||||
import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control";
|
||||
import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule";
|
||||
import { StyleSwitcherControl } from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
|
||||
import {
|
||||
StyleSwitcherControl,
|
||||
type StyleSwitcherControlOptions,
|
||||
} from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
|
||||
import type { Feature, FeatureCollection, GeoJSON } from "geojson";
|
||||
import * as M from "maplibre-gl";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import { onDestroy, onMount, untrack } from "svelte";
|
||||
import type { RadioItem } from "../base/radio_group.svelte";
|
||||
|
||||
interface Props {
|
||||
trails?: Trail[];
|
||||
@@ -92,6 +98,8 @@
|
||||
let epc: ElevationProfileControl;
|
||||
let graticule: MaplibreGraticule;
|
||||
|
||||
let mapState: StyleSwitcherControlOptions["state"];
|
||||
|
||||
let layers: Record<
|
||||
string,
|
||||
{
|
||||
@@ -237,6 +245,16 @@
|
||||
}
|
||||
});
|
||||
|
||||
if (mapLoaded) {
|
||||
for (const [k, v] of Object.entries(mapState.selectedLayers)) {
|
||||
if (v) {
|
||||
const layer = overlayLayers.find((l) => l.text === k);
|
||||
if (!layer) continue;
|
||||
addOverlayLayer(layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!drawing &&
|
||||
fitBounds !== "off" &&
|
||||
@@ -864,36 +882,13 @@
|
||||
value: t.url,
|
||||
}),
|
||||
),
|
||||
{
|
||||
text: "OpenFreeMap",
|
||||
value: "/styles/ofm.json",
|
||||
thumbnail: "https://tile.openstreetmap.org/1/0/0.png",
|
||||
},
|
||||
{
|
||||
text: "OpenTopoMap",
|
||||
value: "/styles/otm.json",
|
||||
thumbnail: "https://tile.opentopomap.org/1/0/0.png",
|
||||
},
|
||||
{
|
||||
text: "Carto Light",
|
||||
value: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
|
||||
thumbnail:
|
||||
"https://basemaps.cartocdn.com/light_all/1/0/0@2x.png",
|
||||
},
|
||||
{
|
||||
text: "Carto Dark",
|
||||
value: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
|
||||
thumbnail:
|
||||
"https://basemaps.cartocdn.com/dark_all/1/0/0@2x.png",
|
||||
},
|
||||
...baseMapStyles,
|
||||
];
|
||||
let preferredMapStyleIndex = mapStyles.findIndex(
|
||||
(s) => s.text === localStorage.getItem("layer"),
|
||||
);
|
||||
|
||||
if (preferredMapStyleIndex == -1) {
|
||||
preferredMapStyleIndex = 0;
|
||||
}
|
||||
mapState = JSON.parse(
|
||||
localStorage.getItem("map-state") ??
|
||||
'{"selectedLayers": {}, "selectedStyle": 0}',
|
||||
);
|
||||
|
||||
if (!mapContainer) {
|
||||
return;
|
||||
@@ -902,9 +897,7 @@
|
||||
const finalMapOptions: M.MapOptions = {
|
||||
...{
|
||||
container: mapContainer,
|
||||
style:
|
||||
mapStyles[preferredMapStyleIndex].value ??
|
||||
mapStyles[0].value,
|
||||
style: mapStyles[mapState.selectedStyle].value,
|
||||
center: [initialState.lng, initialState.lat],
|
||||
zoom: initialState.zoom,
|
||||
},
|
||||
@@ -932,14 +925,26 @@
|
||||
|
||||
const switcherControl = new StyleSwitcherControl({
|
||||
styles: mapStyles,
|
||||
onSwitch: (style) => {
|
||||
layers: overlayLayers,
|
||||
onMapStyleSwitch: (i, style) => {
|
||||
layers = {};
|
||||
map?.setStyle(style.value);
|
||||
localStorage.setItem("layer", style.text);
|
||||
mapState.selectedStyle = i;
|
||||
localStorage.setItem("map-state", JSON.stringify(mapState));
|
||||
},
|
||||
selectedIndex:
|
||||
preferredMapStyleIndex !== -1 ? preferredMapStyleIndex : 0,
|
||||
onLayerChange: (checked, layer) => {
|
||||
mapState.selectedLayers[layer.text] = checked;
|
||||
localStorage.setItem("map-state", JSON.stringify(mapState));
|
||||
|
||||
if (checked) {
|
||||
addOverlayLayer(layer);
|
||||
} else {
|
||||
removeOverlayLayer(layer);
|
||||
}
|
||||
},
|
||||
state: mapState,
|
||||
});
|
||||
|
||||
map.addControl(
|
||||
new M.NavigationControl({ visualizePitch: showTerrain }),
|
||||
);
|
||||
@@ -1176,6 +1181,26 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addOverlayLayer(layer: RadioItem) {
|
||||
map?.addSource(layer.text + "overlay-source", {
|
||||
type: "raster",
|
||||
tiles: [layer.value],
|
||||
tileSize: 256,
|
||||
});
|
||||
|
||||
map?.addLayer({
|
||||
id: layer.text + "overlay-layer",
|
||||
type: "raster",
|
||||
source: layer.text + "overlay-source",
|
||||
minzoom: 4,
|
||||
});
|
||||
}
|
||||
|
||||
function removeOverlayLayer(layer: RadioItem) {
|
||||
map?.removeLayer(layer.text + "overlay-layer");
|
||||
map?.removeSource(layer.text + "overlay-source");
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} on:keyup={handleKeyup} />
|
||||
|
||||
Reference in New Issue
Block a user