adds layer manager
This commit is contained in:
3
web/src/lib/assets/svgs/pois/bakery.svg
Normal file
3
web/src/lib/assets/svgs/pois/bakery.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<path
|
||||||
|
fill="white"
|
||||||
|
d="M257.5 27.6c-.8-5.4-4.9-9.8-10.3-10.6c-22.1-3.1-44.6 .9-64.4 11.4l-74 39.5C89.1 78.4 73.2 94.9 63.4 115L26.7 190.6c-9.8 20.1-13 42.9-9.1 64.9l14.5 82.8c3.9 22.1 14.6 42.3 30.7 57.9l60.3 58.4c16.1 15.6 36.6 25.6 58.7 28.7l83 11.7c22.1 3.1 44.6-.9 64.4-11.4l74-39.5c19.7-10.5 35.6-27 45.4-47.2l36.7-75.5c9.8-20.1 13-42.9 9.1-64.9c-.9-5.3-5.3-9.3-10.6-10.1c-51.5-8.2-92.8-47.1-104.5-97.4c-1.8-7.6-8-13.4-15.7-14.6c-54.6-8.7-97.7-52-106.2-106.8zM208 144a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM144 336a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zm224-64a32 32 0 1 1 0 64 32 32 0 1 1 0-64z" />
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script module lang="ts">
|
<script module lang="ts">
|
||||||
export type RadioItem = {
|
export type RadioItem = {
|
||||||
text: string;
|
text: string;
|
||||||
value: string;
|
value: any;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { StyleSwitcherControlOptions } from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
|
import type { StyleSwitcherControlOptions } from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
|
||||||
import RadioGroup from "../base/radio_group.svelte";
|
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
import RadioGroup, { type RadioItem } from "../base/radio_group.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
settings: StyleSwitcherControlOptions;
|
settings: StyleSwitcherControlOptions;
|
||||||
@@ -9,9 +9,20 @@
|
|||||||
|
|
||||||
let { settings }: Props = $props();
|
let { settings }: Props = $props();
|
||||||
|
|
||||||
|
let mapState = $state(settings.state);
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
|
|
||||||
const mapStyles = $derived(settings.styles);
|
let styleItems: RadioItem[] = $derived(
|
||||||
|
Object.entries(settings.styles).map<RadioItem>(([name, style]) => ({
|
||||||
|
text: name,
|
||||||
|
value: style,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
let selectedStyle: number = $derived(
|
||||||
|
styleItems.findIndex((i) => i.text === mapState.base),
|
||||||
|
);
|
||||||
|
|
||||||
let showSwitcher: boolean = $state(false);
|
let showSwitcher: boolean = $state(false);
|
||||||
|
|
||||||
@@ -37,13 +48,13 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="absolute bg-menu-background rounded-lg px-4 py-2 mt-2 shadow-xl space-y-3"
|
class="absolute bg-menu-background rounded-lg px-4 py-2 mt-2 shadow-xl space-y-3 max-h-96 overflow-y-scroll"
|
||||||
class:hidden={!showSwitcher}
|
class:hidden={!showSwitcher}
|
||||||
style="transform: translateX(calc(-100% + 29px))"
|
style="transform: translateX(calc(-100% + 29px))"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-x-12">
|
<div class="flex items-center gap-x-12">
|
||||||
<span class="font-semibold whitespace-nowrap text-base"
|
<span class="font-semibold whitespace-nowrap text-base"
|
||||||
>Map style</span
|
>{$_("map-style")}</span
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="fa fa-close !rounded-full"
|
class="fa fa-close !rounded-full"
|
||||||
@@ -53,29 +64,55 @@
|
|||||||
</div>
|
</div>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
name="completed"
|
name="completed"
|
||||||
items={mapStyles}
|
items={styleItems}
|
||||||
bind:selected={settings.state.selectedStyle}
|
selected={selectedStyle}
|
||||||
onchange={(style) => settings.onMapStyleSwitch(settings.styles.indexOf(style), style)}
|
onchange={(style) => {
|
||||||
|
mapState.base = style.text;
|
||||||
|
settings.onchange(mapState);
|
||||||
|
}}
|
||||||
></RadioGroup>
|
></RadioGroup>
|
||||||
<hr class="border-input-border" />
|
<hr class="border-input-border" />
|
||||||
<span class="font-semibold whitespace-nowrap text-base">Layers</span>
|
<span class="font-semibold whitespace-nowrap text-base"
|
||||||
{#each settings.layers as l, i}
|
>{$_("layer", { values: { n: 2 } })}</span
|
||||||
|
>
|
||||||
|
{#each Object.keys(settings.state.overlays) as overlay}
|
||||||
<div class="flex items-center mt-2 mb-4">
|
<div class="flex items-center mt-2 mb-4">
|
||||||
<input
|
<input
|
||||||
id="{l.text}-layer-checkbox"
|
id="{overlay}-layer-checkbox"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={settings.state.selectedLayers[l.text]}
|
bind:checked={mapState.overlays[overlay]}
|
||||||
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
|
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
|
||||||
onchange={(e) => {
|
onchange={(e) => {
|
||||||
const checked = (e.target as HTMLInputElement).checked;
|
settings.onchange(mapState);
|
||||||
settings.onLayerChange(checked, l);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label for="{l.text}-layer-checkbox" class="ms-2 text-sm"
|
<label for="{overlay}-layer-checkbox" class="ms-2 text-sm"
|
||||||
>{$_(l.text)}</label
|
>{$_(overlay)}</label
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
<hr class="border-input-border" />
|
||||||
|
<p class="font-semibold whitespace-nowrap text-base">{$_("pois")}</p>
|
||||||
|
{#each Object.entries(settings.state.pois) as [category, pois]}
|
||||||
|
<p class="text-sm font-medium whitespace-nowrap">{$_(category)}</p>
|
||||||
|
{#each Object.keys(pois) as poi}
|
||||||
|
<div class="flex items-center mt-2 mb-4">
|
||||||
|
<input
|
||||||
|
id="{poi}-poi-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={mapState.pois[category][poi]}
|
||||||
|
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
|
||||||
|
onchange={(e) => {
|
||||||
|
settings.onchange(mapState);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="{poi}-layer-checkbox" class="ms-2 text-sm"
|
||||||
|
>{$_(poi)}</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import directionCaret from "$lib/assets/svgs/caret-right-solid.svg";
|
import directionCaret from "$lib/assets/svgs/caret-right-solid.svg";
|
||||||
import type { Settings } from "$lib/models/settings";
|
|
||||||
import type { Trail } from "$lib/models/trail";
|
import type { Trail } from "$lib/models/trail";
|
||||||
import type { Waypoint } from "$lib/models/waypoint";
|
import type { Waypoint } from "$lib/models/waypoint";
|
||||||
import { theme } from "$lib/stores/theme_store";
|
import { theme } from "$lib/stores/theme_store";
|
||||||
@@ -9,15 +8,19 @@
|
|||||||
import { findStartAndEndPoints } from "$lib/util/geojson_util";
|
import { findStartAndEndPoints } from "$lib/util/geojson_util";
|
||||||
import { toGeoJson } from "$lib/util/gpx_util";
|
import { toGeoJson } from "$lib/util/gpx_util";
|
||||||
import {
|
import {
|
||||||
baseMapStyles,
|
|
||||||
createMarkerFromWaypoint,
|
createMarkerFromWaypoint,
|
||||||
createPopupFromTrail,
|
createPopupFromTrail,
|
||||||
FontawesomeMarker,
|
FontawesomeMarker,
|
||||||
overlayLayers,
|
|
||||||
} from "$lib/util/maplibre_util";
|
} from "$lib/util/maplibre_util";
|
||||||
import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
|
import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
|
||||||
import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control";
|
import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control";
|
||||||
import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule";
|
import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule";
|
||||||
|
import {
|
||||||
|
baseMapStyles,
|
||||||
|
overlays,
|
||||||
|
pois,
|
||||||
|
} from "$lib/vendor/maplibre-layer-manager/layers";
|
||||||
|
import { LayerManager } from "$lib/vendor/maplibre-layer-manager/maplibre-layer-manager";
|
||||||
import {
|
import {
|
||||||
StyleSwitcherControl,
|
StyleSwitcherControl,
|
||||||
type StyleSwitcherControlOptions,
|
type StyleSwitcherControlOptions,
|
||||||
@@ -98,7 +101,7 @@
|
|||||||
let epc: ElevationProfileControl;
|
let epc: ElevationProfileControl;
|
||||||
let graticule: MaplibreGraticule;
|
let graticule: MaplibreGraticule;
|
||||||
|
|
||||||
let mapState: StyleSwitcherControlOptions["state"];
|
let layerManager: LayerManager;
|
||||||
|
|
||||||
let layers: Record<
|
let layers: Record<
|
||||||
string,
|
string,
|
||||||
@@ -245,16 +248,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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 (
|
if (
|
||||||
!drawing &&
|
!drawing &&
|
||||||
fitBounds !== "off" &&
|
fitBounds !== "off" &&
|
||||||
@@ -874,30 +867,17 @@
|
|||||||
)
|
)
|
||||||
).ElevationProfileControl;
|
).ElevationProfileControl;
|
||||||
|
|
||||||
const mapStyles: { text: string; value: string; thumbnail?: string }[] =
|
|
||||||
[
|
|
||||||
...((page.data.settings as Settings)?.tilesets ?? []).map(
|
|
||||||
(t) => ({
|
|
||||||
text: t.name,
|
|
||||||
value: t.url,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
...baseMapStyles,
|
|
||||||
];
|
|
||||||
|
|
||||||
mapState = JSON.parse(
|
|
||||||
localStorage.getItem("map-state") ??
|
|
||||||
'{"selectedLayers": {}, "selectedStyle": 0}',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!mapContainer) {
|
if (!mapContainer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const tileset of page.data.settings?.tilesets ?? []) {
|
||||||
|
baseMapStyles[tileset.name] = tileset.url;
|
||||||
|
}
|
||||||
|
|
||||||
const finalMapOptions: M.MapOptions = {
|
const finalMapOptions: M.MapOptions = {
|
||||||
...{
|
...{
|
||||||
container: mapContainer,
|
container: mapContainer,
|
||||||
style: mapStyles[mapState.selectedStyle].value,
|
|
||||||
center: [initialState.lng, initialState.lat],
|
center: [initialState.lng, initialState.lat],
|
||||||
zoom: initialState.zoom,
|
zoom: initialState.zoom,
|
||||||
},
|
},
|
||||||
@@ -905,6 +885,8 @@
|
|||||||
};
|
};
|
||||||
map = new M.Map(finalMapOptions);
|
map = new M.Map(finalMapOptions);
|
||||||
|
|
||||||
|
layerManager = new LayerManager(map);
|
||||||
|
|
||||||
elevationMarker = new FontawesomeMarker(
|
elevationMarker = new FontawesomeMarker(
|
||||||
{
|
{
|
||||||
id: "elevation-marker",
|
id: "elevation-marker",
|
||||||
@@ -924,25 +906,11 @@
|
|||||||
img.src = directionCaret;
|
img.src = directionCaret;
|
||||||
|
|
||||||
const switcherControl = new StyleSwitcherControl({
|
const switcherControl = new StyleSwitcherControl({
|
||||||
styles: mapStyles,
|
styles: baseMapStyles,
|
||||||
layers: overlayLayers,
|
onchange: (state) => {
|
||||||
onMapStyleSwitch: (i, style) => {
|
layerManager.update(JSON.parse(JSON.stringify(state)));
|
||||||
layers = {};
|
|
||||||
map?.setStyle(style.value);
|
|
||||||
mapState.selectedStyle = i;
|
|
||||||
localStorage.setItem("map-state", JSON.stringify(mapState));
|
|
||||||
},
|
},
|
||||||
onLayerChange: (checked, layer) => {
|
state: JSON.parse(JSON.stringify(layerManager.state)),
|
||||||
mapState.selectedLayers[layer.text] = checked;
|
|
||||||
localStorage.setItem("map-state", JSON.stringify(mapState));
|
|
||||||
|
|
||||||
if (checked) {
|
|
||||||
addOverlayLayer(layer);
|
|
||||||
} else {
|
|
||||||
removeOverlayLayer(layer);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
state: mapState,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
map.addControl(
|
map.addControl(
|
||||||
@@ -1128,6 +1096,7 @@
|
|||||||
|
|
||||||
map.on("load", () => {
|
map.on("load", () => {
|
||||||
initMap(true);
|
initMap(true);
|
||||||
|
layerManager.init();
|
||||||
oninit?.(map!);
|
oninit?.(map!);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1181,26 +1150,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeydown} on:keyup={handleKeyup} />
|
<svelte:window on:keydown={handleKeydown} on:keyup={handleKeyup} />
|
||||||
|
|||||||
@@ -190,6 +190,7 @@
|
|||||||
"joined": "Beigetreten",
|
"joined": "Beigetreten",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"latitude": "Breitengrad",
|
"latitude": "Breitengrad",
|
||||||
|
"layer": "{n, plural, =1 {Ebene} other {Ebenen}}",
|
||||||
"license": "Lizenz",
|
"license": "Lizenz",
|
||||||
"like-status": "\"Gefällt mir\" Status",
|
"like-status": "\"Gefällt mir\" Status",
|
||||||
"liked": "Gefällt mir",
|
"liked": "Gefällt mir",
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
"make-one": "Neues erstellen!",
|
"make-one": "Neues erstellen!",
|
||||||
"make-thumbnail": "Thumbnail festlegen",
|
"make-thumbnail": "Thumbnail festlegen",
|
||||||
"map": "Karte",
|
"map": "Karte",
|
||||||
|
"map-style": "Map style",
|
||||||
"max-hiking-difficulty": "Max. Schwierigkeit der Wanderung",
|
"max-hiking-difficulty": "Max. Schwierigkeit der Wanderung",
|
||||||
"metric": "Metrisch",
|
"metric": "Metrisch",
|
||||||
"moderate": "Mittel",
|
"moderate": "Mittel",
|
||||||
@@ -278,6 +280,7 @@
|
|||||||
"pick-a-trail": "Route auswählen",
|
"pick-a-trail": "Route auswählen",
|
||||||
"planned-a-trail": "hat eine Route geplant",
|
"planned-a-trail": "hat eine Route geplant",
|
||||||
"planned-tours": "Geplante Touren",
|
"planned-tours": "Geplante Touren",
|
||||||
|
"pois": "Interessante Orte",
|
||||||
"polish": "Polnisch",
|
"polish": "Polnisch",
|
||||||
"portuguese": "Portugiesisch",
|
"portuguese": "Portugiesisch",
|
||||||
"print": "Drucken",
|
"print": "Drucken",
|
||||||
|
|||||||
@@ -190,6 +190,7 @@
|
|||||||
"joined": "Joined",
|
"joined": "Joined",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
|
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
||||||
"license": "License",
|
"license": "License",
|
||||||
"like-status": "Like Status",
|
"like-status": "Like Status",
|
||||||
"liked": "Liked",
|
"liked": "Liked",
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
"make-one": "Make one!",
|
"make-one": "Make one!",
|
||||||
"make-thumbnail": "Make thumbnail",
|
"make-thumbnail": "Make thumbnail",
|
||||||
"map": "Map",
|
"map": "Map",
|
||||||
|
"map-style": "Map style",
|
||||||
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
||||||
"metric": "Metric",
|
"metric": "Metric",
|
||||||
"moderate": "Moderate",
|
"moderate": "Moderate",
|
||||||
@@ -278,6 +280,7 @@
|
|||||||
"pick-a-trail": "Pick a trail",
|
"pick-a-trail": "Pick a trail",
|
||||||
"planned-a-trail": "planned a trail",
|
"planned-a-trail": "planned a trail",
|
||||||
"planned-tours": "Planned tours",
|
"planned-tours": "Planned tours",
|
||||||
|
"pois": "POIs",
|
||||||
"polish": "Polish",
|
"polish": "Polish",
|
||||||
"portuguese": "Portuguese",
|
"portuguese": "Portuguese",
|
||||||
"print": "Print",
|
"print": "Print",
|
||||||
|
|||||||
@@ -190,6 +190,7 @@
|
|||||||
"joined": "Afiliado",
|
"joined": "Afiliado",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"latitude": "Latitud",
|
"latitude": "Latitud",
|
||||||
|
"layer": "",
|
||||||
"license": "Licencia",
|
"license": "Licencia",
|
||||||
"like-status": "Like Status",
|
"like-status": "Like Status",
|
||||||
"liked": "Liked",
|
"liked": "Liked",
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
"make-one": "¡Crea uno!",
|
"make-one": "¡Crea uno!",
|
||||||
"make-thumbnail": "Generar miniaturas",
|
"make-thumbnail": "Generar miniaturas",
|
||||||
"map": "Mapa",
|
"map": "Mapa",
|
||||||
|
"map-style": "",
|
||||||
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
||||||
"metric": "Métrica",
|
"metric": "Métrica",
|
||||||
"moderate": "Medio",
|
"moderate": "Medio",
|
||||||
@@ -278,6 +280,7 @@
|
|||||||
"pick-a-trail": "Escoge una ruta",
|
"pick-a-trail": "Escoge una ruta",
|
||||||
"planned-a-trail": "planificada una ruta",
|
"planned-a-trail": "planificada una ruta",
|
||||||
"planned-tours": "Planned tours",
|
"planned-tours": "Planned tours",
|
||||||
|
"pois": "",
|
||||||
"polish": "Polaco",
|
"polish": "Polaco",
|
||||||
"portuguese": "Portugués",
|
"portuguese": "Portugués",
|
||||||
"print": "Imprimir",
|
"print": "Imprimir",
|
||||||
|
|||||||
@@ -190,6 +190,7 @@
|
|||||||
"joined": "Joined",
|
"joined": "Joined",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
|
"layer": "",
|
||||||
"license": "License",
|
"license": "License",
|
||||||
"like-status": "Like Status",
|
"like-status": "Like Status",
|
||||||
"liked": "Liked",
|
"liked": "Liked",
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
"make-one": "Make one!",
|
"make-one": "Make one!",
|
||||||
"make-thumbnail": "Make thumbnail",
|
"make-thumbnail": "Make thumbnail",
|
||||||
"map": "Map",
|
"map": "Map",
|
||||||
|
"map-style": "",
|
||||||
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
||||||
"metric": "Metric",
|
"metric": "Metric",
|
||||||
"moderate": "Moderate",
|
"moderate": "Moderate",
|
||||||
@@ -278,6 +280,7 @@
|
|||||||
"pick-a-trail": "Pick a trail",
|
"pick-a-trail": "Pick a trail",
|
||||||
"planned-a-trail": "planned a trail",
|
"planned-a-trail": "planned a trail",
|
||||||
"planned-tours": "Planned tours",
|
"planned-tours": "Planned tours",
|
||||||
|
"pois": "",
|
||||||
"polish": "Polish",
|
"polish": "Polish",
|
||||||
"portuguese": "Portuguese",
|
"portuguese": "Portuguese",
|
||||||
"print": "Print",
|
"print": "Print",
|
||||||
|
|||||||
@@ -190,6 +190,7 @@
|
|||||||
"joined": "Rejoint",
|
"joined": "Rejoint",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
|
"layer": "",
|
||||||
"license": "Licence",
|
"license": "Licence",
|
||||||
"like-status": "Like Status",
|
"like-status": "Like Status",
|
||||||
"liked": "Aimé",
|
"liked": "Aimé",
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
"make-one": "Faites-en un !",
|
"make-one": "Faites-en un !",
|
||||||
"make-thumbnail": "Créer une miniature",
|
"make-thumbnail": "Créer une miniature",
|
||||||
"map": "Carte",
|
"map": "Carte",
|
||||||
|
"map-style": "",
|
||||||
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
||||||
"metric": "Métrique",
|
"metric": "Métrique",
|
||||||
"moderate": "Moyenne",
|
"moderate": "Moyenne",
|
||||||
@@ -278,6 +280,7 @@
|
|||||||
"pick-a-trail": "Choisir un itinéraire",
|
"pick-a-trail": "Choisir un itinéraire",
|
||||||
"planned-a-trail": "a plannifié un itinéraire",
|
"planned-a-trail": "a plannifié un itinéraire",
|
||||||
"planned-tours": "Tournées planifiées",
|
"planned-tours": "Tournées planifiées",
|
||||||
|
"pois": "",
|
||||||
"polish": "polonais",
|
"polish": "polonais",
|
||||||
"portuguese": "Portugais",
|
"portuguese": "Portugais",
|
||||||
"print": "Imprimer",
|
"print": "Imprimer",
|
||||||
|
|||||||
@@ -190,6 +190,7 @@
|
|||||||
"joined": "Joined",
|
"joined": "Joined",
|
||||||
"language": "Nyelf",
|
"language": "Nyelf",
|
||||||
"latitude": "Szélesség",
|
"latitude": "Szélesség",
|
||||||
|
"layer": "",
|
||||||
"license": "License",
|
"license": "License",
|
||||||
"like-status": "Like Status",
|
"like-status": "Like Status",
|
||||||
"liked": "Liked",
|
"liked": "Liked",
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
"make-one": "Készítsen egyet!",
|
"make-one": "Készítsen egyet!",
|
||||||
"make-thumbnail": "Készítsen miniatűrképet",
|
"make-thumbnail": "Készítsen miniatűrképet",
|
||||||
"map": "Térkép",
|
"map": "Térkép",
|
||||||
|
"map-style": "",
|
||||||
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
||||||
"metric": "Metrikus",
|
"metric": "Metrikus",
|
||||||
"moderate": "Mérsékelt",
|
"moderate": "Mérsékelt",
|
||||||
@@ -278,6 +280,7 @@
|
|||||||
"pick-a-trail": "Válasszon útvonalat",
|
"pick-a-trail": "Válasszon útvonalat",
|
||||||
"planned-a-trail": "planned a trail",
|
"planned-a-trail": "planned a trail",
|
||||||
"planned-tours": "Planned tours",
|
"planned-tours": "Planned tours",
|
||||||
|
"pois": "",
|
||||||
"polish": "Lengyel",
|
"polish": "Lengyel",
|
||||||
"portuguese": "Portugál",
|
"portuguese": "Portugál",
|
||||||
"print": "Print",
|
"print": "Print",
|
||||||
|
|||||||
@@ -190,6 +190,7 @@
|
|||||||
"joined": "Aggiunto",
|
"joined": "Aggiunto",
|
||||||
"language": "Lingua",
|
"language": "Lingua",
|
||||||
"latitude": "Latitudine",
|
"latitude": "Latitudine",
|
||||||
|
"layer": "",
|
||||||
"license": "Licenza",
|
"license": "Licenza",
|
||||||
"like-status": "Like Status",
|
"like-status": "Like Status",
|
||||||
"liked": "Liked",
|
"liked": "Liked",
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
"make-one": "Creane uno!",
|
"make-one": "Creane uno!",
|
||||||
"make-thumbnail": "Imposta miniatura",
|
"make-thumbnail": "Imposta miniatura",
|
||||||
"map": "Mappa",
|
"map": "Mappa",
|
||||||
|
"map-style": "",
|
||||||
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
||||||
"metric": "Metrico",
|
"metric": "Metrico",
|
||||||
"moderate": "Moderato",
|
"moderate": "Moderato",
|
||||||
@@ -278,6 +280,7 @@
|
|||||||
"pick-a-trail": "Scegli un percorso",
|
"pick-a-trail": "Scegli un percorso",
|
||||||
"planned-a-trail": "percorso pianificato",
|
"planned-a-trail": "percorso pianificato",
|
||||||
"planned-tours": "Planned tours",
|
"planned-tours": "Planned tours",
|
||||||
|
"pois": "",
|
||||||
"polish": "Polacco",
|
"polish": "Polacco",
|
||||||
"portuguese": "Portoghese",
|
"portuguese": "Portoghese",
|
||||||
"print": "Stampa",
|
"print": "Stampa",
|
||||||
|
|||||||
@@ -190,6 +190,7 @@
|
|||||||
"joined": "Aangesloten",
|
"joined": "Aangesloten",
|
||||||
"language": "Taal",
|
"language": "Taal",
|
||||||
"latitude": "Breedtegraad",
|
"latitude": "Breedtegraad",
|
||||||
|
"layer": "",
|
||||||
"license": "Licentie",
|
"license": "Licentie",
|
||||||
"like-status": "",
|
"like-status": "",
|
||||||
"liked": "Leuk gevonden",
|
"liked": "Leuk gevonden",
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
"make-one": "Maak er een aan!",
|
"make-one": "Maak er een aan!",
|
||||||
"make-thumbnail": "Miniatuur maken",
|
"make-thumbnail": "Miniatuur maken",
|
||||||
"map": "Kaart",
|
"map": "Kaart",
|
||||||
|
"map-style": "",
|
||||||
"max-hiking-difficulty": "Max. Hiking Moeilijkheid",
|
"max-hiking-difficulty": "Max. Hiking Moeilijkheid",
|
||||||
"metric": "Metrisch",
|
"metric": "Metrisch",
|
||||||
"moderate": "Gemiddeld",
|
"moderate": "Gemiddeld",
|
||||||
@@ -278,6 +280,7 @@
|
|||||||
"pick-a-trail": "Kies een Route",
|
"pick-a-trail": "Kies een Route",
|
||||||
"planned-a-trail": "een route gepland",
|
"planned-a-trail": "een route gepland",
|
||||||
"planned-tours": "Geplande tochten",
|
"planned-tours": "Geplande tochten",
|
||||||
|
"pois": "",
|
||||||
"polish": "Pools",
|
"polish": "Pools",
|
||||||
"portuguese": "Portugees",
|
"portuguese": "Portugees",
|
||||||
"print": "Afdrukken",
|
"print": "Afdrukken",
|
||||||
|
|||||||
@@ -190,6 +190,7 @@
|
|||||||
"joined": "Dołączono",
|
"joined": "Dołączono",
|
||||||
"language": "Język",
|
"language": "Język",
|
||||||
"latitude": "Szerokość",
|
"latitude": "Szerokość",
|
||||||
|
"layer": "",
|
||||||
"license": "Licencja",
|
"license": "Licencja",
|
||||||
"like-status": "Status polubień",
|
"like-status": "Status polubień",
|
||||||
"liked": "Polubiony",
|
"liked": "Polubiony",
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
"make-one": "Stwórz ją!",
|
"make-one": "Stwórz ją!",
|
||||||
"make-thumbnail": "Zrób miniaturkę",
|
"make-thumbnail": "Zrób miniaturkę",
|
||||||
"map": "Mapa",
|
"map": "Mapa",
|
||||||
|
"map-style": "",
|
||||||
"max-hiking-difficulty": "Maksymalny poziom trudności wędrówki",
|
"max-hiking-difficulty": "Maksymalny poziom trudności wędrówki",
|
||||||
"metric": "Metryczne",
|
"metric": "Metryczne",
|
||||||
"moderate": "Średni",
|
"moderate": "Średni",
|
||||||
@@ -278,6 +280,7 @@
|
|||||||
"pick-a-trail": "Wybierz szlak",
|
"pick-a-trail": "Wybierz szlak",
|
||||||
"planned-a-trail": "zaplanowano szlak",
|
"planned-a-trail": "zaplanowano szlak",
|
||||||
"planned-tours": "Planowane trasy",
|
"planned-tours": "Planowane trasy",
|
||||||
|
"pois": "",
|
||||||
"polish": "Polski",
|
"polish": "Polski",
|
||||||
"portuguese": "Portugalski",
|
"portuguese": "Portugalski",
|
||||||
"print": "Drukuj",
|
"print": "Drukuj",
|
||||||
|
|||||||
@@ -190,6 +190,7 @@
|
|||||||
"joined": "Joined",
|
"joined": "Joined",
|
||||||
"language": "Língua",
|
"language": "Língua",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
|
"layer": "",
|
||||||
"license": "Licença",
|
"license": "Licença",
|
||||||
"like-status": "Like Status",
|
"like-status": "Like Status",
|
||||||
"liked": "Liked",
|
"liked": "Liked",
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
"make-one": "Faz um!",
|
"make-one": "Faz um!",
|
||||||
"make-thumbnail": "Faça miniatura",
|
"make-thumbnail": "Faça miniatura",
|
||||||
"map": "Mapa",
|
"map": "Mapa",
|
||||||
|
"map-style": "",
|
||||||
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
||||||
"metric": "Métrica",
|
"metric": "Métrica",
|
||||||
"moderate": "Moderado",
|
"moderate": "Moderado",
|
||||||
@@ -278,6 +280,7 @@
|
|||||||
"pick-a-trail": "Escolha uma trilha",
|
"pick-a-trail": "Escolha uma trilha",
|
||||||
"planned-a-trail": "planned a trail",
|
"planned-a-trail": "planned a trail",
|
||||||
"planned-tours": "Planned tours",
|
"planned-tours": "Planned tours",
|
||||||
|
"pois": "",
|
||||||
"polish": "Polonês",
|
"polish": "Polonês",
|
||||||
"portuguese": "Português",
|
"portuguese": "Português",
|
||||||
"print": "Imprimir",
|
"print": "Imprimir",
|
||||||
|
|||||||
@@ -190,6 +190,7 @@
|
|||||||
"joined": "Зарегистрирован",
|
"joined": "Зарегистрирован",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"latitude": "Широта",
|
"latitude": "Широта",
|
||||||
|
"layer": "",
|
||||||
"license": "Лицензия",
|
"license": "Лицензия",
|
||||||
"like-status": "Статус лайка",
|
"like-status": "Статус лайка",
|
||||||
"liked": "Лайкнуто",
|
"liked": "Лайкнуто",
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
"make-one": "Создайте!",
|
"make-one": "Создайте!",
|
||||||
"make-thumbnail": "Сделать миниатюру",
|
"make-thumbnail": "Сделать миниатюру",
|
||||||
"map": "Карта",
|
"map": "Карта",
|
||||||
|
"map-style": "",
|
||||||
"max-hiking-difficulty": "Макс. сложность",
|
"max-hiking-difficulty": "Макс. сложность",
|
||||||
"metric": "Метрическая",
|
"metric": "Метрическая",
|
||||||
"moderate": "Средний",
|
"moderate": "Средний",
|
||||||
@@ -278,6 +280,7 @@
|
|||||||
"pick-a-trail": "Выберите трек",
|
"pick-a-trail": "Выберите трек",
|
||||||
"planned-a-trail": "запланировал(а) трек",
|
"planned-a-trail": "запланировал(а) трек",
|
||||||
"planned-tours": "Запланированные маршруты",
|
"planned-tours": "Запланированные маршруты",
|
||||||
|
"pois": "",
|
||||||
"polish": "Польский",
|
"polish": "Польский",
|
||||||
"portuguese": "Португальский",
|
"portuguese": "Португальский",
|
||||||
"print": "Печать",
|
"print": "Печать",
|
||||||
|
|||||||
@@ -190,6 +190,7 @@
|
|||||||
"joined": "Joined",
|
"joined": "Joined",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"latitude": "维度",
|
"latitude": "维度",
|
||||||
|
"layer": "",
|
||||||
"license": "开源协议",
|
"license": "开源协议",
|
||||||
"like-status": "Like Status",
|
"like-status": "Like Status",
|
||||||
"liked": "Liked",
|
"liked": "Liked",
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
"make-one": "立刻注册!",
|
"make-one": "立刻注册!",
|
||||||
"make-thumbnail": "生成缩略图",
|
"make-thumbnail": "生成缩略图",
|
||||||
"map": "地图",
|
"map": "地图",
|
||||||
|
"map-style": "",
|
||||||
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
||||||
"metric": "公制",
|
"metric": "公制",
|
||||||
"moderate": "中等",
|
"moderate": "中等",
|
||||||
@@ -278,6 +280,7 @@
|
|||||||
"pick-a-trail": "选择一个路线",
|
"pick-a-trail": "选择一个路线",
|
||||||
"planned-a-trail": "planned a trail",
|
"planned-a-trail": "planned a trail",
|
||||||
"planned-tours": "Planned tours",
|
"planned-tours": "Planned tours",
|
||||||
|
"pois": "",
|
||||||
"polish": "波兰语",
|
"polish": "波兰语",
|
||||||
"portuguese": "葡萄牙语",
|
"portuguese": "葡萄牙语",
|
||||||
"print": "打印",
|
"print": "打印",
|
||||||
|
|||||||
@@ -11,43 +11,6 @@ import { handleFromRecordWithIRI } from "./activitypub_util";
|
|||||||
import { getFileURL } from "./file_util";
|
import { getFileURL } from "./file_util";
|
||||||
import { formatDistance, formatElevation, formatTimeHHMM } from "./format_util";
|
import { formatDistance, formatElevation, formatTimeHHMM } from "./format_util";
|
||||||
import { icons } from "./icon_util";
|
import { icons } from "./icon_util";
|
||||||
import type { RadioItem } from "$lib/components/base/radio_group.svelte";
|
|
||||||
|
|
||||||
|
|
||||||
export const baseMapStyles: RadioItem[] = [
|
|
||||||
{
|
|
||||||
text: "OpenFreeMap",
|
|
||||||
value: "/styles/ofm.json",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "OpenTopoMap",
|
|
||||||
value: "/styles/otm.json",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Carto Light",
|
|
||||||
value: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Carto Dark",
|
|
||||||
value: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const overlayLayers: RadioItem[] = [
|
|
||||||
{
|
|
||||||
text: "hiking",
|
|
||||||
value: "https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "cycling",
|
|
||||||
value: "https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "MTB",
|
|
||||||
value: "https://tile.waymarkedtrails.org/mtb/{z}/{x}/{y}.png",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
export class FontawesomeMarker extends M.Marker {
|
export class FontawesomeMarker extends M.Marker {
|
||||||
constructor(options: { icon: string, fontSize?: string, width?: number, backgroundColor?: string, fontColor?: string, id?: string }, markerOptions?: M.MarkerOptions) {
|
constructor(options: { icon: string, fontSize?: string, width?: number, backgroundColor?: string, fontColor?: string, id?: string }, markerOptions?: M.MarkerOptions) {
|
||||||
|
|||||||
249
web/src/lib/vendor/maplibre-layer-manager/layers.ts
vendored
Normal file
249
web/src/lib/vendor/maplibre-layer-manager/layers.ts
vendored
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import Bakery from "$lib/assets/svgs/pois/bakery.svg?raw"
|
||||||
|
import type { MapMouseEvent, Marker, StyleSpecification } from "maplibre-gl"
|
||||||
|
|
||||||
|
export interface BaseLayer {
|
||||||
|
markers?: Record<string, Marker>,
|
||||||
|
spec: StyleSpecification,
|
||||||
|
listener?: {
|
||||||
|
onMouseUp?: (e: MapMouseEvent) => void,
|
||||||
|
onMouseDown?: (e: MapMouseEvent) => void,
|
||||||
|
onEnter?: (e: MapMouseEvent) => void,
|
||||||
|
onLeave?: (e: MapMouseEvent) => void,
|
||||||
|
onMouseMove?: (e: MapMouseEvent) => void,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const baseMapStyles: Record<string, string | StyleSpecification> = {
|
||||||
|
"OpenFreeMap": "/styles/ofm.json",
|
||||||
|
"OpenTopoMap": {
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
openTopoMap: {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: ['https://tile.opentopomap.org/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 17,
|
||||||
|
attribution:
|
||||||
|
'© <a href="https://www.opentopomap.org" target="_blank">OpenTopoMap</a> © <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: 'openTopoMap',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'openTopoMap',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"CyclOSM": {
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
cyclOSM: {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: [
|
||||||
|
'https://a.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png',
|
||||||
|
'https://b.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png',
|
||||||
|
'https://c.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png',
|
||||||
|
],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
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: 'cyclOSM',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'cyclOSM',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"Carto Light": "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
|
||||||
|
"Carto Dark": "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const overlays: Record<string, StyleSpecification> = {
|
||||||
|
hiking: {
|
||||||
|
version: 8,
|
||||||
|
name: "waymarkedTrailsHiking",
|
||||||
|
sources: {
|
||||||
|
waymarkedTrailsHiking: {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: ['https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution:
|
||||||
|
'© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: 'waymarkedTrailsHiking',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsHiking',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cycling: {
|
||||||
|
version: 8,
|
||||||
|
name: "waymarkedTrailsCycling",
|
||||||
|
|
||||||
|
sources: {
|
||||||
|
waymarkedTrailsCycling: {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: ['https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution:
|
||||||
|
'© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: 'waymarkedTrailsCycling',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsCycling',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
MTB: {
|
||||||
|
version: 8,
|
||||||
|
name: "waymarkedTrailsMTB",
|
||||||
|
|
||||||
|
sources: {
|
||||||
|
waymarkedTrailsMTB: {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: ['https://tile.waymarkedtrails.org/mtb/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution:
|
||||||
|
'© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: 'waymarkedTrailsMTB',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsMTB',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Skiing: {
|
||||||
|
name: "waymarkedTrailsWinter",
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
waymarkedTrailsWinter: {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: ['https://tile.waymarkedtrails.org/slopes/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution:
|
||||||
|
'© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: 'waymarkedTrailsWinter',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsWinter',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type POI = { q: string, icon: { svg: any, bg: string } }
|
||||||
|
export const pois: Record<string, POI> = {
|
||||||
|
"grocery-store": { q: "nwr[shop=supermarket];nwr[shop=convenience];", icon: { svg: "", bg: "red" } },
|
||||||
|
"bakery": { q: "nwr[shop=bakery];", icon: { svg: Bakery, bg: "coral" } },
|
||||||
|
"food-drinks": { q: "nwr[amenity=restaurant];nwr[amenity=fast_food];nwr[amenity=cafe];nwr[amenity=pub];nwr[amenity=bar];", icon: { svg: "", bg: "red" } },
|
||||||
|
"toilet": { q: "nwr[amenity=toilets];", icon: { svg: "", bg: "red" } },
|
||||||
|
"drinking-water": { q: "nwr[amenity=drinking_water];nwr[amenity=water_point];nwr[natural=spring][drinking_water=yes];", icon: { svg: "", bg: "red" } },
|
||||||
|
"shower": { q: "nwr[amenity=shower];", icon: { svg: "", bg: "red" } },
|
||||||
|
"shelter": { q: "nwr[amenity=shelter];", icon: { svg: "", bg: "red" } },
|
||||||
|
"barrier": { q: "nwr[barrier=true];", icon: { svg: "", bg: "red" } },
|
||||||
|
"attraction": { q: "nwr[tourism=attraction];", icon: { svg: "", bg: "red" } },
|
||||||
|
"viewpoint": { q: "nwr[tourism=viewpoint];", icon: { svg: "", bg: "red" } },
|
||||||
|
"hotel": { q: "nwr[tourism=hotel];nwr[tourism=hostel];nwr[tourism=guest_house];nwr[tourism=motel];", icon: { svg: "", bg: "red" } },
|
||||||
|
"camp-site": { q: "nwr[tourism=camp_site];", icon: { svg: "", bg: "red" } },
|
||||||
|
"hut": { q: "nwr[tourism=alpine_hut];nwr[tourism=wilderness_hut];", icon: { svg: "", bg: "red" } },
|
||||||
|
"peak": { q: "nwr[natural=peak];", icon: { svg: "", bg: "red" } },
|
||||||
|
"mountain-pass": { q: "nwr[mountain_pass=yes];", icon: { svg: "", bg: "red" } },
|
||||||
|
"climbing": { q: "nwr[sport=climbing];", icon: { svg: "", bg: "red" } },
|
||||||
|
"bicylce-parking": { q: "nwr[amenity=bicycle_parking];", icon: { svg: "", bg: "red" } },
|
||||||
|
"bicycle-rental": { q: "nwr[amenity=bicycle_rental];", icon: { svg: "", bg: "red" } },
|
||||||
|
"bicycle-shop": { q: "nwr[shop=bicycle];", icon: { svg: "", bg: "red" } },
|
||||||
|
"gas-station": { q: "nwr[amenity=fuel];", icon: { svg: "", bg: "red" } },
|
||||||
|
"parking": { q: "nwr[amenity=parking];", icon: { svg: "", bg: "red" } },
|
||||||
|
"car-repair": { q: "nwr[shop=car_repair];", icon: { svg: "", bg: "red" } },
|
||||||
|
"motorcycle-repair": { q: "nwr[shop=motorcycle_repair];", icon: { svg: "", bg: "red" } },
|
||||||
|
"railway-station": { q: "nwr[railway=station];", icon: { svg: "", bg: "red" } },
|
||||||
|
"subway": { q: "nwr[railway=subway_entrance];", icon: { svg: "", bg: "red" } },
|
||||||
|
"tram": { q: "nwr[railway=tram_stop];", icon: { svg: "", bg: "red" } },
|
||||||
|
"bus": { q: "nwr[public_transport=stop_position][bus=yes];nwr[public_transport=platform][bus=yes];", icon: { svg: "", bg: "red" } },
|
||||||
|
"ferry": { q: "nwr[amenity=ferry_terminal];", icon: { svg: "", bg: "red" } },
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type MapState = {
|
||||||
|
base: keyof typeof baseMapStyles,
|
||||||
|
overlays: { [K in keyof typeof overlays]: boolean }
|
||||||
|
pois: Record<string, Record<string, boolean>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultMapState: MapState = {
|
||||||
|
base: "OpenFreeMap",
|
||||||
|
overlays: {
|
||||||
|
waymarkedTrailsHiking: false,
|
||||||
|
waymarkedTrailsCycling: false,
|
||||||
|
waymarkedTrailsHorseRiding: false,
|
||||||
|
waymarkedTrailsMTB: false,
|
||||||
|
waymarkedTrailsSkating: false,
|
||||||
|
waymarkedTrailsWinter: false,
|
||||||
|
},
|
||||||
|
pois: {
|
||||||
|
food: {
|
||||||
|
"food-drinks": false,
|
||||||
|
"grocery-store": false,
|
||||||
|
bakery: false,
|
||||||
|
},
|
||||||
|
tourism: {
|
||||||
|
"camp-site": false,
|
||||||
|
attraction: false,
|
||||||
|
hotel: false,
|
||||||
|
hut: false,
|
||||||
|
viewpoint: false,
|
||||||
|
},
|
||||||
|
ammenity: {
|
||||||
|
"drinking-water": false,
|
||||||
|
barrier: false,
|
||||||
|
shelter: false,
|
||||||
|
shower: false,
|
||||||
|
toilet: false,
|
||||||
|
},
|
||||||
|
hiking: {
|
||||||
|
"mountain-pass": false,
|
||||||
|
climbing: false,
|
||||||
|
peak: false,
|
||||||
|
},
|
||||||
|
cycling: {
|
||||||
|
"bicycle-rental": false,
|
||||||
|
"bicycle-shop": false,
|
||||||
|
"bicylce-parking": false,
|
||||||
|
},
|
||||||
|
"car-motorcycle": {
|
||||||
|
"car-repair": false,
|
||||||
|
"gas-station": false,
|
||||||
|
"motorcycle-repair": false,
|
||||||
|
parking: false,
|
||||||
|
},
|
||||||
|
"public-transport": {
|
||||||
|
"railway-station": false,
|
||||||
|
bus: false,
|
||||||
|
ferry: false,
|
||||||
|
subway: false,
|
||||||
|
tram: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
web/src/lib/vendor/maplibre-layer-manager/maplibre-layer-manager.ts
vendored
Normal file
144
web/src/lib/vendor/maplibre-layer-manager/maplibre-layer-manager.ts
vendored
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import * as M from "maplibre-gl";
|
||||||
|
import { baseMapStyles, defaultMapState, pois, type BaseLayer, type MapState } from "./layers";
|
||||||
|
import { OverlayLayer } from "./overlay-layer";
|
||||||
|
import { OverpassLayer } from "./overpass-layer";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export class LayerManager {
|
||||||
|
private map: M.Map;
|
||||||
|
state!: MapState;
|
||||||
|
layers: Record<string, BaseLayer> = {};
|
||||||
|
|
||||||
|
constructor(map: M.Map) {
|
||||||
|
this.map = map;
|
||||||
|
|
||||||
|
const storedMapState = localStorage.getItem("map-state")
|
||||||
|
if (storedMapState) {
|
||||||
|
this.state = JSON.parse(storedMapState)
|
||||||
|
} else {
|
||||||
|
this.state = defaultMapState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.map.on('moveend', () => { });
|
||||||
|
|
||||||
|
this.map.on('styledata', () => {
|
||||||
|
this.restoreLayers();
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
this.update(this.state, true);
|
||||||
|
|
||||||
|
const overpassLayer = new OverpassLayer()
|
||||||
|
this.addLayer("overpass", overpassLayer)
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
// map is probably not initialized yet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(newState: MapState, initialize: boolean = false) {
|
||||||
|
const oldState = this.state;
|
||||||
|
|
||||||
|
if (oldState.base != newState.base || initialize) {
|
||||||
|
this.updateBaseLayer(baseMapStyles[newState.base])
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, active] of Object.entries(newState.overlays)) {
|
||||||
|
const oldOverlayActive = oldState.overlays[name]
|
||||||
|
|
||||||
|
if (active && (!oldOverlayActive || initialize)) {
|
||||||
|
this.addLayer(name, new OverlayLayer(name))
|
||||||
|
} else if (oldOverlayActive && !active) {
|
||||||
|
this.removeLayer(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const overpassLayer = this.layers.overpass;
|
||||||
|
if (overpassLayer) {
|
||||||
|
const castedOverpassLayer = overpassLayer as OverpassLayer
|
||||||
|
castedOverpassLayer.updateLayer(newState, this.map.getBounds()).then(() => {
|
||||||
|
this.loadIcons(castedOverpassLayer.pois);
|
||||||
|
(this.map.getSource('overpass') as M.GeoJSONSource).setData(castedOverpassLayer.data);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = newState
|
||||||
|
localStorage.setItem("map-state", JSON.stringify(this.state));
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateBaseLayer(layer: string | M.StyleSpecification) {
|
||||||
|
this.map.setStyle(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private addLayer(id: string, layer: BaseLayer) {
|
||||||
|
for (const [id, s] of Object.entries(layer.spec.sources)) {
|
||||||
|
if (!this.map.getSource(id)) {
|
||||||
|
this.map.addSource(id, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const l of layer.spec.layers) {
|
||||||
|
if (!this.map.getLayer(l.id)) {
|
||||||
|
this.map.addLayer(l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.layers[id] = layer
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeLayer(id: string) {
|
||||||
|
const layer = this.layers[id];
|
||||||
|
if (!layer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const l of layer.spec.layers) {
|
||||||
|
if (this.map.getLayer(l.id)) {
|
||||||
|
this.map.removeLayer(l.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, _] of Object.entries(layer.spec.sources)) {
|
||||||
|
if (this.map.getSource(id)) {
|
||||||
|
this.map.removeSource(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete this.layers[id]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private restoreLayers() {
|
||||||
|
for (const [id, layer] of Object.entries(this.layers)) {
|
||||||
|
this.addLayer(id, layer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadIcons(activePois: string[]) {
|
||||||
|
activePois.forEach((poi) => {
|
||||||
|
if (!this.map.hasImage(`overpass-${poi}`)) {
|
||||||
|
let icon = new Image(100, 100);
|
||||||
|
icon.onload = () => {
|
||||||
|
if (!this.map.hasImage(`overpass-${poi}`)) {
|
||||||
|
this.map.addImage(`overpass-${poi}`, icon);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const svg = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||||
|
<circle cx="20" cy="20" r="20" fill="${pois[poi].icon.bg}" />
|
||||||
|
<g transform="translate(8 8) scale(0.05)">
|
||||||
|
${pois[poi].icon.svg}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
|
||||||
|
icon.src =
|
||||||
|
'data:image/svg+xml,' +
|
||||||
|
encodeURIComponent(svg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
10
web/src/lib/vendor/maplibre-layer-manager/overlay-layer.ts
vendored
Normal file
10
web/src/lib/vendor/maplibre-layer-manager/overlay-layer.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { StyleSpecification } from "maplibre-gl";
|
||||||
|
import { overlays, type BaseLayer } from "./layers";
|
||||||
|
|
||||||
|
export class OverlayLayer implements BaseLayer {
|
||||||
|
spec: StyleSpecification;
|
||||||
|
|
||||||
|
constructor(name: string) {
|
||||||
|
this.spec = overlays[name]
|
||||||
|
}
|
||||||
|
}
|
||||||
92
web/src/lib/vendor/maplibre-layer-manager/overpass-layer.ts
vendored
Normal file
92
web/src/lib/vendor/maplibre-layer-manager/overpass-layer.ts
vendored
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import type { LngLatBounds, StyleSpecification } from "maplibre-gl";
|
||||||
|
import { pois, type BaseLayer, type MapState } from "./layers";
|
||||||
|
import type { OverpassResponse } from "./types";
|
||||||
|
|
||||||
|
export class OverpassLayer implements BaseLayer {
|
||||||
|
private overpassApiURL: string = "https://overpass.private.coffee/api/interpreter"
|
||||||
|
|
||||||
|
data: GeoJSON.FeatureCollection = ({ type: 'FeatureCollection', features: [] });
|
||||||
|
pois: string[] = [];
|
||||||
|
|
||||||
|
spec: StyleSpecification = {
|
||||||
|
version: 8,
|
||||||
|
name: "overpass",
|
||||||
|
sources: {
|
||||||
|
overpass: {
|
||||||
|
type: 'geojson',
|
||||||
|
data: this.data,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: 'overpass',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'overpass',
|
||||||
|
layout: {
|
||||||
|
'icon-image': ['get', 'icon'],
|
||||||
|
'icon-size': 0.25,
|
||||||
|
'icon-padding': 0,
|
||||||
|
'icon-allow-overlap': ['step', ['zoom'], false, 14, true],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
async updateLayer(state: MapState, bounds: LngLatBounds) {
|
||||||
|
let activePOIs: string[] = []
|
||||||
|
for (const category of Object.keys(state.pois)) {
|
||||||
|
for (const [name, active] of Object.entries(state.pois[category])) {
|
||||||
|
if (active) {
|
||||||
|
activePOIs.push(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const q = this.getOverpassQuery(activePOIs, bounds)
|
||||||
|
if (!q.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const r = await fetch(`${this.overpassApiURL}?data=${q}`)
|
||||||
|
const response: OverpassResponse = await r.json();
|
||||||
|
|
||||||
|
this.updateData(response)
|
||||||
|
this.pois = activePOIs
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateData(data: OverpassResponse, ) {
|
||||||
|
let pois: GeoJSON.Feature[] = [];
|
||||||
|
|
||||||
|
if (data.elements === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let element of data.elements) {
|
||||||
|
console.log(element);
|
||||||
|
|
||||||
|
pois.push({
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: element.center
|
||||||
|
? [element.center.lon, element.center.lat]
|
||||||
|
: [element.lon, element.lat],
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
id: element.id,
|
||||||
|
lat: element.center ? element.center.lat : element.lat,
|
||||||
|
lon: element.center ? element.center.lon : element.lon,
|
||||||
|
icon: `overpass-bakery`,
|
||||||
|
tags: element.tags,
|
||||||
|
type: element.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.data.features = pois;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOverpassQuery(data: string[], bounds: LngLatBounds) {
|
||||||
|
const q = data.map(p => pois[p].q).join('')
|
||||||
|
return `[bbox:${bounds.getSouth()},${bounds.getWest()},${bounds.getNorth()},${bounds.getEast()}][out:json];(${q});out center;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
web/src/lib/vendor/maplibre-layer-manager/types.ts
vendored
Normal file
21
web/src/lib/vendor/maplibre-layer-manager/types.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export interface OverpassResponse {
|
||||||
|
elements: OverpassElement[];
|
||||||
|
generator: string;
|
||||||
|
osm3s: {
|
||||||
|
copyright: string;
|
||||||
|
timestamp_osm_base: string;
|
||||||
|
};
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
export interface OverpassElement {
|
||||||
|
id: number;
|
||||||
|
center?: {
|
||||||
|
lat: number,
|
||||||
|
lon: number,
|
||||||
|
}
|
||||||
|
lat: number;
|
||||||
|
lon: number;
|
||||||
|
nodes?: number[];
|
||||||
|
tags?: Record<string, string>;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
@@ -1,21 +1,15 @@
|
|||||||
import type { RadioItem } from "$lib/components/base/radio_group.svelte";
|
|
||||||
import StyleSwitcher from "$lib/components/map/style_switcher.svelte";
|
import StyleSwitcher from "$lib/components/map/style_switcher.svelte";
|
||||||
import { type ControlPosition, type IControl } from "maplibre-gl";
|
import { type ControlPosition, type IControl, type StyleSpecification } from "maplibre-gl";
|
||||||
import { mount } from "svelte";
|
import { mount } from "svelte";
|
||||||
|
import type { MapState, pois } from "../maplibre-layer-manager/layers";
|
||||||
/**
|
/**
|
||||||
* Style switcher control options
|
* Style switcher control options
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type StyleSwitcherControlOptions = {
|
export type StyleSwitcherControlOptions = {
|
||||||
styles: RadioItem[];
|
styles: Record<string, string | StyleSpecification>;
|
||||||
layers: RadioItem[];
|
onchange: (state: MapState) => void
|
||||||
onMapStyleSwitch: (index: number, style: RadioItem) => void
|
state: MapState;
|
||||||
onLayerChange: (checked: boolean, layer: RadioItem) => void
|
|
||||||
|
|
||||||
state: {
|
|
||||||
selectedStyle: number;
|
|
||||||
selectedLayers: Record<string, boolean>
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class StyleSwitcherControl implements IControl {
|
export class StyleSwitcherControl implements IControl {
|
||||||
|
|||||||
Reference in New Issue
Block a user