| 
									
										
										
										
											2023-11-16 03:32:04 +01:00
										 |  |  | 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" | 
					
						
							| 
									
										
										
										
											2023-11-16 03:32:04 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 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) | 
					
						
							| 
									
										
										
										
											2023-11-16 03:32:04 +01:00
										 |  |  |     private readonly _targetPoint: Store<{ lon: number; lat: number }> | 
					
						
							|  |  |  |     private readonly _numberOfNeededFeatures: number | 
					
						
							| 
									
										
										
										
											2023-11-22 19:39:19 +01:00
										 |  |  |     private readonly _layerState?: LayerState | 
					
						
							| 
									
										
										
										
											2023-11-16 03:32:04 +01:00
										 |  |  |     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 | 
					
						
							| 
									
										
										
										
											2023-11-16 03:32:04 +01:00
										 |  |  |     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-11-16 03:32:04 +01:00
										 |  |  |     ) { | 
					
						
							| 
									
										
										
										
											2023-12-21 17:36:43 +01:00
										 |  |  |         this._layerState = options?.layerState | 
					
						
							| 
									
										
										
										
											2023-11-16 03:32:04 +01:00
										 |  |  |         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-16 03:32:04 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-01 03:07:37 +02:00
										 |  |  |         this.features = Stores.listStabilized(this._result) | 
					
						
							| 
									
										
										
										
											2023-11-16 03:32:04 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											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-16 03:32:04 +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-16 03:32:04 +01:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-11-22 19:39:19 +01:00
										 |  |  |         const calcSource = this.createSource( | 
					
						
							|  |  |  |             source.features, | 
					
						
							|  |  |  |             flayer.layerDef.minzoom, | 
					
						
							|  |  |  |             flayer.isDisplayed | 
					
						
							|  |  |  |         ) | 
					
						
							| 
									
										
										
										
											2024-01-24 23:45:20 +01:00
										 |  |  |         calcSource.addCallbackAndRunD(() => { | 
					
						
							| 
									
										
										
										
											2023-11-22 19:39:19 +01:00
										 |  |  |             this.update() | 
					
						
							| 
									
										
										
										
											2023-11-16 03:32:04 +01:00
										 |  |  |         }) | 
					
						
							| 
									
										
										
										
											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)) | 
					
						
							| 
									
										
										
										
											2023-11-16 03:32:04 +01:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * 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)) | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2023-11-16 03:32:04 +01:00
										 |  |  |                 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] | 
					
						
							| 
									
										
										
										
											2023-11-16 03:32:04 +01:00
										 |  |  |         ) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |