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 { 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,
},
});
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) => {
onLeave: (e) => unHighlightTrail(id),
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",
},
});
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) {
if (!geojson || !map || !map.style) {
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;
}
if (!map.getSource("cluster-highlight")) {
map.addSource("cluster-highlight", {
type: "geojson",
data: geojson,
});
} else {
(map.getSource("cluster-highlight") as M.GeoJSONSource).setData(
geojson,
highlightCluster(trail);
},
},
}),
);
}
if (!map.getLayer("cluster-highlight")) {
map.addLayer({
id: "cluster-highlight",
type: "line",
source: "cluster-highlight",
paint: {
"line-color": trailColors[0],
"line-width": 5,
},
});
function addClusterHighlightLayer(geojson: FeatureCollection) {
if (!geojson || !map || !map.style) {
return;
}
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) {

View File

@@ -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

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 * as M from "maplibre-gl";
export class DebugLayer implements BaseLayer {
spec: StyleSpecification = {

View File

@@ -9,7 +9,7 @@ import { OverpassLayer } from "./overpass-layer";
export class LayerManager {
private map: M.Map;
state!: MapState;
private layers: Record<string, BaseLayer> = {};
layers: Record<string, BaseLayer> = {};
private addedListeners: Set<string> = 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;

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 ?? {} }
}
}