switches to layer manager

This commit is contained in:
Christian Beutel
2025-08-04 18:01:26 +02:00
parent 53508be008
commit b7c06fcaf2
8 changed files with 358 additions and 354 deletions

View File

@@ -15,8 +15,12 @@
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 { 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 { baseMapStyles } from "$lib/vendor/maplibre-layer-manager/layers";
import { LayerManager } from "$lib/vendor/maplibre-layer-manager/maplibre-layer-manager"; 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 { StyleSwitcherControl } from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
import type { Feature, FeatureCollection, GeoJSON } from "geojson"; import type { Feature, FeatureCollection, GeoJSON } from "geojson";
import * as M from "maplibre-gl"; import * as M from "maplibre-gl";
@@ -101,30 +105,14 @@
let layerManager: LayerManager; 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 elevationMarker: FontawesomeMarker;
let draggingSegment: number | null = null; let draggingSegment: number | null = null;
let hoveringTrail: boolean = false; let hoveringTrail: boolean = false;
let mapLoaded: boolean = false;
const trailColors = [ const trailColors = [
"#3549BB", "#3549BB",
"#592E9E", "#592E9E",
@@ -142,7 +130,7 @@
let [data, clusterData] = $derived(getData(trails)); let [data, clusterData] = $derived(getData(trails));
$effect(() => { $effect(() => {
if (data && map) { if (data && map && mapLoaded) {
untrack(() => initMap(map?.loaded() ?? false)); untrack(() => initMap(map?.loaded() ?? false));
} }
}); });
@@ -153,9 +141,9 @@
toggleEpcTheme(); toggleEpcTheme();
}); });
$effect(() => { $effect(() => {
if (drawing) { if (drawing && map && layerManager) {
untrack(() => startDrawing()); untrack(() => startDrawing());
} else { } else if (map && layerManager) {
untrack(() => stopDrawing()); untrack(() => stopDrawing());
} }
}); });
@@ -192,9 +180,14 @@
}); });
}); });
function getData(trails: Trail[]): [GeoJSON[], FeatureCollection] { function getData(
let cD: FeatureCollection = { type: "FeatureCollection", features: [] }; trails: Trail[],
let r: GeoJSON[] = []; ): [FeatureCollection[], FeatureCollection] {
let clusterData: FeatureCollection = {
type: "FeatureCollection",
features: [],
};
let r: FeatureCollection[] = [];
for (const t of trails) { for (const t of trails) {
if (t.expand?.gpx) { if (t.expand?.gpx) {
@@ -203,7 +196,7 @@
r.push(GPX.parse(t.expand.gpx_data).toGeoJSON()); r.push(GPX.parse(t.expand.gpx_data).toGeoJSON());
} }
if (clusterTrails && t.lat !== null && t.lon !== null) { if (clusterTrails && t.lat !== null && t.lon !== null) {
cD.features.push({ clusterData.features.push({
id: t.id, id: t.id,
type: "Feature", type: "Feature",
properties: { properties: {
@@ -217,11 +210,11 @@
} }
} }
return [r, cD]; return [r, clusterData];
} }
function initMap(mapLoaded: boolean) { function initMap(mapLoaded: boolean) {
if (!map) { if (!map || !layerManager) {
return; return;
} }
@@ -240,11 +233,14 @@
addClusterLayer(clusterData); addClusterLayer(clusterData);
} }
Object.keys(layers).forEach((layerId) => { Object.entries(layerManager.layers).forEach(([id, layer]) => {
const isStillVisible = trails.some((t) => t.id === layerId); if (!(layer instanceof TrailLayer)) {
return;
}
const isStillVisible = trails.some((t) => t.id === id);
if (!isStillVisible) { if (!isStillVisible) {
removeCaretLayer(); removeCaretLayer();
removeTrailLayer(layerId); removeTrailLayer(id);
} }
}); });
@@ -259,7 +255,7 @@
flyToBounds(); flyToBounds();
} }
} else if (drawing && activeTrail !== null && mapLoaded) { } else if (drawing && activeTrail !== null && mapLoaded) {
addCaretLayer(trails[activeTrail]?.id); addCaretLayer(data[activeTrail]);
} }
} }
@@ -322,100 +318,37 @@
} }
function removeTrailLayer(id: string) { function removeTrailLayer(id: string) {
if (!layers[id]) { layerManager.removeLayer(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,
},
};
}
} }
function addTrailLayer( function addTrailLayer(
trail: Trail, trail: Trail,
id: string, id: string,
index: number, index: number,
geojson: GeoJSON | null | undefined, geojson: GeoJSON.FeatureCollection | null | undefined,
) { ) {
if (!geojson || !map) { if (!geojson || !map) {
return; 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)) { onLeave: (e) => unHighlightTrail(id),
try { onMouseUp: (e) => {
map.addSource(id, { activeTrail = trails.findIndex((t) => t.id == trail.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,
}, },
}); onMouseMove: moveCrosshairToCursorPosition,
layers[id].layer = map.getLayer(id) as M.LineLayerSpecification; onMouseDown: (e) => handleDragStart(e, id),
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);
map.on("mouseenter", id, layers[id].listener.onEnter); layerManager.addLayer(id, trailLayer);
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);
}
if (!drawing && !clusterTrails) { if (!drawing && !clusterTrails) {
addStartEndMarkers(trail, id, geojson); addStartEndMarkers(trail, id, geojson);
@@ -426,98 +359,34 @@
if (!geojson || !map || !map.style) { if (!geojson || !map || !map.style) {
return; return;
} }
if (!map.getSource("trails")) { layerManager.addLayer(
map.addSource("trails", { "clusters",
type: "geojson", new ClusterLayer(map, geojson, {
data: geojson, "unclustered-point": {
cluster: true, onMouseDown: (e) => {
clusterRadius: 50, const trail = trails.find(
}); (t) =>
} else { t.id == (e as any).features[0].properties.trail,
(map.getSource("trails") as M.GeoJSONSource).setData(geojson); );
} if (!trail || !map) {
if (!map.getLayer("clusters")) { return;
map.addLayer({ }
id: "clusters", highlightCluster(trail);
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",
}, },
}); }),
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) { if (!geojson || !map || !map.style) {
return; 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")) { layerManager.addLayer(
map.addLayer({ "cluster-highlight",
id: "cluster-highlight", new TrailLayer("cluster-highlight", geojson, trailColors[0]),
type: "line", );
source: "cluster-highlight",
paint: {
"line-color": trailColors[0],
"line-width": 5,
},
});
}
} }
function moveCrosshairToCursorPosition(e: M.MapMouseEvent) { function moveCrosshairToCursorPosition(e: M.MapMouseEvent) {
@@ -567,48 +436,21 @@
draggingSegment = null; draggingSegment = null;
} }
function addCaretLayer(id?: string) { function addCaretLayer(geojson: GeoJSON) {
if (!map || !id || !map.getSource(id)) { if (!map) {
return; return;
} }
if (map.getLayer("direction-carets")) { if (map.getLayer("direction-carets")) {
removeCaretLayer(); removeCaretLayer();
} }
layerManager.addLayer(
map.addLayer({ "direction-carets",
id: "direction-carets", new CaretLayer({ type: "geojson", data: geojson }),
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,
],
},
});
} }
function removeCaretLayer() { function removeCaretLayer() {
if (!map || !map.getLayer("direction-carets")) { layerManager?.removeLayer("direction-carets");
return;
}
map.removeLayer("direction-carets");
} }
export function highlightTrail( export function highlightTrail(
@@ -661,9 +503,7 @@
if (!map || !map.style) { if (!map || !map.style) {
return; return;
} }
if (map?.getLayer("cluster-highlight")) { layerManager.removeLayer("cluster-highlight");
map?.removeLayer("cluster-highlight");
}
if (closePopup) { if (closePopup) {
clusterPopup?.remove(); clusterPopup?.remove();
} }
@@ -697,7 +537,7 @@
epc?.showProfile(); epc?.showProfile();
} }
showWaypoints(); showWaypoints();
addCaretLayer(trail.id!); addCaretLayer(data[activeTrail]);
flyToBounds(); flyToBounds();
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
@@ -740,10 +580,12 @@
map.getCanvas().style.cursor = "inherit"; map.getCanvas().style.cursor = "inherit";
if (activeTrail !== null && trails[activeTrail] && !clusterTrails) { if (activeTrail !== null && trails[activeTrail] && !clusterTrails) {
console.log(data[activeTrail].features[0]);
addStartEndMarkers( addStartEndMarkers(
trails[activeTrail], trails[activeTrail],
trails[activeTrail].id, trails[activeTrail].id,
data?.at(activeTrail), data[activeTrail],
); );
} }
} }
@@ -753,21 +595,24 @@
id: string | undefined, id: string | undefined,
geojson: GeoJSON | null | undefined, geojson: GeoJSON | null | undefined,
) { ) {
if (!map || !trail || !id) { if (
!map ||
!trail ||
!id ||
!(layerManager.layers[id] instanceof TrailLayer)
) {
return; return;
} }
createEmptyLayer(id);
layers[id].startMarker ??= new FontawesomeMarker( const startMarker = new FontawesomeMarker(
{ icon: "fa fa-bullseye" }, { icon: "fa fa-bullseye" },
{}, {},
); );
layerManager.layers[id].markers.start = startMarker;
if (!geojson) { if (!geojson) {
if (trail.lon && trail.lat) { if (trail.lon && trail.lat) {
layers[id].startMarker startMarker.setLngLat([trail.lon, trail.lat]).addTo(map);
.setLngLat([trail.lon, trail.lat])
.addTo(map);
} }
return; return;
} }
@@ -778,34 +623,35 @@
return; return;
} }
if (showInfoPopup) { const endMarker = new FontawesomeMarker(
const popup = createPopupFromTrail(trail);
layers[id].startMarker.setPopup(popup);
layers[id].endMarker?.setPopup(popup);
}
layers[id].endMarker ??= new FontawesomeMarker(
{ icon: "fa fa-flag-checkered" }, { 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, 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 startMarker.addTo(map);
.setLngLat(startEndPoint[0] as M.LngLatLike) if (!clusterTrails) {
.addTo(map); endMarker.addTo(map);
}
} }
function removeStartEndMarkers(id: string | undefined) { function removeStartEndMarkers(id: string | undefined) {
if (!id) { if (!id) {
return; return;
} }
layers[id].startMarker?.remove(); layerManager.layers[id].markers?.start?.remove();
layers[id].endMarker?.remove(); layerManager.layers[id].markers?.end?.remove();
} }
function showWaypoints() { function showWaypoints() {
@@ -999,48 +845,15 @@
); );
} }
map.on("styledata", () => { map.on("styledata", (e) => {
if (showTerrain) { if (showTerrain && page.data.settings?.terrain?.terrain) {
try { layerManager.addLayer(
if ( "terrain",
page.data.settings?.terrain?.terrain && new TerrainLayer(
!map?.getSource("terrain") page.data.settings.terrain.terrain,
) { page.data.settings?.terrain?.hillshading,
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!);
} }
}); });
@@ -1059,55 +872,11 @@
onclick?.(e); 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", () => { map.on("load", () => {
initMap(true);
layerManager.init(); layerManager.init();
initMap(true);
oninit?.(map!); oninit?.(map!);
mapLoaded = true;
}); });
showWaypoints(); showWaypoints();
@@ -1152,7 +921,7 @@
if (e.key == "m") { if (e.key == "m") {
if (trails.length === 1) { if (trails.length === 1) {
addTrailLayer(trails[0], trails[0].id!, 0, data[0]); addTrailLayer(trails[0], trails[0].id!, 0, data[0]);
addCaretLayer(trails[0].id); addCaretLayer(data[0]);
} }
} else if (e.key == "p") { } else if (e.key == "p") {
if (showElevation) { if (showElevation) {

View File

@@ -244,16 +244,16 @@ export default class GPX {
}()) as unknown as GPX; }()) as unknown as GPX;
} }
toGeoJSON(): GeoJSON.FeatureCollection { toGeoJSON(includeRoute: boolean = false, includeWaypoints: boolean = false): GeoJSON.FeatureCollection {
const features: GeoJSON.Feature[] = []; const features: GeoJSON.Feature[] = [];
if (this.wpt) { if (this.wpt && includeWaypoints) {
for (const wpt of this.wpt) { for (const wpt of this.wpt) {
features.push(wpt.toGeoJSON()); features.push(wpt.toGeoJSON());
} }
} }
if (this.rte) { if (this.rte && includeRoute) {
for (const rte of this.rte) { for (const rte of this.rte) {
features.push(rte.toGeoJSON()); features.push(rte.toGeoJSON());
} }
@@ -269,6 +269,7 @@ export default class GPX {
type: "FeatureCollection", type: "FeatureCollection",
features features
}; };
geojson.bbox = bbox(geojson) geojson.bbox = bbox(geojson)
return geojson return geojson

View File

@@ -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,
],
},
}]
};
}
}

View File

@@ -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<string, { onMouseUp?: (e: MapMouseEvent) => 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<string, { onMouseUp?: (e: MapMouseEvent) => 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,
});
}
}

View File

@@ -1,6 +1,5 @@
import type { Marker, StyleSpecification, MapMouseEvent } from "maplibre-gl"; import type { StyleSpecification } from "maplibre-gl";
import type { BaseLayer } from "./layers"; import type { BaseLayer } from "./layers";
import * as M from "maplibre-gl";
export class DebugLayer implements BaseLayer { export class DebugLayer implements BaseLayer {
spec: StyleSpecification = { spec: StyleSpecification = {

View File

@@ -9,7 +9,7 @@ import { OverpassLayer } from "./overpass-layer";
export class LayerManager { export class LayerManager {
private map: M.Map; private map: M.Map;
state!: MapState; state!: MapState;
private layers: Record<string, BaseLayer> = {}; layers: Record<string, BaseLayer> = {};
private addedListeners: Set<string> = new Set(); private addedListeners: Set<string> = new Set();
constructor(map: M.Map) { 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)) { 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; return;
} }
for (const [id, s] of Object.entries(layer.spec.sources)) { for (const [id, s] of Object.entries(layer.spec.sources)) {
@@ -134,7 +144,7 @@ export class LayerManager {
this.layers[id] = layer this.layers[id] = layer
} }
private removeLayer(id: string) { removeLayer(id: string) {
const layer = this.layers[id]; const layer = this.layers[id];
if (!layer) { if (!layer) {
return; return;
@@ -178,7 +188,7 @@ export class LayerManager {
} }
private restoreLayers() { private restoreLayers() {
for (const [id, layer] of Object.entries(this.layers)) { for (const [id, layer] of Object.entries(this.layers)) {
this.addLayer(id, layer) this.addLayer(id, layer)
} }
} }

View File

@@ -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",
}]
}
}
}

View File

@@ -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<string, { onMouseUp?: (e: MapMouseEvent) => void; onMouseDown?: (e: MapMouseEvent) => void; onEnter?: (e: MapMouseEvent) => void; onLeave?: (e: MapMouseEvent) => void; onMouseMove?: (e: MapMouseEvent) => void; }>
markers: Record<string, Marker> = {};
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 ?? {} }
}
}