MapComplete/src/Logic/FeatureSource/Sources/NearbyFeatureSource.ts

119 lines
4.4 KiB
TypeScript
Raw Normal View History

import { FeatureSource } from "../FeatureSource"
import { Store, Stores, UIEventSource } from "../../UIEventSource"
import { Feature } from "geojson"
import { GeoOperations } from "../../GeoOperations"
import FilteringFeatureSource from "./FilteringFeatureSource"
import LayerState from "../../State/LayerState"
2023-12-21 17:36:43 +01:00
import { BBox } from "../../BBox"
export default class NearbyFeatureSource implements FeatureSource {
public readonly features: Store<Feature[]>
2023-12-21 17:36:43 +01:00
private readonly _result = new UIEventSource<Feature[]>(undefined)
private readonly _targetPoint: Store<{ lon: number; lat: number }>
private readonly _numberOfNeededFeatures: number
2023-11-22 19:39:19 +01:00
private readonly _layerState?: LayerState
private readonly _currentZoom: Store<number>
2023-11-22 19:39:19 +01:00
private readonly _allSources: Store<{ feat: Feature; d: number }[]>[] = []
2023-12-21 17:36:43 +01:00
private readonly _bounds: Store<BBox> | undefined
constructor(
targetPoint: Store<{ lon: number; lat: number }>,
sources: ReadonlyMap<string, FilteringFeatureSource>,
2023-12-21 17:36:43 +01:00
options?: {
bounds?: Store<BBox>
numberOfNeededFeatures?: number
layerState?: LayerState
currentZoom?: Store<number>
}
) {
2023-12-21 17:36:43 +01:00
this._layerState = options?.layerState
this._targetPoint = targetPoint.stabilized(100)
2023-12-21 17:36:43 +01:00
this._numberOfNeededFeatures = options?.numberOfNeededFeatures
this._currentZoom = options?.currentZoom.stabilized(500)
this._bounds = options?.bounds
2023-11-22 19:39:19 +01:00
this.features = Stores.ListStabilized(this._result)
2023-11-30 00:31:26 +01:00
sources.forEach((source, layer) => {
this.registerSource(source, layer)
})
2023-11-22 19:39:19 +01:00
}
2023-11-22 19:39:19 +01:00
public registerSource(source: FeatureSource, layerId: string) {
const flayer = this._layerState?.filteredLayers.get(layerId)
if (!flayer) {
return
}
2023-11-22 19:39:19 +01:00
const calcSource = this.createSource(
source.features,
flayer.layerDef.minzoom,
flayer.isDisplayed
)
calcSource.addCallbackAndRunD((features) => {
this.update()
})
2023-11-22 19:39:19 +01:00
this._allSources.push(calcSource)
}
private update() {
let features: { feat: Feature; d: number }[] = []
for (const src of this._allSources) {
2023-12-21 17:36:43 +01:00
if (src.data === undefined) {
this._result.setData(undefined)
return // We cannot yet calculate all the features
}
2023-11-22 19:39:19 +01:00
features.push(...src.data)
}
features.sort((a, b) => a.d - b.d)
if (this._numberOfNeededFeatures !== undefined) {
features = features.slice(0, this._numberOfNeededFeatures)
}
this._result.setData(features.map((f) => f.feat))
}
/**
* Sorts the given source by distance, slices down to the required number
*/
private createSource(
source: Store<Feature[]>,
minZoom: number,
isActive?: Store<boolean>
): Store<{ feat: Feature; d: number }[]> {
const empty = []
return source.stabilized(100).map(
(feats) => {
if (isActive && !isActive.data) {
return empty
}
if (this._currentZoom.data < minZoom) {
return empty
}
2023-12-21 17:36:43 +01:00
if (this._bounds) {
const bbox = this._bounds.data
if (!bbox) {
// We have a 'bounds' store, but the bounds store itself is still empty
// As such, we cannot yet calculate which features are within the store
return undefined
}
feats = feats.filter((f) => bbox.overlapsWithFeature(f))
}
const point = this._targetPoint.data
const lonLat = <[number, number]>[point.lon, point.lat]
const withDistance = feats.map((feat) => ({
d: GeoOperations.distanceBetween(
lonLat,
GeoOperations.centerpointCoordinates(feat)
),
feat,
}))
withDistance.sort((a, b) => a.d - b.d)
if (this._numberOfNeededFeatures !== undefined) {
return withDistance.slice(0, this._numberOfNeededFeatures)
}
return withDistance
},
2023-12-21 17:36:43 +01:00
[this._targetPoint, isActive, this._currentZoom, this._bounds]
)
}
}