import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; import {Store, UIEventSource} from "../../UIEventSource"; import FilteredLayer from "../../../Models/FilteredLayer"; import TileHierarchy from "./TileHierarchy"; import {Tiles} from "../../../Models/TileRange"; import {BBox} from "../../BBox"; /** * Contains all features in a tiled fashion. * The data will be automatically broken down into subtiles when there are too much features in a single tile or if the zoomlevel is too high */ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, FeatureSourceForLayer, TileHierarchy { public readonly z: number; public readonly x: number; public readonly y: number; public readonly parent: TiledFeatureSource; public readonly root: TiledFeatureSource public readonly layer: FilteredLayer; /* An index of all known tiles. allTiles[z][x][y].get('layerid') will yield the corresponding tile. * Only defined on the root element! */ public readonly loadedTiles: Map = undefined; public readonly maxFeatureCount: number; public readonly name; public readonly features: UIEventSource<{ feature: any, freshness: Date }[]> public readonly containedIds: Store> public readonly bbox: BBox; public readonly tileIndex: number; private upper_left: TiledFeatureSource private upper_right: TiledFeatureSource private lower_left: TiledFeatureSource private lower_right: TiledFeatureSource private readonly maxzoom: number; private readonly options: TiledFeatureSourceOptions private constructor(z: number, x: number, y: number, parent: TiledFeatureSource, options?: TiledFeatureSourceOptions) { this.z = z; this.x = x; this.y = y; this.bbox = BBox.fromTile(z, x, y) this.tileIndex = Tiles.tile_index(z, x, y) this.name = `TiledFeatureSource(${z},${x},${y})` this.parent = parent; this.layer = options.layer options = options ?? {} this.maxFeatureCount = options?.maxFeatureCount ?? 250; this.maxzoom = options.maxZoomLevel ?? 18 this.options = options; if (parent === undefined) { throw "Parent is not allowed to be undefined. Use null instead" } if (parent === null && z !== 0 && x !== 0 && y !== 0) { throw "Invalid root tile: z, x and y should all be null" } if (parent === null) { this.root = this; this.loadedTiles = new Map() } else { this.root = this.parent.root; this.loadedTiles = this.root.loadedTiles; const i = Tiles.tile_index(z, x, y) this.root.loadedTiles.set(i, this) } this.features = new UIEventSource([]) this.containedIds = this.features.map(features => { if (features === undefined) { return undefined; } return new Set(features.map(f => f.feature.properties.id)) }) // We register this tile, but only when there is some data in it if (this.options.registerTile !== undefined) { this.features.addCallbackAndRunD(features => { if (features.length === 0) { return; } this.options.registerTile(this) return true; }) } } public static createHierarchy(features: FeatureSource, options?: TiledFeatureSourceOptions): TiledFeatureSource { options = { ...options, layer: features["layer"] ?? options.layer } const root = new TiledFeatureSource(0, 0, 0, null, options) features.features?.addCallbackAndRunD(feats => root.addFeatures(feats)) return root; } private isSplitNeeded(featureCount: number) { if (this.upper_left !== undefined) { // This tile has been split previously, so we keep on splitting return true; } if (this.z >= this.maxzoom) { // We are not allowed to split any further return false } if (this.options.minZoomLevel !== undefined && this.z < this.options.minZoomLevel) { // We must have at least this zoom level before we are allowed to start splitting return true } // To much features - we split return featureCount > this.maxFeatureCount } /*** * Adds the list of features to this hierarchy. * If there are too much features, the list will be broken down and distributed over the subtiles (only retaining features that don't fit a subtile on this level) * @param features * @private */ private addFeatures(features: { feature: any, freshness: Date }[]) { if (features === undefined || features.length === 0) { return; } if (!this.isSplitNeeded(features.length)) { this.features.setData(features) return; } if (this.upper_left === undefined) { this.upper_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2, this, this.options) this.upper_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2, this, this.options) this.lower_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2 + 1, this, this.options) this.lower_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2 + 1, this, this.options) } const ulf = [] const urf = [] const llf = [] const lrf = [] const overlapsboundary = [] for (const feature of features) { const bbox = BBox.get(feature.feature) // There are a few strategies to deal with features that cross tile boundaries if (this.options.noDuplicates) { // Strategy 1: We put the feature into a somewhat matching tile if (bbox.overlapsWith(this.upper_left.bbox)) { ulf.push(feature) } else if (bbox.overlapsWith(this.upper_right.bbox)) { urf.push(feature) } else if (bbox.overlapsWith(this.lower_left.bbox)) { llf.push(feature) } else if (bbox.overlapsWith(this.lower_right.bbox)) { lrf.push(feature) } else { overlapsboundary.push(feature) } } else if (this.options.minZoomLevel === undefined) { // Strategy 2: put it into a strictly matching tile (or in this tile, which is slightly too big) if (bbox.isContainedIn(this.upper_left.bbox)) { ulf.push(feature) } else if (bbox.isContainedIn(this.upper_right.bbox)) { urf.push(feature) } else if (bbox.isContainedIn(this.lower_left.bbox)) { llf.push(feature) } else if (bbox.isContainedIn(this.lower_right.bbox)) { lrf.push(feature) } else { overlapsboundary.push(feature) } } else { // Strategy 3: We duplicate a feature on a boundary into every tile as we need to get to the minZoomLevel if (bbox.overlapsWith(this.upper_left.bbox)) { ulf.push(feature) } if (bbox.overlapsWith(this.upper_right.bbox)) { urf.push(feature) } if (bbox.overlapsWith(this.lower_left.bbox)) { llf.push(feature) } if (bbox.overlapsWith(this.lower_right.bbox)) { lrf.push(feature) } } } this.upper_left.addFeatures(ulf) this.upper_right.addFeatures(urf) this.lower_left.addFeatures(llf) this.lower_right.addFeatures(lrf) this.features.setData(overlapsboundary) } } export interface TiledFeatureSourceOptions { readonly maxFeatureCount?: number, readonly maxZoomLevel?: number, readonly minZoomLevel?: number, /** * IF minZoomLevel is set, and if a feature runs through a tile boundary, it would normally be duplicated. * Setting 'dontEnforceMinZoomLevel' will assign to feature to some matching subtile. */ readonly noDuplicates?: boolean, readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void, readonly layer?: FilteredLayer }