From b7c06fcaf26b578f26fec26e54a09954c3ec68f2 Mon Sep 17 00:00:00 2001 From: Christian Beutel <> Date: Mon, 4 Aug 2025 18:01:26 +0200 Subject: [PATCH] switches to layer manager --- .../trail/map_with_elevation_maplibre.svelte | 459 +++++------------- web/src/lib/models/gpx/gpx.ts | 7 +- .../maplibre-layer-manager/caret-layer.ts | 45 ++ .../maplibre-layer-manager/cluster-layer.ts | 113 +++++ .../maplibre-layer-manager/debug-layer.ts | 3 +- .../maplibre-layer-manager.ts | 18 +- .../maplibre-layer-manager/terrain-layer.ts | 33 ++ .../maplibre-layer-manager/trail-layer.ts | 34 ++ 8 files changed, 358 insertions(+), 354 deletions(-) create mode 100644 web/src/lib/vendor/maplibre-layer-manager/caret-layer.ts create mode 100644 web/src/lib/vendor/maplibre-layer-manager/cluster-layer.ts create mode 100644 web/src/lib/vendor/maplibre-layer-manager/terrain-layer.ts create mode 100644 web/src/lib/vendor/maplibre-layer-manager/trail-layer.ts diff --git a/web/src/lib/components/trail/map_with_elevation_maplibre.svelte b/web/src/lib/components/trail/map_with_elevation_maplibre.svelte index 836d0a73..7abf7dc7 100644 --- a/web/src/lib/components/trail/map_with_elevation_maplibre.svelte +++ b/web/src/lib/components/trail/map_with_elevation_maplibre.svelte @@ -15,8 +15,12 @@ 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 { CaretLayer } from "$lib/vendor/maplibre-layer-manager/caret-layer"; + import { ClusterLayer } from "$lib/vendor/maplibre-layer-manager/cluster-layer"; import { baseMapStyles } from "$lib/vendor/maplibre-layer-manager/layers"; import { LayerManager } from "$lib/vendor/maplibre-layer-manager/maplibre-layer-manager"; + import { TerrainLayer } from "$lib/vendor/maplibre-layer-manager/terrain-layer"; + import { TrailLayer } from "$lib/vendor/maplibre-layer-manager/trail-layer"; import { StyleSwitcherControl } from "$lib/vendor/maplibre-style-switcher/style-switcher-control"; import type { Feature, FeatureCollection, GeoJSON } from "geojson"; import * as M from "maplibre-gl"; @@ -101,30 +105,14 @@ let layerManager: LayerManager; - let layers: Record< - string, - { - startMarker: M.Marker | null; - endMarker: M.Marker | null; - source: M.GeoJSONSource | null; - layer: M.LineLayerSpecification | null; - highlighted: boolean; - listener: { - onEnter: ((e: M.MapMouseEvent) => void) | null; - onLeave: ((e: M.MapMouseEvent) => void) | null; - onMouseUp: ((e: M.MapMouseEvent) => void) | null; - onMouseDown: ((e: M.MapMouseEvent) => void) | null; - onMouseMove: ((e: M.MapMouseEvent) => void) | null; - }; - } - > = {}; - let elevationMarker: FontawesomeMarker; let draggingSegment: number | null = null; let hoveringTrail: boolean = false; + let mapLoaded: boolean = false; + const trailColors = [ "#3549BB", "#592E9E", @@ -142,7 +130,7 @@ let [data, clusterData] = $derived(getData(trails)); $effect(() => { - if (data && map) { + if (data && map && mapLoaded) { untrack(() => initMap(map?.loaded() ?? false)); } }); @@ -153,9 +141,9 @@ toggleEpcTheme(); }); $effect(() => { - if (drawing) { + if (drawing && map && layerManager) { untrack(() => startDrawing()); - } else { + } else if (map && layerManager) { untrack(() => stopDrawing()); } }); @@ -192,9 +180,14 @@ }); }); - function getData(trails: Trail[]): [GeoJSON[], FeatureCollection] { - let cD: FeatureCollection = { type: "FeatureCollection", features: [] }; - let r: GeoJSON[] = []; + function getData( + trails: Trail[], + ): [FeatureCollection[], FeatureCollection] { + let clusterData: FeatureCollection = { + type: "FeatureCollection", + features: [], + }; + let r: FeatureCollection[] = []; for (const t of trails) { if (t.expand?.gpx) { @@ -203,7 +196,7 @@ r.push(GPX.parse(t.expand.gpx_data).toGeoJSON()); } if (clusterTrails && t.lat !== null && t.lon !== null) { - cD.features.push({ + clusterData.features.push({ id: t.id, type: "Feature", properties: { @@ -217,11 +210,11 @@ } } - return [r, cD]; + return [r, clusterData]; } function initMap(mapLoaded: boolean) { - if (!map) { + if (!map || !layerManager) { return; } @@ -240,11 +233,14 @@ addClusterLayer(clusterData); } - Object.keys(layers).forEach((layerId) => { - const isStillVisible = trails.some((t) => t.id === layerId); + Object.entries(layerManager.layers).forEach(([id, layer]) => { + if (!(layer instanceof TrailLayer)) { + return; + } + const isStillVisible = trails.some((t) => t.id === id); if (!isStillVisible) { removeCaretLayer(); - removeTrailLayer(layerId); + removeTrailLayer(id); } }); @@ -259,7 +255,7 @@ flyToBounds(); } } else if (drawing && activeTrail !== null && mapLoaded) { - addCaretLayer(trails[activeTrail]?.id); + addCaretLayer(data[activeTrail]); } } @@ -322,100 +318,37 @@ } function removeTrailLayer(id: string) { - if (!layers[id]) { - return; - } - const layer = layers[id]; - - if (layer.layer) { - map?.removeLayer(id); - } - if (layer.source) { - map?.removeSource(id); - } - layer.startMarker?.remove(); - layer.endMarker?.remove(); - - map?.off("mouseenter", id, layers[id].listener.onEnter!); - map?.off("mouseleave", id, layers[id].listener.onLeave!); - map?.off("mouseup", id, layers[id].listener.onMouseUp!); - map?.off("mousemove", id, layers[id].listener.onMouseMove!); - map?.off("mousedown", id, layers[id].listener.onMouseDown!); - - delete layers[id]; - } - - function createEmptyLayer(id: string) { - if (!layers[id]) { - layers[id] = { - startMarker: null, - endMarker: null, - source: null, - layer: null, - highlighted: false, - listener: { - onMouseUp: null, - onMouseDown: null, - onEnter: null, - onLeave: null, - onMouseMove: null, - }, - }; - } + layerManager.removeLayer(id); } function addTrailLayer( trail: Trail, id: string, index: number, - geojson: GeoJSON | null | undefined, + geojson: GeoJSON.FeatureCollection | null | undefined, ) { if (!geojson || !map) { return; } - createEmptyLayer(id); + const trailLayer = new TrailLayer( + id, + geojson, + trailColors[index % trailColors.length], + { + onEnter: (e) => + highlightTrail(id, trails[activeTrail ?? -1]?.id == id), - if (!layers[id].source || !map.getSource(id)) { - try { - map.addSource(id, { - type: "geojson", - data: geojson, - }); - layers[id].source = map.getSource(id) as M.GeoJSONSource; - } catch (e) { - return; - } - } else { - layers[id].source.setData(geojson); - } - - if (!layers[id].layer || !map.getLayer(id)) { - map.addLayer({ - id: id, - type: "line", - source: id, - paint: { - "line-color": trailColors[index % trailColors.length], - "line-width": 5, + onLeave: (e) => unHighlightTrail(id), + onMouseUp: (e) => { + activeTrail = trails.findIndex((t) => t.id == trail.id); }, - }); - layers[id].layer = map.getLayer(id) as M.LineLayerSpecification; - layers[id].listener.onEnter = (e) => - highlightTrail(id, trails[activeTrail ?? -1]?.id == id); - layers[id].listener.onLeave = (e) => unHighlightTrail(id); - layers[id].listener.onMouseUp = (e) => { - activeTrail = trails.findIndex((t) => t.id == trail.id); - }; - layers[id].listener.onMouseMove = moveCrosshairToCursorPosition; - layers[id].listener.onMouseDown = (e) => handleDragStart(e, id); + onMouseMove: moveCrosshairToCursorPosition, + onMouseDown: (e) => handleDragStart(e, id), + }, + ); - map.on("mouseenter", id, layers[id].listener.onEnter); - map.on("mouseleave", id, layers[id].listener.onLeave); - map.on("mouseup", id, layers[id].listener.onMouseUp); - map.on("mousemove", id, layers[id].listener.onMouseMove); - map.on("mousedown", id, layers[id].listener.onMouseDown); - } + layerManager.addLayer(id, trailLayer); if (!drawing && !clusterTrails) { addStartEndMarkers(trail, id, geojson); @@ -426,98 +359,34 @@ if (!geojson || !map || !map.style) { return; } - if (!map.getSource("trails")) { - map.addSource("trails", { - type: "geojson", - data: geojson, - cluster: true, - clusterRadius: 50, - }); - } else { - (map.getSource("trails") as M.GeoJSONSource).setData(geojson); - } - if (!map.getLayer("clusters")) { - map.addLayer({ - id: "clusters", - type: "circle", - source: "trails", - filter: ["has", "point_count"], - paint: { - "circle-color": "#242734", - "circle-radius": [ - "step", - ["get", "point_count"], - 10, - 10, - 15, - 20, - 20, - 50, - 25, - 100, - 30, - 200, - 35, - ], - "circle-stroke-width": 3, - "circle-stroke-color": "#fff", + layerManager.addLayer( + "clusters", + new ClusterLayer(map, geojson, { + "unclustered-point": { + onMouseDown: (e) => { + const trail = trails.find( + (t) => + t.id == (e as any).features[0].properties.trail, + ); + if (!trail || !map) { + return; + } + highlightCluster(trail); + }, }, - }); - map.addLayer({ - id: "cluster-count", - type: "symbol", - source: "trails", - filter: ["has", "point_count"], - paint: { - "text-color": "#fff", - }, - layout: { - "text-field": "{point_count_abbreviated}", - "text-font": ["Noto Sans Regular"], - "text-size": 12, - }, - }); - map.addLayer({ - id: "unclustered-point", - type: "circle", - source: "trails", - filter: ["!", ["has", "point_count"]], - paint: { - "circle-color": "#242734", - "circle-radius": 7, - "circle-stroke-width": 2, - "circle-stroke-color": "#fff", - }, - }); - } + }), + ); } - function addClusterHighlightLayer(geojson: GeoJSON) { + function addClusterHighlightLayer(geojson: FeatureCollection) { if (!geojson || !map || !map.style) { return; } - if (!map.getSource("cluster-highlight")) { - map.addSource("cluster-highlight", { - type: "geojson", - data: geojson, - }); - } else { - (map.getSource("cluster-highlight") as M.GeoJSONSource).setData( - geojson, - ); - } - if (!map.getLayer("cluster-highlight")) { - map.addLayer({ - id: "cluster-highlight", - type: "line", - source: "cluster-highlight", - paint: { - "line-color": trailColors[0], - "line-width": 5, - }, - }); - } + layerManager.addLayer( + "cluster-highlight", + new TrailLayer("cluster-highlight", geojson, trailColors[0]), + ); } function moveCrosshairToCursorPosition(e: M.MapMouseEvent) { @@ -567,48 +436,21 @@ draggingSegment = null; } - function addCaretLayer(id?: string) { - if (!map || !id || !map.getSource(id)) { + function addCaretLayer(geojson: GeoJSON) { + if (!map) { return; } if (map.getLayer("direction-carets")) { removeCaretLayer(); } - - map.addLayer({ - id: "direction-carets", - type: "symbol", - source: id, - layout: { - "symbol-placement": "line", - "symbol-spacing": [ - "interpolate", - ["exponential", 1.5], - ["zoom"], - 0, - 80, - 18, - 200, - ], - "icon-image": "direction-caret", - "icon-size": [ - "interpolate", - ["exponential", 1.5], - ["zoom"], - 0, - 0.5, - 18, - 0.8, - ], - }, - }); + layerManager.addLayer( + "direction-carets", + new CaretLayer({ type: "geojson", data: geojson }), + ); } function removeCaretLayer() { - if (!map || !map.getLayer("direction-carets")) { - return; - } - map.removeLayer("direction-carets"); + layerManager?.removeLayer("direction-carets"); } export function highlightTrail( @@ -661,9 +503,7 @@ if (!map || !map.style) { return; } - if (map?.getLayer("cluster-highlight")) { - map?.removeLayer("cluster-highlight"); - } + layerManager.removeLayer("cluster-highlight"); if (closePopup) { clusterPopup?.remove(); } @@ -697,7 +537,7 @@ epc?.showProfile(); } showWaypoints(); - addCaretLayer(trail.id!); + addCaretLayer(data[activeTrail]); flyToBounds(); } catch (e) { console.warn(e); @@ -740,10 +580,12 @@ map.getCanvas().style.cursor = "inherit"; if (activeTrail !== null && trails[activeTrail] && !clusterTrails) { + console.log(data[activeTrail].features[0]); + addStartEndMarkers( trails[activeTrail], trails[activeTrail].id, - data?.at(activeTrail), + data[activeTrail], ); } } @@ -753,21 +595,24 @@ id: string | undefined, geojson: GeoJSON | null | undefined, ) { - if (!map || !trail || !id) { + if ( + !map || + !trail || + !id || + !(layerManager.layers[id] instanceof TrailLayer) + ) { return; } - createEmptyLayer(id); - layers[id].startMarker ??= new FontawesomeMarker( + const startMarker = new FontawesomeMarker( { icon: "fa fa-bullseye" }, {}, ); + layerManager.layers[id].markers.start = startMarker; if (!geojson) { if (trail.lon && trail.lat) { - layers[id].startMarker - .setLngLat([trail.lon, trail.lat]) - .addTo(map); + startMarker.setLngLat([trail.lon, trail.lat]).addTo(map); } return; } @@ -778,34 +623,35 @@ return; } - if (showInfoPopup) { - const popup = createPopupFromTrail(trail); - layers[id].startMarker.setPopup(popup); - layers[id].endMarker?.setPopup(popup); - } - - layers[id].endMarker ??= new FontawesomeMarker( + const endMarker = new FontawesomeMarker( { icon: "fa fa-flag-checkered" }, {}, ); - layers[id].endMarker.setLngLat( + layerManager.layers[id].markers.end = endMarker; + + startMarker.setLngLat(startEndPoint[0] as M.LngLatLike); + endMarker.setLngLat( startEndPoint[startEndPoint.length - 1] as M.LngLatLike, ); - if (!clusterTrails) { - layers[id].endMarker.addTo(map); + + if (showInfoPopup) { + const popup = createPopupFromTrail(trail); + startMarker.setPopup(popup); + endMarker.setPopup(popup); } - layers[id].startMarker - .setLngLat(startEndPoint[0] as M.LngLatLike) - .addTo(map); + startMarker.addTo(map); + if (!clusterTrails) { + endMarker.addTo(map); + } } function removeStartEndMarkers(id: string | undefined) { if (!id) { return; } - layers[id].startMarker?.remove(); - layers[id].endMarker?.remove(); + layerManager.layers[id].markers?.start?.remove(); + layerManager.layers[id].markers?.end?.remove(); } function showWaypoints() { @@ -999,48 +845,15 @@ ); } - map.on("styledata", () => { - if (showTerrain) { - try { - if ( - page.data.settings?.terrain?.terrain && - !map?.getSource("terrain") - ) { - map!.addSource("terrain", { - type: "raster-dem", - url: page.data.settings?.terrain?.terrain, - }); - } - if ( - page.data.settings?.terrain?.hillshading && - !map?.getSource("hillshading") - ) { - map!.addSource("hillshading", { - type: "raster-dem", - url: page.data.settings?.terrain?.hillshading, - }); - map!.addLayer({ - id: "hillshading", - source: "terrain", - type: "hillshade", - }); - } - } catch (e) {} - } - trails.forEach((t, i) => { - const layerId = t.id!; - - if (map?.getLayer(layerId)) { - return; - } - addTrailLayer(t, layerId, i, data[i]); - }); - if ( - activeTrail !== null && - trails[activeTrail] && - !map?.getLayer("direction-carets") - ) { - addCaretLayer(trails[activeTrail].id!); + map.on("styledata", (e) => { + if (showTerrain && page.data.settings?.terrain?.terrain) { + layerManager.addLayer( + "terrain", + new TerrainLayer( + page.data.settings.terrain.terrain, + page.data.settings?.terrain?.hillshading, + ), + ); } }); @@ -1059,55 +872,11 @@ onclick?.(e); }); - map.on("click", "clusters", async (e) => { - if (!map) { - return; - } - const features = map.queryRenderedFeatures(e.point, { - layers: ["clusters"], - }); - const clusterId = features[0].properties.cluster_id; - const zoom = await ( - map.getSource("trails") as M.GeoJSONSource - ).getClusterExpansionZoom(clusterId); - map.flyTo({ - center: (features[0].geometry as any).coordinates, - zoom, - }); - }); - - map.on( - "click", - "unclustered-point", - async (e: M.MapMouseEvent & Object) => { - const trail = trails.find( - (t) => t.id == (e as any).features[0].properties.trail, - ); - if (!trail || !map) { - return; - } - highlightCluster(trail); - }, - ); - - map.on("mouseenter", "clusters", () => { - map!.getCanvas().style.cursor = "pointer"; - }); - map.on("mouseleave", "clusters", () => { - map!.getCanvas().style.cursor = ""; - }); - - map.on("mouseenter", "unclustered-point", () => { - map!.getCanvas().style.cursor = "pointer"; - }); - map.on("mouseleave", "unclustered-point", () => { - map!.getCanvas().style.cursor = ""; - }); - map.on("load", () => { - initMap(true); layerManager.init(); + initMap(true); oninit?.(map!); + mapLoaded = true; }); showWaypoints(); @@ -1152,7 +921,7 @@ if (e.key == "m") { if (trails.length === 1) { addTrailLayer(trails[0], trails[0].id!, 0, data[0]); - addCaretLayer(trails[0].id); + addCaretLayer(data[0]); } } else if (e.key == "p") { if (showElevation) { diff --git a/web/src/lib/models/gpx/gpx.ts b/web/src/lib/models/gpx/gpx.ts index c1119018..33b1fad3 100644 --- a/web/src/lib/models/gpx/gpx.ts +++ b/web/src/lib/models/gpx/gpx.ts @@ -244,16 +244,16 @@ export default class GPX { }()) as unknown as GPX; } - toGeoJSON(): GeoJSON.FeatureCollection { + toGeoJSON(includeRoute: boolean = false, includeWaypoints: boolean = false): GeoJSON.FeatureCollection { const features: GeoJSON.Feature[] = []; - if (this.wpt) { + if (this.wpt && includeWaypoints) { for (const wpt of this.wpt) { features.push(wpt.toGeoJSON()); } } - if (this.rte) { + if (this.rte && includeRoute) { for (const rte of this.rte) { features.push(rte.toGeoJSON()); } @@ -269,6 +269,7 @@ export default class GPX { type: "FeatureCollection", features }; + geojson.bbox = bbox(geojson) return geojson diff --git a/web/src/lib/vendor/maplibre-layer-manager/caret-layer.ts b/web/src/lib/vendor/maplibre-layer-manager/caret-layer.ts new file mode 100644 index 00000000..4d8412d2 --- /dev/null +++ b/web/src/lib/vendor/maplibre-layer-manager/caret-layer.ts @@ -0,0 +1,45 @@ +import type { SourceSpecification, StyleSpecification } from "maplibre-gl"; +import type { BaseLayer } from "./layers"; + +export class CaretLayer implements BaseLayer { + + spec: StyleSpecification; + + constructor(source: SourceSpecification) { + this.spec = { + version: 8, + name: "direction-carets", + sources: { + caret: source + }, + layers: [{ + id: "direction-carets", + type: "symbol", + source: "caret", + layout: { + "symbol-placement": "line", + "symbol-spacing": [ + "interpolate", + ["exponential", 1.5], + ["zoom"], + 0, + 80, + 18, + 200, + ], + "icon-image": "direction-caret", + "icon-size": [ + "interpolate", + ["exponential", 1.5], + ["zoom"], + 0, + 0.5, + 18, + 0.8, + ], + }, + }] + + }; + } +} \ No newline at end of file diff --git a/web/src/lib/vendor/maplibre-layer-manager/cluster-layer.ts b/web/src/lib/vendor/maplibre-layer-manager/cluster-layer.ts new file mode 100644 index 00000000..717c0eb4 --- /dev/null +++ b/web/src/lib/vendor/maplibre-layer-manager/cluster-layer.ts @@ -0,0 +1,113 @@ +import type { MapMouseEvent, SourceSpecification, StyleSpecification } from "maplibre-gl"; +import type { BaseLayer } from "./layers"; +import * as M from "maplibre-gl"; + +export class ClusterLayer implements BaseLayer { + + spec: StyleSpecification; + + private map: M.Map; + + + listeners: Record void; onMouseDown?: (e: MapMouseEvent) => void; onEnter?: (e: MapMouseEvent) => void; onLeave?: (e: MapMouseEvent) => void; onMouseMove?: (e: MapMouseEvent) => void; }> = { + "clusters": { + onMouseDown: this.zoomOnCluster.bind(this), + onEnter: () => this.map!.getCanvas().style.cursor = "pointer", + onLeave: () => this.map!.getCanvas().style.cursor = "" + }, + "unclustered-point": { + onEnter: () => this.map!.getCanvas().style.cursor = "pointer", + onLeave: () => this.map!.getCanvas().style.cursor = "" + } + }; + + constructor(map: M.Map, geojson: GeoJSON.FeatureCollection, listeners?: Record void; onMouseDown?: (e: MapMouseEvent) => void; onEnter?: (e: MapMouseEvent) => void; onLeave?: (e: MapMouseEvent) => void; onMouseMove?: (e: MapMouseEvent) => void; }>) { + this.map = map; + this.listeners = { + "clusters": { ...this.listeners["clusters"], ...listeners?.["clusters"] }, + "unclustered-point": { ...this.listeners["unclustered-point"], ...listeners?.["unclustered-point"] } + } + this.spec = { + version: 8, + name: "clusters", + glyphs: "https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf", + sources: { + "cluster-trails": { + type: "geojson", + data: geojson, + cluster: true, + clusterRadius: 50, + } + }, + layers: [ + { + id: "clusters", + type: "circle", + source: "cluster-trails", + filter: ["has", "point_count"], + paint: { + "circle-color": "#242734", + "circle-radius": [ + "step", + ["get", "point_count"], + 10, + 10, + 15, + 20, + 20, + 50, + 25, + 100, + 30, + 200, + 35, + ], + "circle-stroke-width": 3, + "circle-stroke-color": "#fff", + }, + }, + { + id: "cluster-count", + type: "symbol", + source: "cluster-trails", + filter: ["has", "point_count"], + paint: { + "text-color": "#fff", + }, + layout: { + "text-field": "{point_count_abbreviated}", + "text-font": ["Noto Sans Regular"], + "text-size": 12, + }, + }, + { + id: "unclustered-point", + type: "circle", + source: "cluster-trails", + filter: ["!", ["has", "point_count"]], + paint: { + "circle-color": "#242734", + "circle-radius": 7, + "circle-stroke-width": 2, + "circle-stroke-color": "#fff", + }, + } + ] + + }; + } + + private async zoomOnCluster(e: MapMouseEvent) { + const features = this.map.queryRenderedFeatures(e.point, { + layers: ["clusters"], + }); + const clusterId = features[0].properties.cluster_id; + const zoom = await ( + this.map.getSource("cluster-trails") as M.GeoJSONSource + ).getClusterExpansionZoom(clusterId); + this.map.flyTo({ + center: (features[0].geometry as any).coordinates, + zoom, + }); + } +} \ No newline at end of file diff --git a/web/src/lib/vendor/maplibre-layer-manager/debug-layer.ts b/web/src/lib/vendor/maplibre-layer-manager/debug-layer.ts index 3605c6a6..98fb199f 100644 --- a/web/src/lib/vendor/maplibre-layer-manager/debug-layer.ts +++ b/web/src/lib/vendor/maplibre-layer-manager/debug-layer.ts @@ -1,6 +1,5 @@ -import type { Marker, StyleSpecification, MapMouseEvent } from "maplibre-gl"; +import type { StyleSpecification } from "maplibre-gl"; import type { BaseLayer } from "./layers"; -import * as M from "maplibre-gl"; export class DebugLayer implements BaseLayer { spec: StyleSpecification = { 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 index 87dedb2d..e1b61421 100644 --- a/web/src/lib/vendor/maplibre-layer-manager/maplibre-layer-manager.ts +++ b/web/src/lib/vendor/maplibre-layer-manager/maplibre-layer-manager.ts @@ -9,7 +9,7 @@ import { OverpassLayer } from "./overpass-layer"; export class LayerManager { private map: M.Map; state!: MapState; - private layers: Record = {}; + layers: Record = {}; private addedListeners: Set = new Set(); constructor(map: M.Map) { @@ -87,8 +87,18 @@ export class LayerManager { } - private addLayer(id: string, layer: BaseLayer) { + addLayer(id: string, layer: BaseLayer) { if (this.layers[id] && this.map.getLayer(id)) { + // update sources and return + for (const [sourceId, s] of Object.entries(layer.spec.sources)) { + if (s.type != "geojson" || !this.map.getSource(sourceId)) { + continue; + } + + const source = this.map.getSource(sourceId) as M.GeoJSONSource + source.setData(s.data) + this.layers[id] = layer + } return; } for (const [id, s] of Object.entries(layer.spec.sources)) { @@ -134,7 +144,7 @@ export class LayerManager { this.layers[id] = layer } - private removeLayer(id: string) { + removeLayer(id: string) { const layer = this.layers[id]; if (!layer) { return; @@ -178,7 +188,7 @@ export class LayerManager { } private restoreLayers() { - for (const [id, layer] of Object.entries(this.layers)) { + for (const [id, layer] of Object.entries(this.layers)) { this.addLayer(id, layer) } } diff --git a/web/src/lib/vendor/maplibre-layer-manager/terrain-layer.ts b/web/src/lib/vendor/maplibre-layer-manager/terrain-layer.ts new file mode 100644 index 00000000..9704e522 --- /dev/null +++ b/web/src/lib/vendor/maplibre-layer-manager/terrain-layer.ts @@ -0,0 +1,33 @@ +import type { StyleSpecification } from "maplibre-gl"; +import type { BaseLayer } from "./layers"; + +export class TerrainLayer implements BaseLayer { + spec: StyleSpecification; + + constructor(terrainURL: string, hillshadingURL?: string) { + this.spec = { + version: 8, + name: "terrain", + sources: { + terrain: { + type: "raster-dem", + url: terrainURL, + }, + ...(hillshadingURL ? { + hillshading: { + type: "raster-dem", + url: hillshadingURL + } + } : {}) + + }, + layers: [{ + id: "hillshading", + source: "terrain", + type: "hillshade", + }] + + } + + } +} \ No newline at end of file diff --git a/web/src/lib/vendor/maplibre-layer-manager/trail-layer.ts b/web/src/lib/vendor/maplibre-layer-manager/trail-layer.ts new file mode 100644 index 00000000..aebbb72a --- /dev/null +++ b/web/src/lib/vendor/maplibre-layer-manager/trail-layer.ts @@ -0,0 +1,34 @@ +import type { MapMouseEvent, Marker, StyleSpecification } from "maplibre-gl"; +import type { BaseLayer } from "./layers"; + +export class TrailLayer implements BaseLayer { + + spec: StyleSpecification; + listeners: Record void; onMouseDown?: (e: MapMouseEvent) => void; onEnter?: (e: MapMouseEvent) => void; onLeave?: (e: MapMouseEvent) => void; onMouseMove?: (e: MapMouseEvent) => void; }> + markers: Record = {}; + + constructor(id: string, geojson: GeoJSON.FeatureCollection, color: string, listerners?: { onMouseUp?: (e: MapMouseEvent) => void; onMouseDown?: (e: MapMouseEvent) => void; onEnter?: (e: MapMouseEvent) => void; onLeave?: (e: MapMouseEvent) => void; onMouseMove?: (e: MapMouseEvent) => void; }) { + this.spec = { + version: 8, + name: id, + sources: { + [id]: { + type: "geojson", + data: geojson, + } + }, + layers: [{ + id: id, + type: "line", + source: id, + paint: { + "line-color": color, + "line-width": 5, + }, + }] + + }; + + this.listeners = { id: listerners ?? {} } + } +} \ No newline at end of file