forked from MapComplete/MapComplete
		
	Feature: more or less working version of clustering, clusters of multiple layers are joined
This commit is contained in:
		
							parent
							
								
									5bc8f11d24
								
							
						
					
					
						commit
						0048c091d0
					
				
					 9 changed files with 143 additions and 65 deletions
				
			
		|  | @ -1,25 +1,27 @@ | |||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import FilteredLayer from "../../Models/FilteredLayer" | ||||
| import { Feature } from "geojson" | ||||
| import { Feature, Geometry } from "geojson" | ||||
| import { OsmTags } from "../../Models/OsmFeature" | ||||
| 
 | ||||
| export interface FeatureSource<T extends Feature = Feature> { | ||||
| export interface FeatureSource<T extends Feature = Feature<Geometry, OsmTags>> { | ||||
|     features: Store<T[]> | ||||
| } | ||||
| 
 | ||||
| export interface UpdatableFeatureSource<T extends Feature = Feature> extends FeatureSource<T> { | ||||
| export interface UpdatableFeatureSource<T extends Feature = Feature<Geometry, OsmTags>> extends FeatureSource<T> { | ||||
|     /** | ||||
|      * Forces an update and downloads the data, even if the feature source is supposed to be active | ||||
|      */ | ||||
|     updateAsync() | ||||
|     updateAsync(): void | ||||
| } | ||||
| export interface WritableFeatureSource<T extends Feature = Feature> extends FeatureSource<T> { | ||||
| 
 | ||||
| export interface WritableFeatureSource<T extends Feature = Feature<Geometry, OsmTags>> extends FeatureSource<T> { | ||||
|     features: UIEventSource<T[]> | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * A feature source which only contains features for the defined layer | ||||
|  */ | ||||
| export interface FeatureSourceForLayer<T extends Feature = Feature> extends FeatureSource<T> { | ||||
| export interface FeatureSourceForLayer<T extends Feature = Feature<Geometry, OsmTags>> extends FeatureSource<T> { | ||||
|     readonly layer: FilteredLayer | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -11,17 +11,17 @@ import { UIEventSource } from "../UIEventSource" | |||
|  * If this is the case, multiple objects with a different _matching_layer_id are generated. | ||||
|  * In any case, this featureSource marks the objects with _matching_layer_id | ||||
|  */ | ||||
| export default class PerLayerFeatureSourceSplitter<T extends FeatureSource = FeatureSource> { | ||||
|     public readonly perLayer: ReadonlyMap<string, T> | ||||
| export default class PerLayerFeatureSourceSplitter<T extends Feature, SRC extends FeatureSource<T>> { | ||||
|     public readonly perLayer: ReadonlyMap<string, SRC> | ||||
|     constructor( | ||||
|         layers: FilteredLayer[], | ||||
|         upstream: FeatureSource, | ||||
|         upstream: FeatureSource<T>, | ||||
|         options?: { | ||||
|             constructStore?: (features: UIEventSource<Feature[]>, layer: FilteredLayer) => T | ||||
|             handleLeftovers?: (featuresWithoutLayer: Feature[]) => void | ||||
|             constructStore?: (features: UIEventSource<T[]>, layer: FilteredLayer) => SRC | ||||
|             handleLeftovers?: (featuresWithoutLayer: T[]) => void | ||||
|         } | ||||
|     ) { | ||||
|         const knownLayers = new Map<string, T>() | ||||
|         const knownLayers = new Map<string, SRC>() | ||||
|         /** | ||||
|          * Keeps track of the ids that are included per layer. | ||||
|          * Used to know if the downstream feature source needs to be pinged | ||||
|  | @ -32,12 +32,12 @@ export default class PerLayerFeatureSourceSplitter<T extends FeatureSource = Fea | |||
|         const constructStore = | ||||
|             options?.constructStore ?? ((store, layer) => new SimpleFeatureSource(layer, store)) | ||||
|         for (const layer of layers) { | ||||
|             const src = new UIEventSource<Feature[]>([]) | ||||
|             const src = new UIEventSource<T[]>([]) | ||||
|             layerSources.set(layer.layerDef.id, src) | ||||
|             knownLayers.set(layer.layerDef.id, <T>constructStore(src, layer)) | ||||
|             knownLayers.set(layer.layerDef.id, <SRC>constructStore(src, layer)) | ||||
|         } | ||||
| 
 | ||||
|         upstream.features.addCallbackAndRunD((features) => { | ||||
|         upstream.features.addCallbackAndRunD((features: T[]) => { | ||||
|             if (layers === undefined) { | ||||
|                 return | ||||
|             } | ||||
|  | @ -51,7 +51,7 @@ export default class PerLayerFeatureSourceSplitter<T extends FeatureSource = Fea | |||
|              */ | ||||
|             const hasChanged: boolean[] = layers.map(() => false) | ||||
|             const newIndices: Set<string>[] = layers.map(() => new Set<string>()) | ||||
|             const noLayerFound: Feature[] = [] | ||||
|             const noLayerFound: T[] = [] | ||||
| 
 | ||||
|             for (const layer of layers) { | ||||
|                 featuresPerLayer.set(layer.layerDef.id, []) | ||||
|  | @ -115,7 +115,7 @@ export default class PerLayerFeatureSourceSplitter<T extends FeatureSource = Fea | |||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public forEach(f: (featureSource: T) => void) { | ||||
|     public forEach(f: ((src: SRC) => void)) { | ||||
|         for (const fs of this.perLayer.values()) { | ||||
|             f(fs) | ||||
|         } | ||||
|  |  | |||
|  | @ -1,24 +1,19 @@ | |||
| import { Feature as GeojsonFeature, Geometry } from "geojson" | ||||
| import { Feature, Feature as GeojsonFeature, Geometry } from "geojson" | ||||
| 
 | ||||
| import { Store, UIEventSource } from "../../UIEventSource" | ||||
| import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource" | ||||
| import { MvtToGeojson } from "mvt-to-geojson" | ||||
| import { OsmTags } from "../../../Models/OsmFeature" | ||||
| 
 | ||||
| export default class MvtSource implements FeatureSourceForTile, UpdatableFeatureSource { | ||||
|     public readonly features: Store<GeojsonFeature<Geometry, { [name: string]: any }>[]> | ||||
|     public readonly features: Store<GeojsonFeature<Geometry, OsmTags>[]> | ||||
|     public readonly x: number | ||||
|     public readonly y: number | ||||
|     public readonly z: number | ||||
|     private readonly _url: string | ||||
|     private readonly _layerName: string | ||||
|     private readonly _features: UIEventSource< | ||||
|         GeojsonFeature< | ||||
|             Geometry, | ||||
|             { | ||||
|                 [name: string]: any | ||||
|             } | ||||
|         >[] | ||||
|     > = new UIEventSource<GeojsonFeature<Geometry, { [p: string]: any }>[]>([]) | ||||
|         GeojsonFeature<Geometry, OsmTags>[] | ||||
|     > = new UIEventSource<GeojsonFeature<Geometry, OsmTags>[]>([]) | ||||
|     private currentlyRunning: Promise<any> | ||||
| 
 | ||||
|     constructor( | ||||
|  | @ -26,11 +21,9 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature | |||
|         x: number, | ||||
|         y: number, | ||||
|         z: number, | ||||
|         layerName?: string, | ||||
|         isActive?: Store<boolean> | ||||
|     ) { | ||||
|         this._url = url | ||||
|         this._layerName = layerName | ||||
|         this.x = x | ||||
|         this.y = y | ||||
|         this.z = z | ||||
|  | @ -61,7 +54,7 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature | |||
|                 return | ||||
|             } | ||||
|             const buffer = await result.arrayBuffer() | ||||
|             const features = MvtToGeojson.fromBuffer(buffer, this.x, this.y, this.z) | ||||
|             const features = <Feature<Geometry, OsmTags>[]>MvtToGeojson.fromBuffer(buffer, this.x, this.y, this.z) | ||||
|             for (const feature of features) { | ||||
|                 const properties = feature.properties | ||||
|                 if (!properties["osm_type"]) { | ||||
|  |  | |||
|  | @ -1,14 +1,15 @@ | |||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import { FeatureSourceForLayer } from "../FeatureSource" | ||||
| import { Feature } from "geojson" | ||||
| import { Feature, Geometry } from "geojson" | ||||
| import { OsmTags } from "../../../Models/OsmFeature" | ||||
| 
 | ||||
| export default class SimpleFeatureSource implements FeatureSourceForLayer { | ||||
|     public readonly features: UIEventSource<Feature[]> | ||||
| export default class SimpleFeatureSource<T extends Feature = Feature<Geometry, OsmTags>> implements FeatureSourceForLayer<T> { | ||||
|     public readonly features: UIEventSource<T[]> | ||||
|     public readonly layer: FilteredLayer | ||||
| 
 | ||||
|     constructor(layer: FilteredLayer, featureSource?: UIEventSource<Feature[]>) { | ||||
|     constructor(layer: FilteredLayer, featureSource?: UIEventSource<T[]>) { | ||||
|         this.layer = layer | ||||
|         this.features = featureSource ?? new UIEventSource<Feature[]>([]) | ||||
|         this.features = featureSource ?? new UIEventSource<T[]>([]) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ import { FeatureSource } from "../FeatureSource" | |||
| import { Feature, Point } from "geojson" | ||||
| import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| import StaticFeatureSource from "../Sources/StaticFeatureSource" | ||||
| import { Tiles } from "../../../Models/TileRange" | ||||
| 
 | ||||
| export interface ClusteringOptions { | ||||
|  | @ -19,9 +18,14 @@ export interface ClusteringOptions { | |||
|     showSummaryAt?: "tilecenter" | "average" | ||||
| } | ||||
| 
 | ||||
| interface SummaryProperties { | ||||
|     id: string, | ||||
|     total: number, | ||||
|     tile_id: number | ||||
| } | ||||
| 
 | ||||
| export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>> implements FeatureSource<T> { | ||||
| 
 | ||||
|     public readonly summaryPoints: FeatureSource | ||||
|     private readonly id: string | ||||
|     private readonly showSummaryAt: "tilecenter" | "average" | ||||
|     features: Store<T[]> | ||||
|  | @ -42,11 +46,9 @@ export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>> | |||
|         const clusterCutoff = options?.dontClusterAboveZoom ?? 17 | ||||
|         const doCluster = options?.dontClusterAboveZoom === undefined ? new ImmutableStore(true) : currentZoomlevel.map(zoom => zoom <= clusterCutoff) | ||||
|         const cutoff = options?.cutoff ?? 20 | ||||
|         const summaryPoints = new UIEventSource<Feature<Point>[]>([]) | ||||
|         currentZoomlevel = currentZoomlevel.stabilized(500) | ||||
|         this.summaryPoints = new StaticFeatureSource(summaryPoints) | ||||
|         const summaryPoints = new UIEventSource<Feature<Point, SummaryProperties>[]>([]) | ||||
|         currentZoomlevel = currentZoomlevel.stabilized(500).map(z => Math.floor(z)) | ||||
|         this.features = (upstream.features.map(features => { | ||||
|             console.log(">>> Updating features in clusters ", this.id, ":", features) | ||||
|             if (!doCluster.data) { | ||||
|                 summaryPoints.set([]) | ||||
|                 return features | ||||
|  | @ -55,7 +57,7 @@ export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>> | |||
|             const z = currentZoomlevel.data | ||||
|             const perTile = GeoOperations.spreadIntoBboxes(features, z) | ||||
|             const resultingFeatures = [] | ||||
|             const summary: Feature<Point>[] = [] | ||||
|             const summary: Feature<Point, SummaryProperties>[] = [] | ||||
|             for (const tileIndex of perTile.keys()) { | ||||
|                 const tileFeatures: Feature<Point>[] = perTile.get(tileIndex) | ||||
|                 if (tileFeatures.length > cutoff) { | ||||
|  | @ -69,11 +71,12 @@ export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>> | |||
| 
 | ||||
|         }, [doCluster, currentZoomlevel])) | ||||
| 
 | ||||
|         ClusterGrouping.singleton.registerSource(summaryPoints) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private createSummaryFeature(features: Feature<Point>[], tileId: number): Feature<Point> { | ||||
|     private createSummaryFeature(features: Feature<Point>[], tileId: number): Feature<Point, SummaryProperties> { | ||||
| 
 | ||||
|         let lon: number | ||||
|         let lat: number | ||||
|  | @ -91,7 +94,7 @@ export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>> | |||
|             lon = lonSum / features.length | ||||
|             lat = latSum / features.length | ||||
|         } | ||||
|         return <Feature<Point>>{ | ||||
|         return { | ||||
|             type: "Feature", | ||||
|             geometry: { | ||||
|                 type: "Point", | ||||
|  | @ -99,9 +102,68 @@ export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>> | |||
|             }, | ||||
|             properties: { | ||||
|                 id: "summary_" + this.id + "_" + tileId, | ||||
|                 z, | ||||
|                 total_metric: "" + features.length | ||||
|                 tile_id: tileId, | ||||
|                 total: features.length | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Groups multiple summaries together | ||||
|  */ | ||||
| export class ClusterGrouping implements FeatureSource<Feature<Point, { total_metric: string }>> { | ||||
|     private readonly _features: UIEventSource<Feature<Point, { total_metric: string }>[]> = new UIEventSource([]) | ||||
|     public readonly features: Store<Feature<Point, { total_metric: string }>[]> = this._features | ||||
| 
 | ||||
|     public static readonly singleton = new ClusterGrouping() | ||||
| 
 | ||||
|     public readonly isDirty = new UIEventSource(false) | ||||
| 
 | ||||
|     private constructor() { | ||||
|         this.isDirty.stabilized(200).addCallback(dirty => { | ||||
|             if (dirty) { | ||||
|                 this.update() | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private allSource: Store<Feature<Point, { total: number, tile_id: number }>[]>[] = [] | ||||
| 
 | ||||
|     private update() { | ||||
|         const countPerTile = new Map<number, number>() | ||||
|         for (const source of this.allSource) { | ||||
|             for (const f of source.data) { | ||||
|                 const id = f.properties.tile_id | ||||
|                 const count = f.properties.total + (countPerTile.get(id) ?? 0) | ||||
|                 countPerTile.set(id, count) | ||||
|             } | ||||
|         } | ||||
|         const features: Feature<Point, { total_metric: string, id: string }>[] = [] | ||||
|         for (const tileId of countPerTile.keys()) { | ||||
|             const coordinates = Tiles.centerPointOf(tileId) | ||||
|             features.push({ | ||||
|                 type: "Feature", | ||||
|                 properties: { | ||||
|                     total_metric: "" + countPerTile.get(tileId), | ||||
|                     id: "clustered_all_" + tileId | ||||
|                 }, | ||||
|                 geometry: { | ||||
|                     type: "Point", | ||||
|                     coordinates | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
|         this._features.set(features) | ||||
|         this.isDirty.set(false) | ||||
|     } | ||||
| 
 | ||||
|     public registerSource(features: Store<Feature<Point, SummaryProperties>[]>) { | ||||
|         this.allSource.push(features) | ||||
|         features.addCallbackAndRun(() => { | ||||
|             //this.isDirty.set(true)
 | ||||
|             this.update() | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -18,9 +18,10 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource" | |||
| import NearbyFeatureSource from "../../Logic/FeatureSource/Sources/NearbyFeatureSource" | ||||
| import { | ||||
|     SummaryTileSource, | ||||
|     SummaryTileSourceRewriter, | ||||
|     SummaryTileSourceRewriter | ||||
| } from "../../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource" | ||||
| import { ShowDataLayerOptions } from "../../UI/Map/ShowDataLayerOptions" | ||||
| import { ClusterGrouping } from "../../Logic/FeatureSource/TiledFeatureSource/ClusteringFeatureSource" | ||||
| 
 | ||||
| export class WithSpecialLayers extends WithChangesState { | ||||
|     readonly favourites: FavouritesFeatureSource | ||||
|  | @ -61,6 +62,7 @@ export class WithSpecialLayers extends WithChangesState { | |||
|         this.closestFeatures.registerSource(this.favourites, "favourite") | ||||
| 
 | ||||
|         this.featureSummary = this.setupSummaryLayer() | ||||
|         this.setupClusterLayer() | ||||
|         this.initActorsSpecialLayers() | ||||
|         this.drawSelectedElement() | ||||
|         this.drawSpecialLayers() | ||||
|  | @ -128,6 +130,18 @@ export class WithSpecialLayers extends WithChangesState { | |||
|         return source | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * On high zoom levels, the clusters will be gathered in the GroupClustering. | ||||
|      * This method is responsible for drawing it | ||||
|      * @private | ||||
|      */ | ||||
|     private setupClusterLayer(): void { | ||||
|         new ShowDataLayer(this.map, { | ||||
|             features: ClusterGrouping.singleton, | ||||
|             layer: new LayerConfig(<LayerConfigJson>(<unknown>summaryLayer), "summaryLayer") | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     protected registerSpecialLayer(flayer: FilteredLayer, source: FeatureSource) { | ||||
|         if (!source?.features) { | ||||
|             return | ||||
|  |  | |||
|  | @ -53,11 +53,20 @@ export class Tiles { | |||
| 
 | ||||
|     /** | ||||
|      * Returns the centerpoint [lon, lat] of the specified tile | ||||
|      * @param z | ||||
|      * @param z OR tileId | ||||
|      * @param x | ||||
|      * @param y | ||||
|      */ | ||||
|     static centerPointOf(z: number, x: number, y: number): [number, number] { | ||||
|     static centerPointOf(z: number, x: number, y: number): [number, number] ; | ||||
|     static centerPointOf(tileId: number): [number, number] ; | ||||
| 
 | ||||
|     static centerPointOf(zOrId: number, x?: number, y?: number): [number, number] { | ||||
|         let z: number | ||||
|         if (x === undefined) { | ||||
|             [z, x, y] = Tiles.tile_from_index(zOrId) | ||||
|         } else { | ||||
|             z = zOrId | ||||
|         } | ||||
|         return [ | ||||
|             (Tiles.tile2long(x, z) + Tiles.tile2long(x + 1, z)) / 2, | ||||
|             (Tiles.tile2lat(y, z) + Tiles.tile2lat(y + 1, z)) / 2, | ||||
|  |  | |||
|  | @ -3,9 +3,9 @@ import type { AddLayerObject, Map as MlMap } from "maplibre-gl" | |||
| import { GeoJSONSource } from "maplibre-gl" | ||||
| import { ShowDataLayerOptions } from "./ShowDataLayerOptions" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import { FeatureSource, FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource" | ||||
| import { FeatureSource } from "../../Logic/FeatureSource/FeatureSource" | ||||
| import { BBox } from "../../Logic/BBox" | ||||
| import { Feature } from "geojson" | ||||
| import { Feature, Geometry } from "geojson" | ||||
| import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig" | ||||
| import { Utils } from "../../Utils" | ||||
| import * as range_layer from "../../../assets/layers/range/range.json" | ||||
|  | @ -16,7 +16,7 @@ import { TagsFilter } from "../../Logic/Tags/TagsFilter" | |||
| import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" | ||||
| import { PointRenderingLayer } from "./PointRenderingLayer" | ||||
| import { ClusteringFeatureSource } from "../../Logic/FeatureSource/TiledFeatureSource/ClusteringFeatureSource" | ||||
| import summaryLayer from "../../../public/assets/generated/layers/summary.json" | ||||
| import { OsmTags } from "../../Models/OsmFeature" | ||||
| 
 | ||||
| class LineRenderingLayer { | ||||
|     /** | ||||
|  | @ -361,7 +361,7 @@ export default class ShowDataLayer { | |||
|         layers: LayerConfig[], | ||||
|         options?: Partial<Omit<ShowDataLayerOptions, "features" | "layer">> | ||||
|     ) { | ||||
|         const perLayer: PerLayerFeatureSourceSplitter<FeatureSourceForLayer> = | ||||
|         const perLayer = | ||||
|             new PerLayerFeatureSourceSplitter( | ||||
|                 layers.filter((l) => l.source !== null).map((l) => new FilteredLayer(l)), | ||||
|                 features, | ||||
|  | @ -379,10 +379,10 @@ export default class ShowDataLayer { | |||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         perLayer.forEach((fs) => { | ||||
|         perLayer.forEach((features) => { | ||||
|             new ShowDataLayer(mlmap, { | ||||
|                 layer: fs.layer.layerDef, | ||||
|                 features: fs, | ||||
|                 layer: features.layer.layerDef, | ||||
|                 features, | ||||
|                 ...(options ?? {}), | ||||
|             }) | ||||
|         }) | ||||
|  | @ -396,14 +396,10 @@ export default class ShowDataLayer { | |||
|             const clustering = new ClusteringFeatureSource(feats, state.mapProperties.zoom.map(z => z + 2), | ||||
|                 options.layer.id, | ||||
|                 { | ||||
|                     cutoff: 2, | ||||
|                     cutoff: 7, | ||||
|                     showSummaryAt: "tilecenter" | ||||
|                 }) | ||||
|             new ShowDataLayer(mlmap, { | ||||
|                 features: clustering.summaryPoints, | ||||
|                 layer: new LayerConfig(<LayerConfigJson>(<unknown>summaryLayer), "summaryLayer") | ||||
|                 // doShowLayer: this.mapProperties.zoom.map((z) => z < maxzoom),
 | ||||
|             }) | ||||
| 
 | ||||
|             return clustering | ||||
|         } | ||||
|         new ShowDataLayer(mlmap, options) | ||||
|  | @ -411,7 +407,7 @@ export default class ShowDataLayer { | |||
| 
 | ||||
|     public static showRange( | ||||
|         map: Store<MlMap>, | ||||
|         features: FeatureSource, | ||||
|         features: FeatureSource<Feature<Geometry, OsmTags>>, | ||||
|         doShowLayer?: Store<boolean> | ||||
|     ): ShowDataLayer { | ||||
|         return new ShowDataLayer(map, { | ||||
|  |  | |||
|  | @ -1,12 +1,13 @@ | |||
| import { FeatureSource } from "../../Logic/FeatureSource/FeatureSource" | ||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import { Feature, Point } from "geojson" | ||||
| import { Feature, Geometry, Point } from "geojson" | ||||
| import { OsmTags } from "../../Models/OsmFeature" | ||||
| 
 | ||||
| export interface ShowDataLayerOptions { | ||||
|     /** | ||||
|      * Features to show | ||||
|      */ | ||||
|     features: FeatureSource | ||||
|     features: FeatureSource<Feature<Geometry, OsmTags>> | ||||
|     /** | ||||
|      * Indication of the current selected element; overrides some filters. | ||||
|      * When a feature is tapped, the feature will be put in there | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue