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 { /** * If the zoomlevel is (strictly) above the specified value, don't cluster no matter how many features the tile contains. */ dontClusterAboveZoom?: number /** * If the number of features in a _tile_ is equal or more then this number, * drop those features and emit a summary tile instead */ cutoff?: 20 | number } export class ClusteringFeatureSource = Feature> implements FeatureSource { public readonly summaryPoints: FeatureSource private readonly id: string features: Store /** *The clustering feature source works _only_ on points and is a preprocessing step for the ShowDataLayer. * If a tile contains many points, a 'summary' point is emitted instead in 'summaryPoints'. * The points from the summary will _not_ be emitted in 'this.features' in that case. * * We ignore the polygons, as polygons get smaller when zoomed out and thus don't clutter the map too much */ constructor(upstream: FeatureSource, currentZoomlevel: Store, id: string, options?: ClusteringOptions) { this.id = id 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[]>([]) currentZoomlevel = currentZoomlevel.stabilized(500) this.summaryPoints = new StaticFeatureSource(summaryPoints) this.features = (upstream.features.map(features => { if (!doCluster.data) { summaryPoints.set([]) return features } const z = currentZoomlevel.data const perTile = GeoOperations.spreadIntoBboxes(features, z) const resultingFeatures = [] const summary: Feature[] = [] for (const tileIndex of perTile.keys()) { const tileFeatures: Feature[] = perTile.get(tileIndex) if (tileFeatures.length > cutoff) { summary.push(this.createSummaryFeature(tileFeatures, tileIndex)) } else { resultingFeatures.push(...tileFeatures) } } summaryPoints.set(summary) return resultingFeatures }, [doCluster, currentZoomlevel])) } private createSummaryFeature(features: Feature[], tileId: number): Feature { const [z, x, y] = Tiles.tile_from_index(tileId) const [lon, lat] = Tiles.centerPointOf(z, x, y) return >{ type: "Feature", geometry: { type: "Point", coordinates: [lon, lat] }, properties: { id: "summary_" + this.id + "_" + tileId, z, total_metric: "" + features.length } } } }