forked from MapComplete/MapComplete
87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
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<T extends Feature<Point> = Feature<Point>> implements FeatureSource<T> {
|
|
|
|
public readonly summaryPoints: FeatureSource
|
|
private readonly id: string
|
|
features: Store<T[]>
|
|
|
|
/**
|
|
*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<T>,
|
|
currentZoomlevel: Store<number>,
|
|
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<Feature<Point>[]>([])
|
|
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<Point>[] = []
|
|
for (const tileIndex of perTile.keys()) {
|
|
const tileFeatures: Feature<Point>[] = 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<Point>[], tileId: number): Feature<Point> {
|
|
const [z, x, y] = Tiles.tile_from_index(tileId)
|
|
const [lon, lat] = Tiles.centerPointOf(z, x, y)
|
|
return <Feature<Point>>{
|
|
type: "Feature",
|
|
geometry: {
|
|
type: "Point",
|
|
coordinates: [lon, lat]
|
|
},
|
|
properties: {
|
|
id: "summary_" + this.id + "_" + tileId,
|
|
z,
|
|
total_metric: "" + features.length
|
|
}
|
|
}
|
|
}
|
|
}
|