diff --git a/web/src/lib/assets/svgs/pois/bakery.svg b/web/src/lib/assets/svgs/pois/bakery.svg
new file mode 100644
index 00000000..124e97b6
--- /dev/null
+++ b/web/src/lib/assets/svgs/pois/bakery.svg
@@ -0,0 +1,3 @@
+
diff --git a/web/src/lib/components/base/radio_group.svelte b/web/src/lib/components/base/radio_group.svelte
index 2901a5c6..49beb54a 100644
--- a/web/src/lib/components/base/radio_group.svelte
+++ b/web/src/lib/components/base/radio_group.svelte
@@ -1,7 +1,7 @@
diff --git a/web/src/lib/i18n/locales/de.json b/web/src/lib/i18n/locales/de.json
index 8f93dea1..06965a85 100644
--- a/web/src/lib/i18n/locales/de.json
+++ b/web/src/lib/i18n/locales/de.json
@@ -190,6 +190,7 @@
"joined": "Beigetreten",
"language": "Sprache",
"latitude": "Breitengrad",
+ "layer": "{n, plural, =1 {Ebene} other {Ebenen}}",
"license": "Lizenz",
"like-status": "\"Gefällt mir\" Status",
"liked": "Gefällt mir",
@@ -210,6 +211,7 @@
"make-one": "Neues erstellen!",
"make-thumbnail": "Thumbnail festlegen",
"map": "Karte",
+ "map-style": "Map style",
"max-hiking-difficulty": "Max. Schwierigkeit der Wanderung",
"metric": "Metrisch",
"moderate": "Mittel",
@@ -278,6 +280,7 @@
"pick-a-trail": "Route auswählen",
"planned-a-trail": "hat eine Route geplant",
"planned-tours": "Geplante Touren",
+ "pois": "Interessante Orte",
"polish": "Polnisch",
"portuguese": "Portugiesisch",
"print": "Drucken",
diff --git a/web/src/lib/i18n/locales/en.json b/web/src/lib/i18n/locales/en.json
index 1f71e77f..5106e717 100644
--- a/web/src/lib/i18n/locales/en.json
+++ b/web/src/lib/i18n/locales/en.json
@@ -190,6 +190,7 @@
"joined": "Joined",
"language": "Language",
"latitude": "Latitude",
+ "layer": "{n, plural, =1 {Layer} other {Layers}}",
"license": "License",
"like-status": "Like Status",
"liked": "Liked",
@@ -210,6 +211,7 @@
"make-one": "Make one!",
"make-thumbnail": "Make thumbnail",
"map": "Map",
+ "map-style": "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": "POIs",
"polish": "Polish",
"portuguese": "Portuguese",
"print": "Print",
diff --git a/web/src/lib/i18n/locales/es.json b/web/src/lib/i18n/locales/es.json
index 0428f651..5b2d7e89 100644
--- a/web/src/lib/i18n/locales/es.json
+++ b/web/src/lib/i18n/locales/es.json
@@ -190,6 +190,7 @@
"joined": "Afiliado",
"language": "Idioma",
"latitude": "Latitud",
+ "layer": "",
"license": "Licencia",
"like-status": "Like Status",
"liked": "Liked",
@@ -210,6 +211,7 @@
"make-one": "¡Crea uno!",
"make-thumbnail": "Generar miniaturas",
"map": "Mapa",
+ "map-style": "",
"max-hiking-difficulty": "Max. Hiking Difficulty",
"metric": "Métrica",
"moderate": "Medio",
@@ -278,6 +280,7 @@
"pick-a-trail": "Escoge una ruta",
"planned-a-trail": "planificada una ruta",
"planned-tours": "Planned tours",
+ "pois": "",
"polish": "Polaco",
"portuguese": "Portugués",
"print": "Imprimir",
diff --git a/web/src/lib/i18n/locales/eu.json b/web/src/lib/i18n/locales/eu.json
index 4644b87a..0788e1cf 100644
--- a/web/src/lib/i18n/locales/eu.json
+++ b/web/src/lib/i18n/locales/eu.json
@@ -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",
diff --git a/web/src/lib/i18n/locales/fr.json b/web/src/lib/i18n/locales/fr.json
index 3d0ddb83..4de69763 100644
--- a/web/src/lib/i18n/locales/fr.json
+++ b/web/src/lib/i18n/locales/fr.json
@@ -190,6 +190,7 @@
"joined": "Rejoint",
"language": "Langue",
"latitude": "Latitude",
+ "layer": "",
"license": "Licence",
"like-status": "Like Status",
"liked": "Aimé",
@@ -210,6 +211,7 @@
"make-one": "Faites-en un !",
"make-thumbnail": "Créer une miniature",
"map": "Carte",
+ "map-style": "",
"max-hiking-difficulty": "Max. Hiking Difficulty",
"metric": "Métrique",
"moderate": "Moyenne",
@@ -278,6 +280,7 @@
"pick-a-trail": "Choisir un itinéraire",
"planned-a-trail": "a plannifié un itinéraire",
"planned-tours": "Tournées planifiées",
+ "pois": "",
"polish": "polonais",
"portuguese": "Portugais",
"print": "Imprimer",
diff --git a/web/src/lib/i18n/locales/hu.json b/web/src/lib/i18n/locales/hu.json
index 97c7abee..589879a7 100644
--- a/web/src/lib/i18n/locales/hu.json
+++ b/web/src/lib/i18n/locales/hu.json
@@ -190,6 +190,7 @@
"joined": "Joined",
"language": "Nyelf",
"latitude": "Szélesség",
+ "layer": "",
"license": "License",
"like-status": "Like Status",
"liked": "Liked",
@@ -210,6 +211,7 @@
"make-one": "Készítsen egyet!",
"make-thumbnail": "Készítsen miniatűrképet",
"map": "Térkép",
+ "map-style": "",
"max-hiking-difficulty": "Max. Hiking Difficulty",
"metric": "Metrikus",
"moderate": "Mérsékelt",
@@ -278,6 +280,7 @@
"pick-a-trail": "Válasszon útvonalat",
"planned-a-trail": "planned a trail",
"planned-tours": "Planned tours",
+ "pois": "",
"polish": "Lengyel",
"portuguese": "Portugál",
"print": "Print",
diff --git a/web/src/lib/i18n/locales/it.json b/web/src/lib/i18n/locales/it.json
index 9f59b137..00799471 100644
--- a/web/src/lib/i18n/locales/it.json
+++ b/web/src/lib/i18n/locales/it.json
@@ -190,6 +190,7 @@
"joined": "Aggiunto",
"language": "Lingua",
"latitude": "Latitudine",
+ "layer": "",
"license": "Licenza",
"like-status": "Like Status",
"liked": "Liked",
@@ -210,6 +211,7 @@
"make-one": "Creane uno!",
"make-thumbnail": "Imposta miniatura",
"map": "Mappa",
+ "map-style": "",
"max-hiking-difficulty": "Max. Hiking Difficulty",
"metric": "Metrico",
"moderate": "Moderato",
@@ -278,6 +280,7 @@
"pick-a-trail": "Scegli un percorso",
"planned-a-trail": "percorso pianificato",
"planned-tours": "Planned tours",
+ "pois": "",
"polish": "Polacco",
"portuguese": "Portoghese",
"print": "Stampa",
diff --git a/web/src/lib/i18n/locales/nl.json b/web/src/lib/i18n/locales/nl.json
index 8b43c4f7..8fc5e0b0 100644
--- a/web/src/lib/i18n/locales/nl.json
+++ b/web/src/lib/i18n/locales/nl.json
@@ -190,6 +190,7 @@
"joined": "Aangesloten",
"language": "Taal",
"latitude": "Breedtegraad",
+ "layer": "",
"license": "Licentie",
"like-status": "",
"liked": "Leuk gevonden",
@@ -210,6 +211,7 @@
"make-one": "Maak er een aan!",
"make-thumbnail": "Miniatuur maken",
"map": "Kaart",
+ "map-style": "",
"max-hiking-difficulty": "Max. Hiking Moeilijkheid",
"metric": "Metrisch",
"moderate": "Gemiddeld",
@@ -278,6 +280,7 @@
"pick-a-trail": "Kies een Route",
"planned-a-trail": "een route gepland",
"planned-tours": "Geplande tochten",
+ "pois": "",
"polish": "Pools",
"portuguese": "Portugees",
"print": "Afdrukken",
diff --git a/web/src/lib/i18n/locales/pl.json b/web/src/lib/i18n/locales/pl.json
index ab7eedb7..a6431994 100644
--- a/web/src/lib/i18n/locales/pl.json
+++ b/web/src/lib/i18n/locales/pl.json
@@ -190,6 +190,7 @@
"joined": "Dołączono",
"language": "Język",
"latitude": "Szerokość",
+ "layer": "",
"license": "Licencja",
"like-status": "Status polubień",
"liked": "Polubiony",
@@ -210,6 +211,7 @@
"make-one": "Stwórz ją!",
"make-thumbnail": "Zrób miniaturkę",
"map": "Mapa",
+ "map-style": "",
"max-hiking-difficulty": "Maksymalny poziom trudności wędrówki",
"metric": "Metryczne",
"moderate": "Średni",
@@ -278,6 +280,7 @@
"pick-a-trail": "Wybierz szlak",
"planned-a-trail": "zaplanowano szlak",
"planned-tours": "Planowane trasy",
+ "pois": "",
"polish": "Polski",
"portuguese": "Portugalski",
"print": "Drukuj",
diff --git a/web/src/lib/i18n/locales/pt.json b/web/src/lib/i18n/locales/pt.json
index 28a46c9c..b2603292 100644
--- a/web/src/lib/i18n/locales/pt.json
+++ b/web/src/lib/i18n/locales/pt.json
@@ -190,6 +190,7 @@
"joined": "Joined",
"language": "Língua",
"latitude": "Latitude",
+ "layer": "",
"license": "Licença",
"like-status": "Like Status",
"liked": "Liked",
@@ -210,6 +211,7 @@
"make-one": "Faz um!",
"make-thumbnail": "Faça miniatura",
"map": "Mapa",
+ "map-style": "",
"max-hiking-difficulty": "Max. Hiking Difficulty",
"metric": "Métrica",
"moderate": "Moderado",
@@ -278,6 +280,7 @@
"pick-a-trail": "Escolha uma trilha",
"planned-a-trail": "planned a trail",
"planned-tours": "Planned tours",
+ "pois": "",
"polish": "Polonês",
"portuguese": "Português",
"print": "Imprimir",
diff --git a/web/src/lib/i18n/locales/ru.json b/web/src/lib/i18n/locales/ru.json
index dd136105..5cf19100 100644
--- a/web/src/lib/i18n/locales/ru.json
+++ b/web/src/lib/i18n/locales/ru.json
@@ -190,6 +190,7 @@
"joined": "Зарегистрирован",
"language": "Язык",
"latitude": "Широта",
+ "layer": "",
"license": "Лицензия",
"like-status": "Статус лайка",
"liked": "Лайкнуто",
@@ -210,6 +211,7 @@
"make-one": "Создайте!",
"make-thumbnail": "Сделать миниатюру",
"map": "Карта",
+ "map-style": "",
"max-hiking-difficulty": "Макс. сложность",
"metric": "Метрическая",
"moderate": "Средний",
@@ -278,6 +280,7 @@
"pick-a-trail": "Выберите трек",
"planned-a-trail": "запланировал(а) трек",
"planned-tours": "Запланированные маршруты",
+ "pois": "",
"polish": "Польский",
"portuguese": "Португальский",
"print": "Печать",
diff --git a/web/src/lib/i18n/locales/zh.json b/web/src/lib/i18n/locales/zh.json
index aa82851b..e3973812 100644
--- a/web/src/lib/i18n/locales/zh.json
+++ b/web/src/lib/i18n/locales/zh.json
@@ -190,6 +190,7 @@
"joined": "Joined",
"language": "语言",
"latitude": "维度",
+ "layer": "",
"license": "开源协议",
"like-status": "Like Status",
"liked": "Liked",
@@ -210,6 +211,7 @@
"make-one": "立刻注册!",
"make-thumbnail": "生成缩略图",
"map": "地图",
+ "map-style": "",
"max-hiking-difficulty": "Max. Hiking Difficulty",
"metric": "公制",
"moderate": "中等",
@@ -278,6 +280,7 @@
"pick-a-trail": "选择一个路线",
"planned-a-trail": "planned a trail",
"planned-tours": "Planned tours",
+ "pois": "",
"polish": "波兰语",
"portuguese": "葡萄牙语",
"print": "打印",
diff --git a/web/src/lib/util/maplibre_util.ts b/web/src/lib/util/maplibre_util.ts
index b4003187..5ac195c0 100644
--- a/web/src/lib/util/maplibre_util.ts
+++ b/web/src/lib/util/maplibre_util.ts
@@ -11,43 +11,6 @@ import { handleFromRecordWithIRI } from "./activitypub_util";
import { getFileURL } from "./file_util";
import { formatDistance, formatElevation, formatTimeHHMM } from "./format_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 {
constructor(options: { icon: string, fontSize?: string, width?: number, backgroundColor?: string, fontColor?: string, id?: string }, markerOptions?: M.MarkerOptions) {
diff --git a/web/src/lib/vendor/maplibre-layer-manager/layers.ts b/web/src/lib/vendor/maplibre-layer-manager/layers.ts
new file mode 100644
index 00000000..638ff34e
--- /dev/null
+++ b/web/src/lib/vendor/maplibre-layer-manager/layers.ts
@@ -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,
+ 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 = {
+ "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:
+ '© OpenTopoMap © OpenStreetMap',
+ },
+ },
+ 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:
+ '© CyclOSM © OpenStreetMap',
+ },
+ },
+ 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 = {
+ hiking: {
+ version: 8,
+ name: "waymarkedTrailsHiking",
+ sources: {
+ waymarkedTrailsHiking: {
+ type: 'raster',
+ tiles: ['https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png'],
+ tileSize: 256,
+ maxzoom: 18,
+ attribution:
+ '© Waymarked Trails',
+ },
+ },
+ 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:
+ '© Waymarked Trails',
+ },
+ },
+ 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:
+ '© Waymarked Trails',
+ },
+ },
+ 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:
+ '© Waymarked Trails',
+ },
+ },
+ layers: [
+ {
+ id: 'waymarkedTrailsWinter',
+ type: 'raster',
+ source: 'waymarkedTrailsWinter',
+ },
+ ],
+ }
+}
+
+export type POI = { q: string, icon: { svg: any, bg: string } }
+export const pois: Record = {
+ "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>
+}
+
+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,
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/src/lib/vendor/maplibre-layer-manager/maplibre-layer-manager.ts b/web/src/lib/vendor/maplibre-layer-manager/maplibre-layer-manager.ts
new file mode 100644
index 00000000..f29ac687
--- /dev/null
+++ b/web/src/lib/vendor/maplibre-layer-manager/maplibre-layer-manager.ts
@@ -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 = {};
+
+ 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 = `
+
+ `
+
+ icon.src =
+ 'data:image/svg+xml,' +
+ encodeURIComponent(svg);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/web/src/lib/vendor/maplibre-layer-manager/overlay-layer.ts b/web/src/lib/vendor/maplibre-layer-manager/overlay-layer.ts
new file mode 100644
index 00000000..a3219e4f
--- /dev/null
+++ b/web/src/lib/vendor/maplibre-layer-manager/overlay-layer.ts
@@ -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]
+ }
+}
\ No newline at end of file
diff --git a/web/src/lib/vendor/maplibre-layer-manager/overpass-layer.ts b/web/src/lib/vendor/maplibre-layer-manager/overpass-layer.ts
new file mode 100644
index 00000000..021fed21
--- /dev/null
+++ b/web/src/lib/vendor/maplibre-layer-manager/overpass-layer.ts
@@ -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;`;
+ }
+
+}
\ No newline at end of file
diff --git a/web/src/lib/vendor/maplibre-layer-manager/types.ts b/web/src/lib/vendor/maplibre-layer-manager/types.ts
new file mode 100644
index 00000000..ee0154f0
--- /dev/null
+++ b/web/src/lib/vendor/maplibre-layer-manager/types.ts
@@ -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;
+ type: string;
+}
\ No newline at end of file
diff --git a/web/src/lib/vendor/maplibre-style-switcher/style-switcher-control.ts b/web/src/lib/vendor/maplibre-style-switcher/style-switcher-control.ts
index f52e1406..c29c25bd 100644
--- a/web/src/lib/vendor/maplibre-style-switcher/style-switcher-control.ts
+++ b/web/src/lib/vendor/maplibre-style-switcher/style-switcher-control.ts
@@ -1,21 +1,15 @@
-import type { RadioItem } from "$lib/components/base/radio_group.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 type { MapState, pois } from "../maplibre-layer-manager/layers";
/**
* Style switcher control options
*/
export type StyleSwitcherControlOptions = {
- styles: RadioItem[];
- layers: RadioItem[];
- onMapStyleSwitch: (index: number, style: RadioItem) => void
- onLayerChange: (checked: boolean, layer: RadioItem) => void
-
- state: {
- selectedStyle: number;
- selectedLayers: Record
- };
+ styles: Record;
+ onchange: (state: MapState) => void
+ state: MapState;
};
export class StyleSwitcherControl implements IControl {