forked from MapComplete/MapComplete
		
	Huge refactoring of the feature pipeline, WIP
This commit is contained in:
		
							parent
							
								
									7793297348
								
							
						
					
					
						commit
						973b5d8bbe
					
				
					 25 changed files with 522 additions and 591 deletions
				
			
		
							
								
								
									
										35
									
								
								Logic/FeatureSource/Actors/LocalStorageSaverActor.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								Logic/FeatureSource/Actors/LocalStorageSaverActor.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | |||
| import {FeatureSourceForLayer} from "./FeatureSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| 
 | ||||
| /*** | ||||
|  * Saves all the features that are passed in to localstorage, so they can be retrieved on the next run | ||||
|  * | ||||
|  * Technically, more an Actor then a featuresource, but it fits more neatly this ay | ||||
|  */ | ||||
| export default class LocalStorageSaverActor { | ||||
|     public static readonly storageKey: string = "cached-features"; | ||||
| 
 | ||||
|     constructor(source: FeatureSourceForLayer, x: number, y: number, z: number) { | ||||
|         source.features.addCallbackAndRunD(features => { | ||||
|             const index = Utils.tile_index(z, x, y) | ||||
|             const key = `${LocalStorageSaverActor.storageKey}-${source.layer.layerDef.id}-${index}` | ||||
|             const now = new Date().getTime() | ||||
| 
 | ||||
|             if (features.length == 0) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 localStorage.setItem(key, JSON.stringify(features)); | ||||
|                 console.log("Saved ", features.length, "elements to", key) | ||||
|                 localStorage.setItem(key + "-time", JSON.stringify(now)) | ||||
|             } catch (e) { | ||||
|                 console.warn("Could not save the features to local storage:", e) | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -2,7 +2,7 @@ import FeatureSource from "./FeatureSource"; | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import State from "../../State"; | ||||
| 
 | ||||
| export default class RegisteringFeatureSource implements FeatureSource { | ||||
| export default class RegisteringAllFromFeatureSourceActor { | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly name; | ||||
| 
 | ||||
|  | @ -1,64 +0,0 @@ | |||
| import FeatureSource from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled) | ||||
|  * If this is the case, multiple objects with a different _matching_layer_id are generated. | ||||
|  * In any case, this featureSource marks the objects with _matching_layer_id | ||||
|  */ | ||||
| export default class FeatureDuplicatorPerLayer implements FeatureSource { | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
| 
 | ||||
|     public readonly name; | ||||
| 
 | ||||
|     constructor(layers: UIEventSource<FilteredLayer[]>, upstream: FeatureSource) { | ||||
|         this.name = "FeatureDuplicator of " + upstream.name; | ||||
|         this.features = upstream.features.map(features => { | ||||
|             const newFeatures: { feature: any, freshness: Date }[] = []; | ||||
|             if (features === undefined) { | ||||
|                 return newFeatures; | ||||
|             } | ||||
| 
 | ||||
|             for (const f of features) { | ||||
|                 if (f.feature._matching_layer_id) { | ||||
|                     // Already matched previously
 | ||||
|                     // We simply add it
 | ||||
|                     newFeatures.push(f); | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 let foundALayer = false; | ||||
|                 for (const layer of layers.data) { | ||||
|                     if (layer.layerDef.source.osmTags.matchesProperties(f.feature.properties)) { | ||||
|                         foundALayer = true; | ||||
|                         if (layer.layerDef.passAllFeatures) { | ||||
| 
 | ||||
|                             // We copy the feature; the "properties" field is kept identical though!
 | ||||
|                             // Keeping "properties" identical is needed, as it might break the 'allElementStorage' otherwise
 | ||||
|                             const newFeature = { | ||||
|                                 geometry: f.feature.geometry, | ||||
|                                 id: f.feature.id, | ||||
|                                 type: f.feature.type, | ||||
|                                 properties: f.feature.properties, | ||||
|                                 _matching_layer_id: layer.layerDef.id | ||||
|                             } | ||||
|                             newFeatures.push({feature: newFeature, freshness: f.freshness}); | ||||
|                         } else { | ||||
|                             // If not 'passAllFeatures', we are done
 | ||||
|                             f.feature._matching_layer_id = layer.layerDef.id; | ||||
|                             newFeatures.push(f); | ||||
|                             break; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             return newFeatures; | ||||
| 
 | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,162 +0,0 @@ | |||
| import FeatureSource from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import Hash from "../Web/Hash"; | ||||
| import {TagsFilter} from "../Tags/TagsFilter"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| 
 | ||||
| export default class FilteringFeatureSource implements FeatureSource { | ||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> = | ||||
|         new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||
|     public readonly name = "FilteringFeatureSource"; | ||||
| 
 | ||||
|     constructor( | ||||
|         layers: UIEventSource<{ | ||||
|             isDisplayed: UIEventSource<boolean>; | ||||
|             layerDef: LayerConfig; | ||||
|             appliedFilters: UIEventSource<TagsFilter>; | ||||
|         }[]>, | ||||
|         location: UIEventSource<Loc>, | ||||
|         selectedElement: UIEventSource<any>, | ||||
|         upstream: FeatureSource | ||||
|     ) { | ||||
|         const self = this; | ||||
| 
 | ||||
|         function update() { | ||||
|             const layerDict = {}; | ||||
|             if (layers.data.length == 0) { | ||||
|                 console.warn("No layers defined!"); | ||||
|                 return; | ||||
|             } | ||||
|             for (const layer of layers.data) { | ||||
|                 const prev = layerDict[layer.layerDef.id] | ||||
|                 if (prev !== undefined) { | ||||
|                     // We have seen this layer before!
 | ||||
|                     // We prefer the one which has a name
 | ||||
|                     if (layer.layerDef.name === undefined) { | ||||
|                         // This one is hidden, so we skip it
 | ||||
|                         console.log("Ignoring layer selection from ", layer) | ||||
|                         continue; | ||||
|                     } | ||||
|                 } | ||||
|                 layerDict[layer.layerDef.id] = layer; | ||||
|             } | ||||
| 
 | ||||
|             const features: { feature: any; freshness: Date }[] = | ||||
|                 upstream.features.data; | ||||
| 
 | ||||
|             const missingLayers = new Set<string>(); | ||||
| 
 | ||||
|             const newFeatures = features.filter((f) => { | ||||
|                 const layerId = f.feature._matching_layer_id; | ||||
| 
 | ||||
|                 if ( | ||||
|                     selectedElement.data?.id === f.feature.id || | ||||
|                     f.feature.id === Hash.hash.data) { | ||||
|                     // This is the selected object - it gets a free pass even if zoom is not sufficient or it is filtered away
 | ||||
|                     return true; | ||||
|                 } | ||||
| 
 | ||||
|                 if (layerId === undefined) { | ||||
|                     return false; | ||||
|                 } | ||||
|                 const layer: { | ||||
|                     isDisplayed: UIEventSource<boolean>; | ||||
|                     layerDef: LayerConfig; | ||||
|                     appliedFilters: UIEventSource<TagsFilter>; | ||||
|                 } = layerDict[layerId]; | ||||
|                 if (layer === undefined) { | ||||
|                     missingLayers.add(layerId); | ||||
|                     return false; | ||||
|                 } | ||||
| 
 | ||||
|                 const isShown = layer.layerDef.isShown; | ||||
|                 const tags = f.feature.properties; | ||||
|                 if (isShown.IsKnown(tags)) { | ||||
|                     const result = layer.layerDef.isShown.GetRenderValue( | ||||
|                         f.feature.properties | ||||
|                     ).txt; | ||||
|                     if (result !== "yes") { | ||||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 const tagsFilter = layer.appliedFilters.data; | ||||
|                 if (tagsFilter) { | ||||
|                     if (!tagsFilter.matchesProperties(f.feature.properties)) { | ||||
|                         // Hidden by the filter on the layer itself - we want to hide it no matter wat
 | ||||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
|                 if (!FilteringFeatureSource.showLayer(layer, location)) { | ||||
|                     // The layer itself is either disabled or hidden due to zoom constraints
 | ||||
|                     // We should return true, but it might still match some other layer
 | ||||
|                     return false; | ||||
|                 } | ||||
| 
 | ||||
|                 return true; | ||||
|             }); | ||||
| 
 | ||||
|             self.features.setData(newFeatures); | ||||
|             if (missingLayers.size > 0) { | ||||
|                 console.error( | ||||
|                     "Some layers were not found: ", | ||||
|                     Array.from(missingLayers) | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         upstream.features.addCallback(() => { | ||||
|             update(); | ||||
|         }); | ||||
|         location | ||||
|             .map((l) => { | ||||
|                 // We want something that is stable for the shown layers
 | ||||
|                 const displayedLayerIndexes = []; | ||||
|                 for (let i = 0; i < layers.data.length; i++) { | ||||
|                     const layer = layers.data[i]; | ||||
|                     if (l.zoom < layer.layerDef.minzoom) { | ||||
|                         continue; | ||||
|                     } | ||||
| 
 | ||||
|                     if (!layer.isDisplayed.data) { | ||||
|                         continue; | ||||
|                     } | ||||
|                     displayedLayerIndexes.push(i); | ||||
|                 } | ||||
|                 return displayedLayerIndexes.join(","); | ||||
|             }) | ||||
|             .addCallback(() => { | ||||
|                 update(); | ||||
|             }); | ||||
| 
 | ||||
|         layers.addCallback(update); | ||||
| 
 | ||||
|         const registered = new Set<UIEventSource<boolean>>(); | ||||
|         layers.addCallbackAndRun((layers) => { | ||||
|             for (const layer of layers) { | ||||
|                 if (registered.has(layer.isDisplayed)) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 registered.add(layer.isDisplayed); | ||||
|                 layer.isDisplayed.addCallback(() => update()); | ||||
|                 layer.appliedFilters.addCallback(() => update()); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         update(); | ||||
|     } | ||||
| 
 | ||||
|     private static showLayer( | ||||
|         layer: { | ||||
|             isDisplayed: UIEventSource<boolean>; | ||||
|             layerDef: LayerConfig; | ||||
|         }, | ||||
|         location: UIEventSource<Loc> | ||||
|     ) { | ||||
|         return ( | ||||
|             layer.isDisplayed.data && | ||||
|             layer.layerDef.minzoomVisible <= location.data.zoom | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | @ -1,207 +0,0 @@ | |||
| import FeatureSource from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import State from "../../State"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Fetches a geojson file somewhere and passes it along | ||||
|  */ | ||||
| export default class GeoJsonSource implements FeatureSource { | ||||
| 
 | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly name; | ||||
|     public readonly isOsmCache: boolean | ||||
|     private onFail: ((errorMsg: any, url: string) => void) = undefined; | ||||
|     private readonly layerId: string; | ||||
|     private readonly seenids: Set<string> = new Set<string>() | ||||
| 
 | ||||
|     private constructor(locationControl: UIEventSource<Loc>, | ||||
|                         flayer: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }, | ||||
|                         onFail?: ((errorMsg: any) => void)) { | ||||
|         this.layerId = flayer.layerDef.id; | ||||
|         let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); | ||||
|         this.name = "GeoJsonSource of " + url; | ||||
|         const zoomLevel = flayer.layerDef.source.geojsonZoomLevel; | ||||
| 
 | ||||
|         this.isOsmCache = flayer.layerDef.source.isOsmCacheLayer; | ||||
| 
 | ||||
|         this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||
| 
 | ||||
|         if (zoomLevel === undefined) { | ||||
|             // This is a classic, static geojson layer
 | ||||
|             if (onFail === undefined) { | ||||
|                 onFail = _ => { | ||||
|                 } | ||||
|             } | ||||
|             this.onFail = onFail; | ||||
| 
 | ||||
|             this.LoadJSONFrom(url) | ||||
|         } else { | ||||
|             this.ConfigureDynamicLayer(url, zoomLevel, locationControl, flayer) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Merges together the layers which have the same source | ||||
|      * @param flayers | ||||
|      * @param locationControl | ||||
|      * @constructor | ||||
|      */ | ||||
|     public static ConstructMultiSource(flayers: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[], locationControl: UIEventSource<Loc>): GeoJsonSource[] { | ||||
| 
 | ||||
|         const flayersPerSource = new Map<string, { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[]>(); | ||||
|         for (const flayer of flayers) { | ||||
|             const url = flayer.layerDef.source.geojsonSource?.replace(/{layer}/g, flayer.layerDef.id) | ||||
|             if (url === undefined) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             if (!flayersPerSource.has(url)) { | ||||
|                 flayersPerSource.set(url, []) | ||||
|             } | ||||
|             flayersPerSource.get(url).push(flayer) | ||||
|         } | ||||
| 
 | ||||
|         const sources: GeoJsonSource[] = [] | ||||
| 
 | ||||
|         flayersPerSource.forEach((flayers, key) => { | ||||
|             if (flayers.length == 1) { | ||||
|                 sources.push(new GeoJsonSource(locationControl, flayers[0])); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const zoomlevels = Utils.Dedup(flayers.map(flayer => "" + (flayer.layerDef.source.geojsonZoomLevel ?? ""))) | ||||
|             if (zoomlevels.length > 1) { | ||||
|                 throw "Multiple zoomlevels defined for same geojson source " + key | ||||
|             } | ||||
| 
 | ||||
|             let isShown = new UIEventSource<boolean>(true, "IsShown for multiple layers: or of multiple values"); | ||||
|             for (const flayer of flayers) { | ||||
|                 flayer.isDisplayed.addCallbackAndRun(() => { | ||||
|                     let value = false; | ||||
|                     for (const flayer of flayers) { | ||||
|                         value = flayer.isDisplayed.data || value; | ||||
|                     } | ||||
|                     isShown.setData(value); | ||||
|                 }); | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|             const source = new GeoJsonSource(locationControl, { | ||||
|                 isDisplayed: isShown, | ||||
|                 layerDef: flayers[0].layerDef // We only care about the source info here
 | ||||
|             }) | ||||
|             sources.push(source) | ||||
| 
 | ||||
|         }) | ||||
|         return sources; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private ConfigureDynamicLayer(url: string, zoomLevel: number, locationControl: UIEventSource<Loc>, flayer: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }) { | ||||
|         // This is a dynamic template with a fixed zoom level
 | ||||
|         url = url.replace("{z}", "" + zoomLevel) | ||||
|         const loadedTiles = new Set<string>(); | ||||
|         const self = this; | ||||
|         this.onFail = (msg, url) => { | ||||
|             console.warn(`Could not load geojson layer from`, url, "due to", msg) | ||||
|             loadedTiles.add(url); // We add the url to the 'loadedTiles' in order to not reload it in the future
 | ||||
|         } | ||||
| 
 | ||||
|         const neededTiles = locationControl.map( | ||||
|             location => { | ||||
|                 if (!flayer.isDisplayed.data) { | ||||
|                     // No need to download! - the layer is disabled
 | ||||
|                     return undefined; | ||||
|                 } | ||||
| 
 | ||||
|                 if (location.zoom < flayer.layerDef.minzoom) { | ||||
|                     // No need to download! - the layer is disabled
 | ||||
|                     return undefined; | ||||
|                 } | ||||
| 
 | ||||
|                 // Yup, this is cheating to just get the bounds here
 | ||||
|                 const bounds = State.state.leafletMap.data?.getBounds() | ||||
|                 if(bounds === undefined){ | ||||
|                     // We'll retry later
 | ||||
|                     return undefined | ||||
|                 } | ||||
|                 const tileRange = Utils.TileRangeBetween(zoomLevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) | ||||
|                 const needed = Utils.MapRange(tileRange, (x, y) => { | ||||
|                     return url.replace("{x}", "" + x).replace("{y}", "" + y); | ||||
|                 }) | ||||
|                 return new Set<string>(needed); | ||||
|             } | ||||
|             , [flayer.isDisplayed, State.state.leafletMap]); | ||||
|         neededTiles.stabilized(250).addCallback((needed: Set<string>) => { | ||||
|             if (needed === undefined) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             needed.forEach(neededTile => { | ||||
|                 if (loadedTiles.has(neededTile)) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 loadedTiles.add(neededTile) | ||||
|                 self.LoadJSONFrom(neededTile) | ||||
| 
 | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private LoadJSONFrom(url: string) { | ||||
|         const eventSource = this.features; | ||||
|         const self = this; | ||||
|         Utils.downloadJson(url) | ||||
|             .then(json => { | ||||
|                 if (json.elements === [] && json.remarks.indexOf("runtime error") > 0) { | ||||
|                     self.onFail("Runtime error (timeout)", url) | ||||
|                     return; | ||||
|                 } | ||||
|                 const time = new Date(); | ||||
|                 const newFeatures: { feature: any, freshness: Date } [] = [] | ||||
|                 let i = 0; | ||||
|                 let skipped = 0; | ||||
|                 for (const feature of json.features) { | ||||
|                     const props = feature.presets | ||||
|                     for (const key in props) { | ||||
|                         if(typeof props[key] !== "string"){ | ||||
|                             props[key] = ""+props[key] | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     if (props.id === undefined) { | ||||
|                         props.id = url + "/" + i; | ||||
|                         feature.id = url + "/" + i; | ||||
|                         i++; | ||||
|                     } | ||||
|                     if (self.seenids.has(props.id)) { | ||||
|                         skipped++; | ||||
|                         continue; | ||||
|                     } | ||||
|                     self.seenids.add(props.id) | ||||
| 
 | ||||
|                     let freshness: Date = time; | ||||
|                     if (feature.properties["_last_edit:timestamp"] !== undefined) { | ||||
|                         freshness = new Date(props["_last_edit:timestamp"]) | ||||
|                     } | ||||
| 
 | ||||
|                     newFeatures.push({feature: feature, freshness: freshness}) | ||||
|                 } | ||||
|                 console.debug("Downloaded " + newFeatures.length + " new features and " + skipped + " already seen features from " + url); | ||||
| 
 | ||||
|                 if (newFeatures.length == 0) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 eventSource.setData(eventSource.data.concat(newFeatures)) | ||||
| 
 | ||||
|             }).catch(msg => self.onFail(msg, url)) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,41 +0,0 @@ | |||
| /*** | ||||
|  * Saves all the features that are passed in to localstorage, so they can be retrieved on the next run | ||||
|  * | ||||
|  * Technically, more an Actor then a featuresource, but it fits more neatly this ay | ||||
|  */ | ||||
| import FeatureSource from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| 
 | ||||
| export default class LocalStorageSaver implements FeatureSource { | ||||
|     public static readonly storageKey: string = "cached-features"; | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
| 
 | ||||
|     public readonly name = "LocalStorageSaver"; | ||||
| 
 | ||||
|     constructor(source: FeatureSource, layout: UIEventSource<LayoutConfig>) { | ||||
|         this.features = source.features; | ||||
| 
 | ||||
|         this.features.addCallbackAndRunD(features => { | ||||
|             const now = new Date().getTime() | ||||
|             features = features.filter(f => layout.data.cacheTimeout > Math.abs(now - f.freshness.getTime()) / 1000) | ||||
| 
 | ||||
| 
 | ||||
|             if (features.length == 0) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 const key = LocalStorageSaver.storageKey + layout.data.id | ||||
|                 localStorage.setItem(key, JSON.stringify(features)); | ||||
|                 console.log("Saved ", features.length, "elements to", key) | ||||
|             } catch (e) { | ||||
|                 console.warn("Could not save the features to local storage:", e) | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,52 +0,0 @@ | |||
| import FeatureSource from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import State from "../../State"; | ||||
| import Hash from "../Web/Hash"; | ||||
| import MetaTagging from "../MetaTagging"; | ||||
| 
 | ||||
| export default class MetaTaggingFeatureSource implements FeatureSource { | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>(undefined); | ||||
| 
 | ||||
|     public readonly name; | ||||
| 
 | ||||
|     /*** | ||||
|      * Constructs a new metatagger which'll calculate various tags | ||||
|      * @param allFeaturesSource: A source where all the currently known features can be found - used to calculate overlaps etc | ||||
|      * @param source: the source of features that should get their metatag and which should be exported again | ||||
|      * @param updateTrigger | ||||
|      */ | ||||
|     constructor(allFeaturesSource: UIEventSource<{ feature: any; freshness: Date }[]>, source: FeatureSource, updateTrigger?: UIEventSource<any>) { | ||||
|         const self = this; | ||||
|         this.name = "MetaTagging of " + source.name | ||||
| 
 | ||||
|         if (allFeaturesSource === undefined) { | ||||
|             throw ("UIEVentSource is undefined") | ||||
|         } | ||||
| 
 | ||||
|         function update() { | ||||
|             const featuresFreshness = source.features.data | ||||
|             if (featuresFreshness === undefined) { | ||||
|                 return; | ||||
|             } | ||||
|             featuresFreshness.forEach(featureFresh => { | ||||
|                 const feature = featureFresh.feature; | ||||
| 
 | ||||
|                 if (Hash.hash.data === feature.properties.id) { | ||||
|                     State.state.selectedElement.setData(feature); | ||||
|                 } | ||||
|             }) | ||||
| 
 | ||||
|             MetaTagging.addMetatags(featuresFreshness, | ||||
|                 allFeaturesSource, | ||||
|                 State.state.knownRelations.data, State.state.layoutToUse.data.layers); | ||||
|             self.features.setData(featuresFreshness); | ||||
|         } | ||||
| 
 | ||||
|         source.features.addCallbackAndRun(_ => update()); | ||||
|         updateTrigger?.addCallback(_ => { | ||||
|             console.debug("Updating because of external call") | ||||
|             update(); | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										87
									
								
								Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,87 @@ | |||
| import FeatureSource from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import OverpassFeatureSource from "../Actors/OverpassFeatureSource"; | ||||
| import SimpleFeatureSource from "./SimpleFeatureSource"; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled) | ||||
|  * If this is the case, multiple objects with a different _matching_layer_id are generated. | ||||
|  * In any case, this featureSource marks the objects with _matching_layer_id | ||||
|  */ | ||||
| export default class PerLayerFeatureSourceSplitter { | ||||
| 
 | ||||
|     constructor(layers: UIEventSource<FilteredLayer[]>, | ||||
|                 handleLayerData: (source: FeatureSource) => void, | ||||
|                 upstream: OverpassFeatureSource) { | ||||
| 
 | ||||
|         const knownLayers = new Map<string, FeatureSource>() | ||||
| 
 | ||||
|         function update() { | ||||
|             const features = upstream.features.data; | ||||
|             if (features === undefined) { | ||||
|                 return; | ||||
|             } | ||||
|             if(layers.data === undefined){ | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // We try to figure out (for each feature) in which feature store it should be saved.
 | ||||
|             // Note that this splitter is only run when it is invoked by the overpass feature source, so we can't be sure in which layer it should go
 | ||||
| 
 | ||||
|             const featuresPerLayer = new Map<string, { feature, freshness } []>(); | ||||
| 
 | ||||
|             function addTo(layer: FilteredLayer, feature: { feature, freshness }) { | ||||
|                 const id = layer.layerDef.id | ||||
|                 const list = featuresPerLayer.get(id) | ||||
|                 if (list !== undefined) { | ||||
|                     list.push(feature) | ||||
|                 } else { | ||||
|                     featuresPerLayer.set(id, [feature]) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             for (const f of features) { | ||||
|                 for (const layer of layers.data) { | ||||
|                     if (layer.layerDef.source.osmTags.matchesProperties(f.feature.properties)) { | ||||
|                         // We have found our matching layer!
 | ||||
|                         addTo(layer, f) | ||||
|                         if (!layer.layerDef.passAllFeatures) { | ||||
|                             // If not 'passAllFeatures', we are done for this feature
 | ||||
|                             break; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // At this point, we have our features per layer as a list
 | ||||
|             // We assign them to the correct featureSources
 | ||||
|             for (const layer of layers.data) { | ||||
|                 const id = layer.layerDef.id; | ||||
|                 const features = featuresPerLayer.get(id) | ||||
|                 if (features === undefined) { | ||||
|                     // No such features for this layer
 | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 let featureSource = knownLayers.get(id) | ||||
|                 if (featureSource === undefined) { | ||||
|                     // Not yet initialized - now is a good time
 | ||||
|                     featureSource = new SimpleFeatureSource(layer) | ||||
|                     knownLayers.set(id, featureSource) | ||||
|                     handleLayerData(featureSource) | ||||
|                 } | ||||
|                 featureSource.features.setData(features) | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             upstream.features.addCallbackAndRunD(_ => update()) | ||||
|             layers.addCallbackAndRunD(_ => update()) | ||||
| 
 | ||||
|         } | ||||
|          | ||||
|         layers.addCallbackAndRunD(_ => update()) | ||||
|         upstream.features.addCallbackAndRunD(_ => update()) | ||||
|     } | ||||
| } | ||||
|  | @ -1,27 +1,44 @@ | |||
| import FeatureSource from "./FeatureSource"; | ||||
| import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| 
 | ||||
| /** | ||||
|  * Merges features from different featureSources | ||||
|  * Merges features from different featureSources for a single layer | ||||
|  * Uses the freshest feature available in the case multiple sources offer data with the same identifier | ||||
|  */ | ||||
| export default class FeatureSourceMerger implements FeatureSource { | ||||
| export default class FeatureSourceMerger implements FeatureSourceForLayer { | ||||
| 
 | ||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||
|     public readonly name; | ||||
|     private readonly _sources: FeatureSource[]; | ||||
|     public readonly layer: FilteredLayer | ||||
|     private readonly _sources: UIEventSource<FeatureSource[]>; | ||||
| 
 | ||||
|     constructor(sources: FeatureSource[]) { | ||||
|     constructor(layer: FilteredLayer ,sources: UIEventSource<FeatureSource[]>) { | ||||
|         this._sources = sources; | ||||
|         this.name = "SourceMerger of (" + sources.map(s => s.name).join(", ") + ")" | ||||
|         this.layer = layer; | ||||
|         this.name = "SourceMerger" | ||||
|         const self = this; | ||||
| 
 | ||||
|         const handledSources = new Set<FeatureSource>(); | ||||
| 
 | ||||
|         sources.addCallbackAndRunD(sources => { | ||||
|             let newSourceRegistered = false; | ||||
|             for (let i = 0; i < sources.length; i++) { | ||||
|                 let source = sources[i]; | ||||
|                 if (handledSources.has(source)) { | ||||
|                     continue | ||||
|                 } | ||||
|                 handledSources.add(source) | ||||
|                 newSourceRegistered = true | ||||
|                 source.features.addCallback(() => { | ||||
|                     self.Update(); | ||||
|                 }); | ||||
|                 if (newSourceRegistered) { | ||||
|                     self.Update(); | ||||
|                 } | ||||
|         this.Update(); | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private Update() { | ||||
|  | @ -34,7 +51,7 @@ export default class FeatureSourceMerger implements FeatureSource { | |||
|             all.set(oldValue.feature.id + oldValue.feature._matching_layer_id, oldValue) | ||||
|         } | ||||
| 
 | ||||
|         for (const source of this._sources) { | ||||
|         for (const source of this._sources.data) { | ||||
|             if (source?.features?.data === undefined) { | ||||
|                 continue; | ||||
|             } | ||||
|  | @ -64,7 +81,7 @@ export default class FeatureSourceMerger implements FeatureSource { | |||
|         } | ||||
| 
 | ||||
|         const newList = []; | ||||
|         all.forEach((value, key) => { | ||||
|         all.forEach((value, _) => { | ||||
|             newList.push(value) | ||||
|         }) | ||||
|         this.features.setData(newList); | ||||
							
								
								
									
										101
									
								
								Logic/FeatureSource/Sources/FilteringFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								Logic/FeatureSource/Sources/FilteringFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,101 @@ | |||
| import {FeatureSourceForLayer} from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import Hash from "../Web/Hash"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| 
 | ||||
| export default class FilteringFeatureSource implements FeatureSourceForLayer { | ||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> = | ||||
|         new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||
|     public readonly name = "FilteringFeatureSource"; | ||||
|     public readonly layer: FilteredLayer; | ||||
| 
 | ||||
|     constructor( | ||||
|         state: { | ||||
|             locationControl: UIEventSource<{ zoom: number }>, | ||||
|             selectedElement: UIEventSource<any>, | ||||
|         }, | ||||
|         upstream: FeatureSourceForLayer | ||||
|     ) { | ||||
|         const self = this; | ||||
| 
 | ||||
|         this.layer = upstream.layer; | ||||
|         const layer = upstream.layer; | ||||
| 
 | ||||
|         function update() { | ||||
|             const features: { feature: any; freshness: Date }[] = upstream.features.data; | ||||
|             const newFeatures = features.filter((f) => { | ||||
|                 if ( | ||||
|                     state.selectedElement.data?.id === f.feature.id || | ||||
|                     f.feature.id === Hash.hash.data) { | ||||
|                     // This is the selected object - it gets a free pass even if zoom is not sufficient or it is filtered away
 | ||||
|                     return true; | ||||
|                 } | ||||
| 
 | ||||
|                 const isShown = layer.layerDef.isShown; | ||||
|                 const tags = f.feature.properties; | ||||
|                 if (isShown.IsKnown(tags)) { | ||||
|                     const result = layer.layerDef.isShown.GetRenderValue( | ||||
|                         f.feature.properties | ||||
|                     ).txt; | ||||
|                     if (result !== "yes") { | ||||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 const tagsFilter = layer.appliedFilters.data; | ||||
|                 if (tagsFilter) { | ||||
|                     if (!tagsFilter.matchesProperties(f.feature.properties)) { | ||||
|                         // Hidden by the filter on the layer itself - we want to hide it no matter wat
 | ||||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
|                 if (!FilteringFeatureSource.showLayer(layer, state.locationControl.data)) { | ||||
|                     // The layer itself is either disabled or hidden due to zoom constraints
 | ||||
|                     // We should return true, but it might still match some other layer
 | ||||
|                     return false; | ||||
|                 } | ||||
|                 return true; | ||||
|             }); | ||||
| 
 | ||||
|             self.features.setData(newFeatures); | ||||
|         } | ||||
| 
 | ||||
|         upstream.features.addCallback(() => { | ||||
|             update(); | ||||
|         }); | ||||
| 
 | ||||
|         let isShown = state.locationControl.map((l) => FilteringFeatureSource.showLayer(layer, l), | ||||
|             [layer.isDisplayed]) | ||||
|              | ||||
|         isShown.addCallback(isShown => { | ||||
|             if (isShown) { | ||||
|                 update(); | ||||
|             } else { | ||||
|                 self.features.setData([]) | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         layer.appliedFilters.addCallback(_ => { | ||||
|             if(!isShown.data){ | ||||
|                 // Currently not shown.
 | ||||
|                 // Note that a change in 'isSHown' will trigger an update as well, so we don't have to watch it another time
 | ||||
|                 return; | ||||
|             } | ||||
|             update() | ||||
|         }) | ||||
| 
 | ||||
|         update(); | ||||
|     } | ||||
| 
 | ||||
|     private static showLayer( | ||||
|         layer: { | ||||
|             isDisplayed: UIEventSource<boolean>; | ||||
|             layerDef: LayerConfig; | ||||
|         }, | ||||
|         location: { zoom: number }) { | ||||
|         return layer.isDisplayed.data && | ||||
|             layer.layerDef.minzoomVisible <= location.zoom; | ||||
| 
 | ||||
|     } | ||||
| } | ||||
							
								
								
									
										95
									
								
								Logic/FeatureSource/Sources/GeoJsonSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								Logic/FeatureSource/Sources/GeoJsonSource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,95 @@ | |||
| import {FeatureSourceForLayer} from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import {control} from "leaflet"; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Fetches a geojson file somewhere and passes it along | ||||
|  */ | ||||
| export default class GeoJsonSource implements FeatureSourceForLayer { | ||||
| 
 | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly name; | ||||
|     public readonly isOsmCache: boolean | ||||
|     private onFail: ((errorMsg: any, url: string) => void) = undefined; | ||||
|     private readonly seenids: Set<string> = new Set<string>() | ||||
|     public readonly layer: FilteredLayer; | ||||
| 
 | ||||
| 
 | ||||
|     public constructor(flayer: FilteredLayer, | ||||
|                        zxy?: [number, number, number]) { | ||||
| 
 | ||||
|         if (flayer.layerDef.source.geojsonZoomLevel !== undefined && zxy === undefined) { | ||||
|             throw "Dynamic layers are not supported. Use 'DynamicGeoJsonTileSource instead" | ||||
|         } | ||||
| 
 | ||||
|         this.layer = flayer; | ||||
|         let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); | ||||
|         if (zxy !== undefined) { | ||||
|             url = url | ||||
|                 .replace('{z}', "" + zxy[0]) | ||||
|                 .replace('{x}', "" + zxy[1]) | ||||
|                 .replace('{y}', "" + zxy[2]) | ||||
|         } | ||||
| 
 | ||||
|         this.name = "GeoJsonSource of " + url; | ||||
| 
 | ||||
|         this.isOsmCache = flayer.layerDef.source.isOsmCacheLayer; | ||||
|         this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||
|         this.LoadJSONFrom(url) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private LoadJSONFrom(url: string) { | ||||
|         const eventSource = this.features; | ||||
|         const self = this; | ||||
|         Utils.downloadJson(url) | ||||
|             .then(json => { | ||||
|                 if (json.elements === [] && json.remarks.indexOf("runtime error") > 0) { | ||||
|                     self.onFail("Runtime error (timeout)", url) | ||||
|                     return; | ||||
|                 } | ||||
|                 const time = new Date(); | ||||
|                 const newFeatures: { feature: any, freshness: Date } [] = [] | ||||
|                 let i = 0; | ||||
|                 let skipped = 0; | ||||
|                 for (const feature of json.features) { | ||||
|                     const props = feature.properties | ||||
|                     for (const key in props) { | ||||
|                         if (typeof props[key] !== "string") { | ||||
|                             props[key] = "" + props[key] | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     if (props.id === undefined) { | ||||
|                         props.id = url + "/" + i; | ||||
|                         feature.id = url + "/" + i; | ||||
|                         i++; | ||||
|                     } | ||||
|                     if (self.seenids.has(props.id)) { | ||||
|                         skipped++; | ||||
|                         continue; | ||||
|                     } | ||||
|                     self.seenids.add(props.id) | ||||
| 
 | ||||
|                     let freshness: Date = time; | ||||
|                     if (feature.properties["_last_edit:timestamp"] !== undefined) { | ||||
|                         freshness = new Date(props["_last_edit:timestamp"]) | ||||
|                     } | ||||
| 
 | ||||
|                     newFeatures.push({feature: feature, freshness: freshness}) | ||||
|                 } | ||||
|                 console.debug("Downloaded " + newFeatures.length + " new features and " + skipped + " already seen features from " + url); | ||||
| 
 | ||||
|                 if (newFeatures.length == 0) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 eventSource.setData(eventSource.data.concat(newFeatures)) | ||||
| 
 | ||||
|             }).catch(msg => console.error("Could not load geojon layer", url, "due to", msg)) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,6 +1,6 @@ | |||
| import FeatureSource from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import LocalStorageSaver from "./LocalStorageSaver"; | ||||
| import LocalStorageSaverActor from "./LocalStorageSaverActor"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| 
 | ||||
| export default class LocalStorageSource implements FeatureSource { | ||||
|  | @ -9,7 +9,7 @@ export default class LocalStorageSource implements FeatureSource { | |||
| 
 | ||||
|     constructor(layout: UIEventSource<LayoutConfig>) { | ||||
|         this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||
|         const key = LocalStorageSaver.storageKey + layout.data.id | ||||
|         const key = LocalStorageSaverActor.storageKey + layout.data.id | ||||
|         layout.addCallbackAndRun(_ => { | ||||
|             try { | ||||
|                 const fromStorage = localStorage.getItem(key); | ||||
|  | @ -4,7 +4,6 @@ import {OsmObject} from "../Osm/OsmObject"; | |||
| import {Utils} from "../../Utils"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| 
 | ||||
| 
 | ||||
| export default class OsmApiFeatureSource implements FeatureSource { | ||||
|  | @ -15,19 +14,23 @@ export default class OsmApiFeatureSource implements FeatureSource { | |||
|         leafletMap: UIEventSource<any>; | ||||
|         locationControl: UIEventSource<Loc>, filteredLayers: UIEventSource<FilteredLayer[]>}; | ||||
| 
 | ||||
|     constructor(minZoom = undefined, state: {locationControl: UIEventSource<Loc>, filteredLayers: UIEventSource<FilteredLayer[]>, leafletMap: UIEventSource<any>}) { | ||||
|     constructor(state: {locationControl: UIEventSource<Loc>, filteredLayers: UIEventSource<FilteredLayer[]>, leafletMap: UIEventSource<any>, | ||||
|     overpassMaxZoom: UIEventSource<number>}) { | ||||
|         this._state = state; | ||||
|         if(minZoom !== undefined){ | ||||
|         const self = this; | ||||
|         function update(){ | ||||
|             const minZoom = state.overpassMaxZoom.data; | ||||
|             const location = state.locationControl.data | ||||
|             if(minZoom === undefined || location === undefined){ | ||||
|                 return; | ||||
|             } | ||||
|             if(minZoom < 14){ | ||||
|                 throw "MinZoom should be at least 14 or higher, OSM-api won't work otherwise" | ||||
|             } | ||||
|             const self = this; | ||||
|             state.locationControl.addCallbackAndRunD(location => { | ||||
|             if(location.zoom > minZoom){ | ||||
|                 return; | ||||
|             } | ||||
|             self.loadArea() | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -59,10 +62,6 @@ export default class OsmApiFeatureSource implements FeatureSource { | |||
|         if (disabledLayers.length > 0) { | ||||
|             return false; | ||||
|         } | ||||
|         const loc = this._state.locationControl.data; | ||||
|         if (loc.zoom < Constants.useOsmApiAt) { | ||||
|             return false; | ||||
|         } | ||||
|         if (this._state.leafletMap.data === undefined) { | ||||
|             return false; // Not yet inited
 | ||||
|         } | ||||
|  | @ -1,12 +1,14 @@ | |||
| /** | ||||
|  * Every previously added point is remembered, but new points are added | ||||
|  */ | ||||
| import FeatureSource from "./FeatureSource"; | ||||
| 
 | ||||
| import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| 
 | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| /** | ||||
|  * Every previously added point is remembered, but new points are added. | ||||
|  * Data coming from upstream will always overwrite a previous value | ||||
|  */ | ||||
| export default class RememberingSource implements FeatureSource { | ||||
|     public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>; | ||||
| 
 | ||||
|     public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>; | ||||
|     public readonly name; | ||||
| 
 | ||||
|     constructor(source: FeatureSource) { | ||||
|  | @ -20,9 +22,9 @@ export default class RememberingSource implements FeatureSource { | |||
|             } | ||||
| 
 | ||||
|             // Then new ids
 | ||||
|             const ids = new Set<string>(features.map(f => f.feature.properties.id + f.feature.geometry.type + f.feature._matching_layer_id)); | ||||
|             const ids = new Set<string>(features.map(f => f.feature.properties.id + f.feature.geometry.type)); | ||||
|             // the old data
 | ||||
|             const oldData = oldFeatures.filter(old => !ids.has(old.feature.properties.id + old.feature.geometry.type + old.feature._matching_layer_id)) | ||||
|             const oldData = oldFeatures.filter(old => !ids.has(old.feature.properties.id + old.feature.geometry.type)) | ||||
|             return [...features, ...oldData]; | ||||
|         }) | ||||
|     } | ||||
							
								
								
									
										16
									
								
								Logic/FeatureSource/Sources/SimpleFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								Logic/FeatureSource/Sources/SimpleFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | |||
| import {FeatureSourceForLayer} from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| 
 | ||||
| export default class SimpleFeatureSource implements FeatureSourceForLayer { | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||
|     public readonly name: string = "SimpleFeatureSource"; | ||||
|     public readonly layer: FilteredLayer; | ||||
| 
 | ||||
|     constructor(layer: FilteredLayer) { | ||||
|         this.name = "SimpleFeatureSource("+layer.layerDef.id+")" | ||||
|         this.layer = layer | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,4 +1,4 @@ | |||
| import FeatureSource from "./FeatureSource"; | ||||
| import {FeatureSourceForLayer} from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {GeoOperations} from "../GeoOperations"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
|  | @ -6,39 +6,31 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | |||
| /** | ||||
|  * This is the part of the pipeline which introduces extra points at the center of an area (but only if this is demanded by the wayhandling) | ||||
|  */ | ||||
| export default class WayHandlingApplyingFeatureSource implements FeatureSource { | ||||
| export default class WayHandlingApplyingFeatureSource implements FeatureSourceForLayer { | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly name; | ||||
|     public readonly layer; | ||||
| 
 | ||||
|     constructor(layers: UIEventSource<{ | ||||
|                     layerDef: LayerConfig | ||||
|                 }[]>, | ||||
|                 upstream: FeatureSource) { | ||||
|     constructor(upstream: FeatureSourceForLayer) { | ||||
|         this.name = "Wayhandling of " + upstream.name; | ||||
|         this.layer = upstream.layer | ||||
|         const layer = upstream.layer.layerDef; | ||||
|          | ||||
|         if (layer.wayHandling === LayerConfig.WAYHANDLING_DEFAULT) { | ||||
|             // We don't have to do anything fancy
 | ||||
|             // lets just wire up the upstream
 | ||||
|             this.features = upstream.features; | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.features = upstream.features.map( | ||||
|             features => { | ||||
|                 if (features === undefined) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 const layerDict = {}; | ||||
|                 let allDefaultWayHandling = true; | ||||
|                 for (const layer of layers.data) { | ||||
|                     layerDict[layer.layerDef.id] = layer; | ||||
|                     if (layer.layerDef.wayHandling !== LayerConfig.WAYHANDLING_DEFAULT) { | ||||
|                         allDefaultWayHandling = false; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 const newFeatures: { feature: any, freshness: Date }[] = []; | ||||
|                 for (const f of features) { | ||||
|                     const feat = f.feature; | ||||
|                     const layerId = feat._matching_layer_id; | ||||
|                     const layer: LayerConfig = layerDict[layerId].layerDef; | ||||
|                     if (layer === undefined) { | ||||
|                         console.error("No layer found with id " + layerId); | ||||
|                         continue; | ||||
|                     } | ||||
| 
 | ||||
|                     if (layer.wayHandling === LayerConfig.WAYHANDLING_DEFAULT) { | ||||
|                         newFeatures.push(f); | ||||
|  | @ -47,19 +39,17 @@ export default class WayHandlingApplyingFeatureSource implements FeatureSource { | |||
| 
 | ||||
|                     if (feat.geometry.type === "Point") { | ||||
|                         newFeatures.push(f); | ||||
|                         // it is a point, nothing to do here
 | ||||
|                         // feature is a point, nothing to do here
 | ||||
|                         continue; | ||||
|                     } | ||||
| 
 | ||||
|                     // Create the copy
 | ||||
|                     const centerPoint = GeoOperations.centerpoint(feat); | ||||
|                     centerPoint["_matching_layer_id"] = feat._matching_layer_id; | ||||
| 
 | ||||
|                     newFeatures.push({feature: centerPoint, freshness: f.freshness}); | ||||
|                     if (layer.wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY) { | ||||
|                         newFeatures.push(f); | ||||
|                     } | ||||
| 
 | ||||
|                 } | ||||
|                 return newFeatures; | ||||
|             } | ||||
							
								
								
									
										72
									
								
								Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,72 @@ | |||
| /*** | ||||
|  * A tiled source which dynamically loads the required tiles | ||||
|  */ | ||||
| import State from "../../../State"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer} from "../FeatureSource"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import Loc from "../../../Models/Loc"; | ||||
| 
 | ||||
| export default class DynamicTileSource { | ||||
|     private readonly _loadedTiles = new Set<number>(); | ||||
|      | ||||
|     public readonly existingTiles: Map<number, Map<number, FeatureSourceForLayer>> = new Map<number, Map<number, FeatureSourceForLayer>>() | ||||
| 
 | ||||
|     constructor( | ||||
|         layer: FilteredLayer, | ||||
|         zoomlevel: number, | ||||
|         constructTile: (xy: [number, number]) => FeatureSourceForLayer, | ||||
|         state: { | ||||
|             locationControl: UIEventSource<Loc> | ||||
|             leafletMap: any | ||||
|         } | ||||
|     ) { | ||||
|         state = State.state | ||||
|         const self = this; | ||||
|         const neededTiles = state.locationControl.map( | ||||
|             location => { | ||||
|                 if (!layer.isDisplayed.data) { | ||||
|                     // No need to download! - the layer is disabled
 | ||||
|                     return undefined; | ||||
|                 } | ||||
| 
 | ||||
|                 if (location.zoom < layer.layerDef.minzoom) { | ||||
|                     // No need to download! - the layer is disabled
 | ||||
|                     return undefined; | ||||
|                 } | ||||
| 
 | ||||
|                 // Yup, this is cheating to just get the bounds here
 | ||||
|                 const bounds = state.leafletMap.data?.getBounds() | ||||
|                 if (bounds === undefined) { | ||||
|                     // We'll retry later
 | ||||
|                     return undefined | ||||
|                 } | ||||
|                 const tileRange = Utils.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) | ||||
| 
 | ||||
|                 const needed = Utils.MapRange(tileRange, (x, y) => Utils.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i)) | ||||
|                 if(needed.length === 0){ | ||||
|                     return undefined | ||||
|                 } | ||||
|                 return needed | ||||
|             } | ||||
|             , [layer.isDisplayed, state.leafletMap]).stabilized(250); | ||||
|          | ||||
|         neededTiles.addCallbackAndRunD(neededIndexes => { | ||||
|             for (const neededIndex of neededIndexes) { | ||||
|                 self._loadedTiles.add(neededIndex) | ||||
|                 const xy = Utils.tile_from_index(zoomlevel, neededIndex) | ||||
|                 const src = constructTile(xy) | ||||
|                 let xmap = self.existingTiles.get(xy[0]) | ||||
|                 if(xmap === undefined){ | ||||
|                    xmap =  new Map<number, FeatureSourceForLayer>() | ||||
|                    self.existingTiles.set(xy[0], xmap)  | ||||
|                 } | ||||
|             xmap.set(xy[1], src) | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										3
									
								
								Logic/FeatureSource/TiledFeatureSource/README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								Logic/FeatureSource/TiledFeatureSource/README.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| Data in MapComplete can come from multiple sources. | ||||
| 
 | ||||
| In order to keep thins snappy, they are distributed over a tiled database | ||||
							
								
								
									
										0
									
								
								Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,40 @@ | |||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer} from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import Loc from "../../../Models/Loc"; | ||||
| import GeoJsonSource from "../GeoJsonSource"; | ||||
| import DynamicTileSource from "./DynamicTileSource"; | ||||
| 
 | ||||
| export default class DynamicGeoJsonTileSource extends DynamicTileSource { | ||||
|     constructor(layer: FilteredLayer, | ||||
|                 registerLayer: (layer: FeatureSourceForLayer) => void, | ||||
|                 state: { | ||||
|                     locationControl: UIEventSource<Loc> | ||||
|                     leafletMap: any | ||||
|                 }) { | ||||
|         const source = layer.layerDef.source | ||||
|         if (source.geojsonZoomLevel === undefined) { | ||||
|             throw "Invalid layer: geojsonZoomLevel expected" | ||||
|         } | ||||
|         if (source.geojsonSource === undefined) { | ||||
|             throw "Invalid layer: geojsonSource expected" | ||||
|         } | ||||
| 
 | ||||
|         super( | ||||
|             layer, | ||||
|             source.geojsonZoomLevel, | ||||
|             (xy) => { | ||||
|                 const xyz: [number, number, number] = [xy[0], xy[1], source.geojsonZoomLevel] | ||||
|                 const src = new GeoJsonSource( | ||||
|                     layer, | ||||
|                     xyz | ||||
|                 ) | ||||
|                 registerLayer(src) | ||||
|                 return src | ||||
|             }, | ||||
|             state | ||||
|         ); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue