forked from MapComplete/MapComplete
		
	More refactoring: using a decent, configurable datapipeline now
This commit is contained in:
		
							parent
							
								
									6ac8ec84e4
								
							
						
					
					
						commit
						e42a668c4a
					
				
					 17 changed files with 434 additions and 265 deletions
				
			
		|  | @ -145,7 +145,7 @@ export default class LayerConfig { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         this.title = tr("title", undefined); |         this.title = tr("title", undefined); | ||||||
|         this.icon = tr("icon", Img.AsData(Svg.bug)); |         this.icon = tr("icon", Img.AsData(Svg.pin)); | ||||||
|         this.iconOverlays = (json.iconOverlays ?? []).map(overlay => { |         this.iconOverlays = (json.iconOverlays ?? []).map(overlay => { | ||||||
|             let tr = new TagRenderingConfig(overlay.then); |             let tr = new TagRenderingConfig(overlay.then); | ||||||
|             if (typeof overlay.then === "string" && SharedTagRenderings.SharedIcons[overlay.then] !== undefined) { |             if (typeof overlay.then === "string" && SharedTagRenderings.SharedIcons[overlay.then] !== undefined) { | ||||||
|  |  | ||||||
|  | @ -121,7 +121,8 @@ export interface LayerConfigJson { | ||||||
|     hideUnderlayingFeaturesMinPercentage?:number; |     hideUnderlayingFeaturesMinPercentage?:number; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * If set, this layer will pass all the features it receives onto the next layer |      * If set, this layer will pass all the features it receives onto the next layer. | ||||||
|  |      * This is ideal for decoration, e.g. directionss on cameras | ||||||
|      */ |      */ | ||||||
|     passAllFeatures?:boolean |     passAllFeatures?:boolean | ||||||
|      |      | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ import State from "./State"; | ||||||
| import {WelcomeMessage} from "./UI/WelcomeMessage"; | import {WelcomeMessage} from "./UI/WelcomeMessage"; | ||||||
| import {LayerSelection} from "./UI/LayerSelection"; | import {LayerSelection} from "./UI/LayerSelection"; | ||||||
| import {VariableUiElement} from "./UI/Base/VariableUIElement"; | import {VariableUiElement} from "./UI/Base/VariableUIElement"; | ||||||
| import UpdateFromOverpass from "./Logic/UpdateFromOverpass"; | import LoadFromOverpass from "./Logic/Actors/UpdateFromOverpass"; | ||||||
| import {UIEventSource} from "./Logic/UIEventSource"; | import {UIEventSource} from "./Logic/UIEventSource"; | ||||||
| import {QueryParameters} from "./Logic/Web/QueryParameters"; | import {QueryParameters} from "./Logic/Web/QueryParameters"; | ||||||
| import {PersonalLayersPanel} from "./UI/PersonalLayersPanel"; | import {PersonalLayersPanel} from "./UI/PersonalLayersPanel"; | ||||||
|  | @ -40,6 +40,12 @@ import {UserDetails} from "./Logic/Osm/OsmConnection"; | ||||||
| import Attribution from "./UI/Misc/Attribution"; | import Attribution from "./UI/Misc/Attribution"; | ||||||
| import Constants from "./Models/Constants"; | import Constants from "./Models/Constants"; | ||||||
| import MetaTagging from "./Logic/MetaTagging"; | import MetaTagging from "./Logic/MetaTagging"; | ||||||
|  | import FeatureSourceMerger from "./Logic/FeatureSource/FeatureSourceMerger"; | ||||||
|  | import RememberingSource from "./Logic/FeatureSource/RememberingSource"; | ||||||
|  | import FilteringFeatureSource from "./Logic/FeatureSource/FilteringFeatureSource"; | ||||||
|  | import WayHandlingApplyingFeatureSource from "./Logic/FeatureSource/WayHandlingApplyingFeatureSource"; | ||||||
|  | import FeatureSource from "./Logic/FeatureSource/FeatureSource"; | ||||||
|  | import NoOverlapSource from "./Logic/FeatureSource/NoOverlapSource"; | ||||||
| 
 | 
 | ||||||
| export class InitUiElements { | export class InitUiElements { | ||||||
| 
 | 
 | ||||||
|  | @ -374,7 +380,7 @@ export class InitUiElements { | ||||||
|             const flayer: FilteredLayer = new FilteredLayer(layer, generateContents); |             const flayer: FilteredLayer = new FilteredLayer(layer, generateContents); | ||||||
|             flayers.push(flayer); |             flayers.push(flayer); | ||||||
| 
 | 
 | ||||||
|             QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wehter or not layer " + layer.id + " is shown") |             QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wether or not layer " + layer.id + " is shown") | ||||||
|                 .map<boolean>((str) => str !== "false", [], (b) => b.toString()) |                 .map<boolean>((str) => str !== "false", [], (b) => b.toString()) | ||||||
|                 .syncWith( |                 .syncWith( | ||||||
|                     flayer.isDisplayed |                     flayer.isDisplayed | ||||||
|  | @ -383,11 +389,45 @@ export class InitUiElements { | ||||||
| 
 | 
 | ||||||
|         State.state.filteredLayers.setData(flayers); |         State.state.filteredLayers.setData(flayers); | ||||||
| 
 | 
 | ||||||
|         const updater = new UpdateFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap); |         function addMatchingIds(src: FeatureSource) { | ||||||
|  | 
 | ||||||
|  |             src.features.addCallback(features => { | ||||||
|  |                 features.forEach(f => { | ||||||
|  |                     const properties = f.feature.properties; | ||||||
|  |                     if (properties._matching_layer_id) { | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     for (const flayer of flayers) { | ||||||
|  |                         if (flayer.layerDef.overpassTags.matchesProperties(properties)) { | ||||||
|  |                             properties._matching_layer_id = flayer.layerDef.id; | ||||||
|  |                             break; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap); | ||||||
|         State.state.layerUpdater = updater; |         State.state.layerUpdater = updater; | ||||||
| 
 | 
 | ||||||
|         updater.features.addCallback(features => { |         addMatchingIds(updater); | ||||||
|  |         addMatchingIds(State.state.changes); | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  |         const source = | ||||||
|  |             new FilteringFeatureSource( | ||||||
|  |                 flayers, | ||||||
|  |                 State.state.locationControl, | ||||||
|  |                 new FeatureSourceMerger([ | ||||||
|  |                     new RememberingSource(new WayHandlingApplyingFeatureSource(flayers, | ||||||
|  |                         new NoOverlapSource(flayers, updater) | ||||||
|  |                     )), | ||||||
|  |                     State.state.changes])); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         source.features.addCallback((featuresFreshness: { feature: any, freshness: Date }[]) => { | ||||||
|  |             let features = featuresFreshness.map(ff => ff.feature); | ||||||
|             features.forEach(feature => { |             features.forEach(feature => { | ||||||
|                 State.state.allElements.addElement(feature); |                 State.state.allElements.addElement(feature); | ||||||
|             }) |             }) | ||||||
|  | @ -402,13 +442,10 @@ export class InitUiElements { | ||||||
|                     } |                     } | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|                 // We use window.setTimeout to give JS some time to update everything and make the interface not too laggy
 |                 const layer = layers[0]; | ||||||
|                 window.setTimeout(() => { |                 const rest = layers.slice(1, layers.length); | ||||||
|                     const layer = layers[0]; |                 features = layer.SetApplicableData(features); | ||||||
|                     const rest = layers.slice(1, layers.length); |                 renderLayers(rest); | ||||||
|                     features = layer.SetApplicableData(features); |  | ||||||
|                     renderLayers(rest); |  | ||||||
|                 }, 50) |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             renderLayers(flayers); |             renderLayers(flayers); | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ import * as L from "leaflet"; | ||||||
| import {UIElement} from "../../UI/UIElement"; | import {UIElement} from "../../UI/UIElement"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| import {UIEventSource} from "../UIEventSource"; | import {UIEventSource} from "../UIEventSource"; | ||||||
| import {FilteredLayer} from "../FilteredLayer"; |  | ||||||
| import Img from "../../UI/Base/Img"; | import Img from "../../UI/Base/Img"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -16,7 +15,7 @@ export class StrayClickHandler { | ||||||
|     constructor( |     constructor( | ||||||
|         lastClickLocation: UIEventSource<{ lat: number, lon:number }>, |         lastClickLocation: UIEventSource<{ lat: number, lon:number }>, | ||||||
|         selectedElement: UIEventSource<string>, |         selectedElement: UIEventSource<string>, | ||||||
|         filteredLayers: UIEventSource<FilteredLayer[]>, |         filteredLayers: UIEventSource<{ readonly isDisplayed: UIEventSource<boolean>}[]>, | ||||||
|         leafletMap: UIEventSource<L.Map>, |         leafletMap: UIEventSource<L.Map>, | ||||||
|         fullscreenMessage: UIEventSource<UIElement>, |         fullscreenMessage: UIEventSource<UIElement>, | ||||||
|         uiToShow: (() => UIElement)) { |         uiToShow: (() => UIElement)) { | ||||||
|  |  | ||||||
|  | @ -1,22 +1,19 @@ | ||||||
| import {Or, TagsFilter} from "./Tags"; | import {UIEventSource} from "../UIEventSource"; | ||||||
| import {UIEventSource} from "./UIEventSource"; | import Loc from "../../Models/Loc"; | ||||||
| import Bounds from "../Models/Bounds"; | import {Or, TagsFilter} from "../Tags"; | ||||||
| import {Overpass} from "./Osm/Overpass"; | import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | ||||||
| import Loc from "../Models/Loc"; | import {Overpass} from "../Osm/Overpass"; | ||||||
| import LayoutConfig from "../Customizations/JSON/LayoutConfig"; | import Bounds from "../../Models/Bounds"; | ||||||
| import FeatureSource from "./Actors/FeatureSource"; | import FeatureSource from "../FeatureSource/FeatureSource"; | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| export default class UpdateFromOverpass implements FeatureSource{ | export default class UpdateFromOverpass implements FeatureSource{ | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * The last loaded features of the geojson |      * The last loaded features of the geojson | ||||||
|      */ |      */ | ||||||
|     public readonly features: UIEventSource<any[]> = new UIEventSource<any[]>(undefined); |     public readonly features: UIEventSource<{feature:any, freshness: Date}[]> = new UIEventSource<any[]>(undefined); | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * The time of updating according to Overpass |  | ||||||
|      */ |  | ||||||
|     public readonly freshness:UIEventSource<Date> = new UIEventSource<Date>(undefined); |  | ||||||
| 
 | 
 | ||||||
|     public readonly sufficientlyZoomed: UIEventSource<boolean>; |     public readonly sufficientlyZoomed: UIEventSource<boolean>; | ||||||
|     public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false); |     public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||||
|  | @ -142,8 +139,7 @@ export default class UpdateFromOverpass implements FeatureSource{ | ||||||
|             function (data, date) { |             function (data, date) { | ||||||
|                 self._previousBounds.get(z).push(queryBounds); |                 self._previousBounds.get(z).push(queryBounds); | ||||||
|                 self.retries.setData(0); |                 self.retries.setData(0); | ||||||
|                 self.freshness.setData(date); |                 self.features.setData(data.features.map(f => ({feature: f, freshness: date}))); | ||||||
|                 self.features.setData(data.features); |  | ||||||
|                 self.runningQuery.setData(false); |                 self.runningQuery.setData(false); | ||||||
|             }, |             }, | ||||||
|             function (reason) { |             function (reason) { | ||||||
|  | @ -1,8 +1,5 @@ | ||||||
| import {UIEventSource} from "../UIEventSource"; | import {UIEventSource} from "../UIEventSource"; | ||||||
| 
 | 
 | ||||||
| export default interface FeatureSource { | export default interface FeatureSource { | ||||||
|      |     features: UIEventSource<{feature: any, freshness: Date}[]>; | ||||||
|     features : UIEventSource<any[]>; |  | ||||||
|     freshness: UIEventSource<Date>; |  | ||||||
|      |  | ||||||
| } | } | ||||||
							
								
								
									
										40
									
								
								Logic/FeatureSource/FeatureSourceMerger.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								Logic/FeatureSource/FeatureSourceMerger.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,40 @@ | ||||||
|  | import FeatureSource from "./FeatureSource"; | ||||||
|  | import {UIEventSource} from "../UIEventSource"; | ||||||
|  | 
 | ||||||
|  | export default class FeatureSourceMerger implements FeatureSource { | ||||||
|  | 
 | ||||||
|  |     public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{feature: any; freshness: Date}[]>([]); | ||||||
|  |     private readonly _sources: FeatureSource[]; | ||||||
|  | 
 | ||||||
|  |     constructor(sources: FeatureSource[]) { | ||||||
|  |         this._sources = sources; | ||||||
|  |         const self = this; | ||||||
|  |         for (const source of sources) { | ||||||
|  |             source.features.addCallback(() => self.Update()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private Update() { | ||||||
|  |         let all = {}; // Mapping 'id' -> {feature, freshness}
 | ||||||
|  |         for (const source of this._sources) { | ||||||
|  |             for (const f of source.features.data) { | ||||||
|  |                 const id = f.feature.properties.id+f.feature.geometry.type; | ||||||
|  |                 const oldV = all[id]; | ||||||
|  |                 if(oldV === undefined){ | ||||||
|  |                     all[id] = f; | ||||||
|  |                 }else{ | ||||||
|  |                     if(oldV.freshness < f.freshness){ | ||||||
|  |                         all[id]=f; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         const newList = []; | ||||||
|  |         for (const id in all) { | ||||||
|  |             newList.push(all[id]); | ||||||
|  |         } | ||||||
|  |         this.features.setData(newList); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										52
									
								
								Logic/FeatureSource/FilteringFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								Logic/FeatureSource/FilteringFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,52 @@ | ||||||
|  | import FeatureSource from "./FeatureSource"; | ||||||
|  | import {UIEventSource} from "../UIEventSource"; | ||||||
|  | import LayerConfig from "../../Customizations/JSON/LayerConfig"; | ||||||
|  | import Loc from "../../Models/Loc"; | ||||||
|  | 
 | ||||||
|  | export default class FilteringFeatureSource implements FeatureSource { | ||||||
|  |     public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||||
|  | 
 | ||||||
|  |     constructor(layers: { | ||||||
|  |                     isDisplayed: UIEventSource<boolean>, | ||||||
|  |                     layerDef: LayerConfig | ||||||
|  |                 }[], | ||||||
|  |                 location: UIEventSource<Loc>, | ||||||
|  |                 upstream: FeatureSource) { | ||||||
|  | 
 | ||||||
|  |         const layerDict = {}; | ||||||
|  |         | ||||||
|  |         const self = this; | ||||||
|  |          | ||||||
|  |         function update() { | ||||||
|  |             console.log("UPdating...") | ||||||
|  |             const features: { feature: any, freshness: Date }[] = upstream.features.data; | ||||||
|  |             const newFeatures = features.filter(f => { | ||||||
|  |                 const layerId = f.feature.properties._matching_layer_id; | ||||||
|  |                 if (layerId === undefined) { | ||||||
|  |                     console.error(f) | ||||||
|  |                     throw "feature._matching_layer_id is undefined" | ||||||
|  |                 } | ||||||
|  |                 const layer: { | ||||||
|  |                     isDisplayed: UIEventSource<boolean>, | ||||||
|  |                     layerDef: LayerConfig | ||||||
|  |                 } = layerDict[layerId]; | ||||||
|  |                 if (layer === undefined) { | ||||||
|  |                     throw "No layer found with id " + layerId; | ||||||
|  |                 } | ||||||
|  |                 return layer.isDisplayed.data && (layer.layerDef.minzoom <= location.data.zoom); | ||||||
|  |             }); | ||||||
|  |             self.features.setData(newFeatures); | ||||||
|  |         } | ||||||
|  |         for (const layer of layers) { | ||||||
|  |             layerDict[layer.layerDef.id] = layer; | ||||||
|  |             layer.isDisplayed.addCallback(update) | ||||||
|  |         } | ||||||
|  |         upstream.features.addCallback(update); | ||||||
|  |         location.map(l => l.zoom).addCallback(update); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										86
									
								
								Logic/FeatureSource/NoOverlapSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								Logic/FeatureSource/NoOverlapSource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | ||||||
|  | import LayerConfig from "../../Customizations/JSON/LayerConfig"; | ||||||
|  | import FeatureSource from "./FeatureSource"; | ||||||
|  | import {UIEventSource} from "../UIEventSource"; | ||||||
|  | import {GeoOperations} from "../GeoOperations"; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * The no overlap source takes a featureSource and applies a filter on it. | ||||||
|  |  * First, it'll figure out for each feature to which layer it belongs | ||||||
|  |  * Then, it'll check any feature of any 'lower' layer | ||||||
|  |  */ | ||||||
|  | export default class NoOverlapSource { | ||||||
|  | 
 | ||||||
|  |     features: UIEventSource<{ feature: any, freshness: Date }[]> = new UIEventSource<{ feature: any, freshness: Date }[]>([]); | ||||||
|  | 
 | ||||||
|  |     constructor(layers: { | ||||||
|  |                     layerDef: LayerConfig | ||||||
|  |                 }[], | ||||||
|  |                 upstream: FeatureSource) { | ||||||
|  |         const layerDict = {}; | ||||||
|  |         let noOverlapRemoval = true; | ||||||
|  |         const layerIds = [] | ||||||
|  |         for (const layer of layers) { | ||||||
|  |             layerDict[layer.layerDef.id] = layer; | ||||||
|  |             layerIds.push(layer.layerDef.id); | ||||||
|  |             if ((layer.layerDef.hideUnderlayingFeaturesMinPercentage ?? 0) !== 0) { | ||||||
|  |                 noOverlapRemoval = false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (noOverlapRemoval) { | ||||||
|  |             this.features = upstream.features; | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         this.features = upstream.features.map( | ||||||
|  |             features => { | ||||||
|  |                 if (features === undefined) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 | ||||||
|  |                 // There is overlap removal active
 | ||||||
|  |                 // We partition all the features with their respective layerIDs
 | ||||||
|  |                 const partitions = {}; | ||||||
|  |                 for (const layerId of layerIds) { | ||||||
|  |                     partitions[layerId] = [] | ||||||
|  |                 } | ||||||
|  |                 for (const feature of features) { | ||||||
|  |                     partitions[feature.feature.properties._matching_layer_id].push(feature); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // With this partitioning in hand, we run over every layer and remove every underlying feature if needed
 | ||||||
|  |                 for (let i = 0; i < layerIds.length; i++) { | ||||||
|  |                     let layerId = layerIds[i]; | ||||||
|  |                     const percentage = layerDict[layerId].layerDef.hideUnderlayingFeaturesMinPercentage ?? 0; | ||||||
|  |                     if (percentage === 0) { | ||||||
|  |                         // We don't have to remove underlying features!
 | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  |                     const guardPartition = partitions[layerId]; | ||||||
|  |                     for (let j = i + 1; j < layerIds.length; j++) { | ||||||
|  |                         let layerJd = layerIds[j]; | ||||||
|  |                         let partitionToShrink: { feature: any, freshness: Date }[] = partitions[layerJd]; | ||||||
|  |                         let newPartition = []; | ||||||
|  |                         for (const mightBeDeleted of partitionToShrink) { | ||||||
|  |                             const doesOverlap = GeoOperations.featureIsContainedInAny( | ||||||
|  |                                 mightBeDeleted.feature, | ||||||
|  |                                 guardPartition.map(f => f.feature), | ||||||
|  |                                 percentage | ||||||
|  |                             ); | ||||||
|  |                             if(!doesOverlap){ | ||||||
|  |                                 newPartition.push(mightBeDeleted); | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         partitions[layerJd] = newPartition; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // At last, we create the actual new features
 | ||||||
|  |                 let newFeatures: { feature: any, freshness: Date }[] = []; | ||||||
|  |                 for (const layerId of layerIds) { | ||||||
|  |                     newFeatures = newFeatures.concat(partitions[layerId]); | ||||||
|  |                 } | ||||||
|  |                 return newFeatures; | ||||||
|  |             }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								Logic/FeatureSource/RememberingSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Logic/FeatureSource/RememberingSource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | ||||||
|  | /** | ||||||
|  |  * Every previously added point is remembered, but new points are added | ||||||
|  |  */ | ||||||
|  | import FeatureSource from "./FeatureSource"; | ||||||
|  | import {UIEventSource} from "../UIEventSource"; | ||||||
|  | 
 | ||||||
|  | export default class RememberingSource implements FeatureSource{ | ||||||
|  |     features: UIEventSource<{feature: any, freshness: Date}[]> = new UIEventSource<{feature: any, freshness: Date}[]>([]); | ||||||
|  |      | ||||||
|  |     constructor(source: FeatureSource) { | ||||||
|  |         const self = this; | ||||||
|  |         source.features.addCallbackAndRun(features => { | ||||||
|  |             if(features === undefined){ | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             const ids = new Set<string>( features.map(f => f.feature.properties.id+f.feature.geometry.type)); | ||||||
|  |             const newList = features.concat( | ||||||
|  |                 self.features.data.filter(old => !ids.has(old.feature.properties.id+old.feature.geometry.type)) | ||||||
|  |             ) | ||||||
|  |             self.features.setData(newList); | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  | } | ||||||
							
								
								
									
										66
									
								
								Logic/FeatureSource/WayHandlingApplyingFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								Logic/FeatureSource/WayHandlingApplyingFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,66 @@ | ||||||
|  | import FeatureSource from "./FeatureSource"; | ||||||
|  | import {UIEventSource} from "../UIEventSource"; | ||||||
|  | import LayerConfig from "../../Customizations/JSON/LayerConfig"; | ||||||
|  | import {GeoOperations} from "../GeoOperations"; | ||||||
|  | 
 | ||||||
|  | export default class WayHandlingApplyingFeatureSource implements FeatureSource { | ||||||
|  |     features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||||
|  | 
 | ||||||
|  |     constructor(layers: { | ||||||
|  |                     layerDef: LayerConfig | ||||||
|  |                 }[], | ||||||
|  |                 upstream: FeatureSource) { | ||||||
|  |         const layerDict = {}; | ||||||
|  |         let allDefaultWayHandling = true; | ||||||
|  |         for (const layer of layers) { | ||||||
|  |             layerDict[layer.layerDef.id] = layer; | ||||||
|  |             if (layer.layerDef.wayHandling !== LayerConfig.WAYHANDLING_DEFAULT) { | ||||||
|  |                 allDefaultWayHandling = false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (allDefaultWayHandling) { | ||||||
|  |             this.features = upstream.features; | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         this.features = upstream.features.map( | ||||||
|  |             features => { | ||||||
|  |                 if(features === undefined){ | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 const newFeatures: { feature: any, freshness: Date }[] = []; | ||||||
|  |                 for (const f of features) { | ||||||
|  |                     const feat = f.feature; | ||||||
|  |                     const layerId = feat.properties._matching_layer_id; | ||||||
|  |                     const layer: LayerConfig = layerDict[layerId].layerDef; | ||||||
|  |                     if (layer === undefined) { | ||||||
|  |                         throw "No layer found with id " + layerId; | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     if(layer.wayHandling === LayerConfig.WAYHANDLING_DEFAULT){ | ||||||
|  |                         newFeatures.push(f); | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     if (feat.geometry.type === "Point") { | ||||||
|  |                         newFeatures.push(f); | ||||||
|  |                         // it is a point, nothing to do here
 | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     const centerPoint = GeoOperations.centerpoint(feat); | ||||||
|  |                     newFeatures.push({feature: centerPoint, freshness: f.freshness}); | ||||||
|  |                      | ||||||
|  |                     if(layer.wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY){ | ||||||
|  |                         newFeatures.push(f); | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                 } | ||||||
|  |                 return newFeatures; | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -10,29 +10,20 @@ import Hash from "./Web/Hash"; | ||||||
| import LazyElement from "../UI/Base/LazyElement"; | import LazyElement from "../UI/Base/LazyElement"; | ||||||
| 
 | 
 | ||||||
| /*** | /*** | ||||||
|  * A filtered layer is a layer which offers a 'set-data' function |  | ||||||
|  * It is initialized with a tagfilter. |  | ||||||
|  *  |  *  | ||||||
|  * When geojson-data is given to 'setData', all the geojson matching the filter, is rendered on this layer. |  | ||||||
|  * If it is not rendered, it is returned in a 'leftOver'-geojson; which can be consumed by the next layer. |  | ||||||
|  * |  | ||||||
|  * This also makes sure that no objects are rendered twice if they are applicable on two layers |  | ||||||
|  */ |  */ | ||||||
| export class FilteredLayer { | export class FilteredLayer { | ||||||
| 
 | 
 | ||||||
|     public readonly name: string | UIElement; |     public readonly name: string | UIElement; | ||||||
|     public readonly filters: TagsFilter; |  | ||||||
|     public readonly isDisplayed: UIEventSource<boolean> = new UIEventSource(true); |     public readonly isDisplayed: UIEventSource<boolean> = new UIEventSource(true); | ||||||
|     public readonly layerDef: LayerConfig; |     public readonly layerDef: LayerConfig; | ||||||
|     private readonly combinedIsDisplayed: UIEventSource<boolean>; | 
 | ||||||
|  |     private readonly filters: TagsFilter; | ||||||
|     private readonly _maxAllowedOverlap: number; |     private readonly _maxAllowedOverlap: number; | ||||||
| 
 | 
 | ||||||
|     /** The featurecollection from overpass |     /** The featurecollection from overpass | ||||||
|      */ |      */ | ||||||
|     private _dataFromOverpass: any[]; |     private _dataFromOverpass: any[]; | ||||||
|     /** List of new elements, geojson features |  | ||||||
|      */ |  | ||||||
|     private _newElements = []; |  | ||||||
|     /** |     /** | ||||||
|      * The leaflet layer object which should be removed on rerendering |      * The leaflet layer object which should be removed on rerendering | ||||||
|      */ |      */ | ||||||
|  | @ -51,22 +42,7 @@ export class FilteredLayer { | ||||||
|         this.name = name; |         this.name = name; | ||||||
|         this.filters = layerDef.overpassTags; |         this.filters = layerDef.overpassTags; | ||||||
|         this._maxAllowedOverlap = layerDef.hideUnderlayingFeaturesMinPercentage; |         this._maxAllowedOverlap = layerDef.hideUnderlayingFeaturesMinPercentage; | ||||||
|         const self = this; | 
 | ||||||
|         this.combinedIsDisplayed = this.isDisplayed.map<boolean>(isDisplayed => { |  | ||||||
|                 return isDisplayed && State.state.locationControl.data.zoom >= self.layerDef.minzoom |  | ||||||
|             }, |  | ||||||
|             [State.state.locationControl] |  | ||||||
|         ); |  | ||||||
|         this.combinedIsDisplayed.addCallback(function (isDisplayed) { |  | ||||||
|             const map = State.state.leafletMap.data; |  | ||||||
|             if (self._geolayer !== undefined && self._geolayer !== null) { |  | ||||||
|                 if (isDisplayed) { |  | ||||||
|                     self._geolayer.addTo(map); |  | ||||||
|                 } else { |  | ||||||
|                     map.removeLayer(self._geolayer); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -88,29 +64,11 @@ export class FilteredLayer { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.RenderLayer(selfFeatures) |         this.RenderLayer(selfFeatures) | ||||||
| 
 |         return leftoverFeatures; | ||||||
|         const notShadowed = []; |  | ||||||
|         for (const feature of leftoverFeatures) { |  | ||||||
|             if (this._maxAllowedOverlap !== undefined && this._maxAllowedOverlap > 0) { |  | ||||||
|                 if (GeoOperations.featureIsContainedInAny(feature, selfFeatures, this._maxAllowedOverlap)) { |  | ||||||
|                     // This feature is filtered away
 |  | ||||||
|                     continue; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             notShadowed.push(feature); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return notShadowed; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     public AddNewElement(element) { |     private RenderLayer(features: any[]) { | ||||||
|         this._newElements.push(element); |  | ||||||
|         this.RenderLayer(this._dataFromOverpass); // Update the layer
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private RenderLayer(features) { |  | ||||||
| 
 | 
 | ||||||
|         if (this._geolayer !== undefined && this._geolayer !== null) { |         if (this._geolayer !== undefined && this._geolayer !== null) { | ||||||
|             // Remove the old geojson layer from the map - we'll reshow all the elements later on anyway
 |             // Remove the old geojson layer from the map - we'll reshow all the elements later on anyway
 | ||||||
|  | @ -118,12 +76,9 @@ export class FilteredLayer { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // We fetch all the data we have to show:
 |         // We fetch all the data we have to show:
 | ||||||
|         let fusedFeatures = this.ApplyWayHandling(this.FuseData(features)); |  | ||||||
| 
 |  | ||||||
|         // And we copy some features as points - if needed
 |  | ||||||
|         const data = { |         const data = { | ||||||
|             type: "FeatureCollection", |             type: "FeatureCollection", | ||||||
|             features: fusedFeatures |             features: features | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let self = this; |         let self = this; | ||||||
|  | @ -144,13 +99,7 @@ export class FilteredLayer { | ||||||
|                         radius: 25, |                         radius: 25, | ||||||
|                         color: style.color |                         color: style.color | ||||||
|                     }); |                     }); | ||||||
|                 } else if (style.icon.iconUrl.startsWith("$circle")) { |  | ||||||
|                     marker = L.circle(latLng, { |  | ||||||
|                         radius: 25, |  | ||||||
|                         color: style.color |  | ||||||
|                     }); |  | ||||||
|                 } else { |                 } else { | ||||||
|                     style.icon.html.ListenTo(self.isDisplayed) |  | ||||||
|                     marker = L.marker(latLng, { |                     marker = L.marker(latLng, { | ||||||
|                         icon: L.divIcon({ |                         icon: L.divIcon({ | ||||||
|                             html: style.icon.html.Render(), |                             html: style.icon.html.Render(), | ||||||
|  | @ -206,72 +155,9 @@ export class FilteredLayer { | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         if (this.combinedIsDisplayed.data) { |         this._geolayer.addTo(State.state.leafletMap.data); | ||||||
|             this._geolayer.addTo(State.state.leafletMap.data); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private ApplyWayHandling(fusedFeatures: any[]) { |  | ||||||
|         if (this.layerDef.wayHandling === LayerConfig.WAYHANDLING_DEFAULT) { |  | ||||||
|             // We don't have to do anything special
 |  | ||||||
|             return fusedFeatures; |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         // We have to convert all the ways into centerpoints
 |  | ||||||
|         const existingPoints = []; |  | ||||||
|         const newPoints = []; |  | ||||||
|         const existingWays = []; |  | ||||||
| 
 |  | ||||||
|         for (const feature of fusedFeatures) { |  | ||||||
|             if (feature.geometry.type === "Point") { |  | ||||||
|                 existingPoints.push(feature); |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             existingWays.push(feature); |  | ||||||
|             const centerPoint = GeoOperations.centerpoint(feature); |  | ||||||
|             newPoints.push(centerPoint); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         fusedFeatures = existingPoints.concat(newPoints); |  | ||||||
|         if (this.layerDef.wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY) { |  | ||||||
|             fusedFeatures = fusedFeatures.concat(existingWays) |  | ||||||
|         } |  | ||||||
|         return fusedFeatures; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     //*Fuses the old and the new datasets*/
 |  | ||||||
|     private FuseData(data: any[]) { |  | ||||||
|         const oldData = this._dataFromOverpass ?? []; |  | ||||||
| 
 |  | ||||||
|         // We keep track of all the ids that are freshly loaded in order to avoid adding duplicates
 |  | ||||||
|         const idsFromOverpass: Set<number> = new Set<number>(); |  | ||||||
|         // A list of all the features to show
 |  | ||||||
|         const fusedFeatures = []; |  | ||||||
|         // First, we add all the fresh data:
 |  | ||||||
|         for (const feature of data) { |  | ||||||
|             idsFromOverpass.add(feature.properties.id); |  | ||||||
|             fusedFeatures.push(feature); |  | ||||||
|         } |  | ||||||
|         // Now we add all the stale data
 |  | ||||||
|         for (const feature of oldData) { |  | ||||||
|             if (idsFromOverpass.has(feature.properties.id)) { |  | ||||||
|                 continue; // Feature already loaded and a fresher version is available
 |  | ||||||
|             } |  | ||||||
|             idsFromOverpass.add(feature.properties.id); |  | ||||||
|             fusedFeatures.push(feature); |  | ||||||
|         } |  | ||||||
|         this._dataFromOverpass = fusedFeatures; |  | ||||||
| 
 |  | ||||||
|         for (const feature of this._newElements) { |  | ||||||
|             if (!idsFromOverpass.has(feature.properties.id)) { |  | ||||||
|                 // This element is not yet uploaded or not yet visible in overpass
 |  | ||||||
|                 // We include it in the layer
 |  | ||||||
|                 fusedFeatures.push(feature); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return fusedFeatures; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  | @ -1,71 +1,25 @@ | ||||||
| /** |  | ||||||
|  * Handles all changes made to OSM. |  | ||||||
|  * Needs an authenticator via OsmConnection |  | ||||||
|  */ |  | ||||||
| import {OsmNode, OsmObject} from "./OsmObject"; | import {OsmNode, OsmObject} from "./OsmObject"; | ||||||
| import {And, Tag, TagsFilter} from "../Tags"; | import {And, Tag, TagsFilter} from "../Tags"; | ||||||
| import State from "../../State"; | import State from "../../State"; | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
| import {UIEventSource} from "../UIEventSource"; | import {UIEventSource} from "../UIEventSource"; | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants"; | ||||||
|  | import FeatureSource from "../FeatureSource/FeatureSource"; | ||||||
| 
 | 
 | ||||||
| export class Changes { | /** | ||||||
|  |  * Handles all changes made to OSM. | ||||||
|  |  * Needs an authenticator via OsmConnection | ||||||
|  |  */ | ||||||
|  | export class Changes implements FeatureSource{ | ||||||
| 
 | 
 | ||||||
|     private static _nextId = -1; // New assined ID's are negative
 |     public features = new UIEventSource<{feature: any, freshness: Date}[]>([]); | ||||||
|      |      | ||||||
|     addTag(elementId: string, tagsFilter: TagsFilter, |     private static _nextId = -1; // Newly assigned ID's are negative
 | ||||||
|            tags?: UIEventSource<any>) { |  | ||||||
|         const changes = this.tagToChange(tagsFilter); |  | ||||||
|         if (changes.length == 0) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); |  | ||||||
|         const elementTags = eventSource.data; |  | ||||||
|         const pending : {elementId:string, key: string, value: string}[] = []; |  | ||||||
|         for (const change of changes) { |  | ||||||
|             if (elementTags[change.k] !== change.v) { |  | ||||||
|                 elementTags[change.k] = change.v; |  | ||||||
|                 pending.push({elementId: elementTags.id, key: change.k, value: change.v}); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         if(pending.length === 0){ |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         console.log("Sending ping",eventSource) |  | ||||||
|         eventSource.ping(); |  | ||||||
|         this.uploadAll([], pending); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     private tagToChange(tagsFilter: TagsFilter) { |  | ||||||
|         let changes: { k: string, v: string }[] = []; |  | ||||||
| 
 |  | ||||||
|         if (tagsFilter instanceof Tag) { |  | ||||||
|             const tag = tagsFilter as Tag; |  | ||||||
|             if (typeof tag.value !== "string") { |  | ||||||
|                 throw "Invalid value" |  | ||||||
|             } |  | ||||||
|             return [this.checkChange(tag.key, tag.value)]; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (tagsFilter instanceof And) { |  | ||||||
|             const and = tagsFilter as And; |  | ||||||
|             for (const tag of and.and) { |  | ||||||
|                 changes = changes.concat(this.tagToChange(tag)); |  | ||||||
|             } |  | ||||||
|             return changes; |  | ||||||
|         } |  | ||||||
|         console.log("Unsupported tagsfilter element to addTag", tagsFilter); |  | ||||||
|         throw "Unsupported tagsFilter element"; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Adds a change to the pending changes |      * Adds a change to the pending changes | ||||||
|      * @param elementId |  | ||||||
|      * @param key |  | ||||||
|      * @param value |  | ||||||
|      */ |      */ | ||||||
|     private checkChange(key: string, value: string): { k: string, v: string } { |     private static checkChange(key: string, value: string): { k: string, v: string } { | ||||||
|         if (key === undefined || key === null) { |         if (key === undefined || key === null) { | ||||||
|             console.log("Invalid key"); |             console.log("Invalid key"); | ||||||
|             return undefined; |             return undefined; | ||||||
|  | @ -85,12 +39,35 @@ export class Changes { | ||||||
|         return {k: key, v: value}; |         return {k: key, v: value}; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     addTag(elementId: string, tagsFilter: TagsFilter, | ||||||
|  |            tags?: UIEventSource<any>) { | ||||||
|  |         const changes = this.tagToChange(tagsFilter); | ||||||
|  |         if (changes.length == 0) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); | ||||||
|  |         const elementTags = eventSource.data; | ||||||
|  |         const pending: { elementId: string, key: string, value: string }[] = []; | ||||||
|  |         for (const change of changes) { | ||||||
|  |             if (elementTags[change.k] !== change.v) { | ||||||
|  |                 elementTags[change.k] = change.v; | ||||||
|  |                 pending.push({elementId: elementTags.id, key: change.k, value: change.v}); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (pending.length === 0) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         console.log("Sending ping", eventSource) | ||||||
|  |         eventSource.ping(); | ||||||
|  |         this.uploadAll([], pending); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Create a new node element at the given lat/long. |      * Create a new node element at the given lat/long. | ||||||
|      * An internal OsmObject is created to upload later on, a geojson represention is returned. |      * An internal OsmObject is created to upload later on, a geojson represention is returned. | ||||||
|      * Note that the geojson version shares the tags (properties) by pointer, but has _no_ id in properties |      * Note that the geojson version shares the tags (properties) by pointer, but has _no_ id in properties | ||||||
|      */ |      */ | ||||||
|     createElement(basicTags:Tag[], lat: number, lon: number) { |     public createElement(basicTags: Tag[], lat: number, lon: number) { | ||||||
|         console.log("Creating a new element with ", basicTags) |         console.log("Creating a new element with ", basicTags) | ||||||
|         const osmNode = new OsmNode(Changes._nextId); |         const osmNode = new OsmNode(Changes._nextId); | ||||||
|         Changes._nextId--; |         Changes._nextId--; | ||||||
|  | @ -113,6 +90,9 @@ export class Changes { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         this.features.data.push({feature:geojson, freshness: new Date()}); | ||||||
|  |         this.features.ping(); | ||||||
|  |          | ||||||
|         // The basictags are COPIED, the id is included in the properties
 |         // The basictags are COPIED, the id is included in the properties
 | ||||||
|         // The tags are not yet written into the OsmObject, but this is applied onto a 
 |         // The tags are not yet written into the OsmObject, but this is applied onto a 
 | ||||||
|         const changes = []; |         const changes = []; | ||||||
|  | @ -128,6 +108,27 @@ export class Changes { | ||||||
|         return geojson; |         return geojson; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private tagToChange(tagsFilter: TagsFilter) { | ||||||
|  |         let changes: { k: string, v: string }[] = []; | ||||||
|  | 
 | ||||||
|  |         if (tagsFilter instanceof Tag) { | ||||||
|  |             const tag = tagsFilter as Tag; | ||||||
|  |             if (typeof tag.value !== "string") { | ||||||
|  |                 throw "Invalid value" | ||||||
|  |             } | ||||||
|  |             return [Changes.checkChange(tag.key, tag.value)]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (tagsFilter instanceof And) { | ||||||
|  |             const and = tagsFilter as And; | ||||||
|  |             for (const tag of and.and) { | ||||||
|  |                 changes = changes.concat(this.tagToChange(tag)); | ||||||
|  |             } | ||||||
|  |             return changes; | ||||||
|  |         } | ||||||
|  |         console.log("Unsupported tagsfilter element to addTag", tagsFilter); | ||||||
|  |         throw "Unsupported tagsFilter element"; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     private uploadChangesWithLatestVersions( |     private uploadChangesWithLatestVersions( | ||||||
|         knownElements, newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) { |         knownElements, newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) { | ||||||
|  |  | ||||||
							
								
								
									
										35
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										35
									
								
								State.ts
									
										
									
									
									
								
							|  | @ -5,8 +5,6 @@ import {Changes} from "./Logic/Osm/Changes"; | ||||||
| import {OsmConnection} from "./Logic/Osm/OsmConnection"; | import {OsmConnection} from "./Logic/Osm/OsmConnection"; | ||||||
| import Locale from "./UI/i18n/Locale"; | import Locale from "./UI/i18n/Locale"; | ||||||
| import Translations from "./UI/i18n/Translations"; | import Translations from "./UI/i18n/Translations"; | ||||||
| import {FilteredLayer} from "./Logic/FilteredLayer"; |  | ||||||
| import {UpdateFromOverpass} from "./Logic/UpdateFromOverpass"; |  | ||||||
| import {UIEventSource} from "./Logic/UIEventSource"; | import {UIEventSource} from "./Logic/UIEventSource"; | ||||||
| import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; | import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; | ||||||
| import {QueryParameters} from "./Logic/Web/QueryParameters"; | import {QueryParameters} from "./Logic/Web/QueryParameters"; | ||||||
|  | @ -20,6 +18,8 @@ import Constants from "./Models/Constants"; | ||||||
| import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; | import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; | ||||||
| import * as L from "leaflet" | import * as L from "leaflet" | ||||||
| import LayerResetter from "./Logic/Actors/LayerResetter"; | import LayerResetter from "./Logic/Actors/LayerResetter"; | ||||||
|  | import UpdateFromOverpass from "./Logic/Actors/UpdateFromOverpass"; | ||||||
|  | import LayerConfig from "./Customizations/JSON/LayerConfig"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Contains the global state: a bunch of UI-event sources |  * Contains the global state: a bunch of UI-event sources | ||||||
|  | @ -62,7 +62,15 @@ export default class State { | ||||||
|     public layerUpdater: UpdateFromOverpass; |     public layerUpdater: UpdateFromOverpass; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([]) |     public filteredLayers: UIEventSource<{ | ||||||
|  |         readonly   name: string | UIElement; | ||||||
|  |         readonly isDisplayed: UIEventSource<boolean>, | ||||||
|  |         readonly  layerDef: LayerConfig; | ||||||
|  |     }[]> = new UIEventSource<{ | ||||||
|  |         readonly   name: string | UIElement; | ||||||
|  |         readonly isDisplayed: UIEventSource<boolean>, | ||||||
|  |         readonly  layerDef: LayerConfig; | ||||||
|  |     }[]>([]) | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      *  The message that should be shown at the center of the screen |      *  The message that should be shown at the center of the screen | ||||||
|  | @ -102,9 +110,9 @@ export default class State { | ||||||
|      * The location as delivered by the GPS |      * The location as delivered by the GPS | ||||||
|      */ |      */ | ||||||
|     public currentGPSLocation: UIEventSource<{ |     public currentGPSLocation: UIEventSource<{ | ||||||
|         latlng: {lat:number, lng:number}, |         latlng: { lat: number, lng: number }, | ||||||
|         accuracy: number |         accuracy: number | ||||||
|     }> = new UIEventSource<{ latlng: {lat:number, lng:number}, accuracy: number }>(undefined); |     }> = new UIEventSource<{ latlng: { lat: number, lng: number }, accuracy: number }>(undefined); | ||||||
|     public layoutDefinition: string; |     public layoutDefinition: string; | ||||||
|     public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; |     public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; | ||||||
| 
 | 
 | ||||||
|  | @ -121,7 +129,7 @@ export default class State { | ||||||
| 
 | 
 | ||||||
|         const zoom = State.asFloat( |         const zoom = State.asFloat( | ||||||
|             QueryParameters.GetQueryParameter("z", "" + layoutToUse.startZoom, "The initial/current zoom level") |             QueryParameters.GetQueryParameter("z", "" + layoutToUse.startZoom, "The initial/current zoom level") | ||||||
|             .syncWith(LocalStorageSource.Get("zoom"))); |                 .syncWith(LocalStorageSource.Get("zoom"))); | ||||||
|         const lat = State.asFloat(QueryParameters.GetQueryParameter("lat", "" + layoutToUse.startLat, "The initial/current latitude") |         const lat = State.asFloat(QueryParameters.GetQueryParameter("lat", "" + layoutToUse.startLat, "The initial/current latitude") | ||||||
|             .syncWith(LocalStorageSource.Get("lat"))); |             .syncWith(LocalStorageSource.Get("lat"))); | ||||||
|         const lon = State.asFloat(QueryParameters.GetQueryParameter("lon", "" + layoutToUse.startLon, "The initial/current longitude of the app") |         const lon = State.asFloat(QueryParameters.GetQueryParameter("lon", "" + layoutToUse.startLon, "The initial/current longitude of the app") | ||||||
|  | @ -163,11 +171,8 @@ export default class State { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         new LayerResetter( |         new LayerResetter( | ||||||
|             this.backgroundLayer,this.locationControl, |             this.backgroundLayer, this.locationControl, | ||||||
|             this.availableBackgroundLayers, this.layoutToUse.map((layout : LayoutConfig)=> layout.defaultBackgroundId)); |             this.availableBackgroundLayers, this.layoutToUse.map((layout: LayoutConfig) => layout.defaultBackgroundId)); | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|          |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         function featSw(key: string, deflt: (layout: LayoutConfig) => boolean, documentation: string): UIEventSource<boolean> { |         function featSw(key: string, deflt: (layout: LayoutConfig) => boolean, documentation: string): UIEventSource<boolean> { | ||||||
|  | @ -204,8 +209,6 @@ export default class State { | ||||||
|             "Disables/Enables the geolocation button"); |             "Disables/Enables the geolocation button"); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|          |  | ||||||
|          |  | ||||||
|         const testParam = QueryParameters.GetQueryParameter("test", "false", |         const testParam = QueryParameters.GetQueryParameter("test", "false", | ||||||
|             "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org").data; |             "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org").data; | ||||||
|         this.osmConnection = new OsmConnection( |         this.osmConnection = new OsmConnection( | ||||||
|  | @ -231,8 +234,8 @@ export default class State { | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|         h.addCallbackAndRun(hash => { |         h.addCallbackAndRun(hash => { | ||||||
|             if(hash === undefined || hash === ""){ |             if (hash === undefined || hash === "") { | ||||||
|                self.selectedElement.setData(undefined); |                 self.selectedElement.setData(undefined); | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  | @ -284,7 +287,7 @@ export default class State { | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|    private static asFloat(source: UIEventSource<string>): UIEventSource<number> { |     private static asFloat(source: UIEventSource<string>): UIEventSource<number> { | ||||||
|         return source.map(str => { |         return source.map(str => { | ||||||
|             let parsed = parseFloat(str); |             let parsed = parseFloat(str); | ||||||
|             return isNaN(parsed) ? undefined : parsed; |             return isNaN(parsed) ? undefined : parsed; | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| import {UIElement} from "./UIElement"; | import {UIElement} from "./UIElement"; | ||||||
| import {Tag, TagUtils} from "../Logic/Tags"; | import {Tag, TagUtils} from "../Logic/Tags"; | ||||||
| import {FilteredLayer} from "../Logic/FilteredLayer"; |  | ||||||
| import Translations from "./i18n/Translations"; | import Translations from "./i18n/Translations"; | ||||||
| import Combine from "./Base/Combine"; | import Combine from "./Base/Combine"; | ||||||
| import {SubtleButton} from "./Base/SubtleButton"; | import {SubtleButton} from "./Base/SubtleButton"; | ||||||
|  | @ -25,7 +24,9 @@ export class SimpleAddUI extends UIElement { | ||||||
|         name: string | UIElement, |         name: string | UIElement, | ||||||
|         icon: UIElement, |         icon: UIElement, | ||||||
|         tags: Tag[], |         tags: Tag[], | ||||||
|         layerToAddTo: FilteredLayer |         layerToAddTo: { | ||||||
|  |             name: UIElement | string, | ||||||
|  |             isDisplayed: UIEventSource<boolean> } | ||||||
|     }> |     }> | ||||||
|         = new UIEventSource(undefined); |         = new UIEventSource(undefined); | ||||||
|     private confirmButton: UIElement = undefined; |     private confirmButton: UIElement = undefined; | ||||||
|  | @ -81,7 +82,7 @@ export class SimpleAddUI extends UIElement { | ||||||
|                                     "<b>", |                                     "<b>", | ||||||
|                                     Translations.t.general.add.confirmButton.Subs({category: preset.title}), |                                     Translations.t.general.add.confirmButton.Subs({category: preset.title}), | ||||||
|                                     "</b>"])); |                                     "</b>"])); | ||||||
|                             self.confirmButton.onClick(self.CreatePoint(preset.tags, layer)); |                             self.confirmButton.onClick(self.CreatePoint(preset.tags)); | ||||||
|                             self._confirmDescription = preset.description; |                             self._confirmDescription = preset.description; | ||||||
|                             self._confirmPreset.setData({ |                             self._confirmPreset.setData({ | ||||||
|                                 tags: preset.tags, |                                 tags: preset.tags, | ||||||
|  | @ -112,13 +113,11 @@ export class SimpleAddUI extends UIElement { | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private CreatePoint(tags: Tag[], layerToAddTo: FilteredLayer) { |     private CreatePoint(tags: Tag[]) { | ||||||
|         return () => { |         return () => { | ||||||
| 
 |  | ||||||
|             const loc = State.state.LastClickLocation.data; |             const loc = State.state.LastClickLocation.data; | ||||||
|             let feature = State.state.changes.createElement(tags, loc.lat, loc.lon); |             let feature = State.state.changes.createElement(tags, loc.lat, loc.lon); | ||||||
|             State.state.selectedElement.setData(feature); |             State.state.selectedElement.setData(feature); | ||||||
|             layerToAddTo.AddNewElement(feature); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -130,7 +129,7 @@ export class SimpleAddUI extends UIElement { | ||||||
|              |              | ||||||
|             if(!this._confirmPreset.data.layerToAddTo.isDisplayed.data){ |             if(!this._confirmPreset.data.layerToAddTo.isDisplayed.data){ | ||||||
|                 return new Combine([ |                 return new Combine([ | ||||||
|                     Translations.t.general.add.layerNotEnabled.Subs({layer: this._confirmPreset.data.layerToAddTo.layerDef.name}) |                     Translations.t.general.add.layerNotEnabled.Subs({layer: this._confirmPreset.data.layerToAddTo.name}) | ||||||
|                         .SetClass("alert"), |                         .SetClass("alert"), | ||||||
|                     this.openLayerControl, |                     this.openLayerControl, | ||||||
|                      |                      | ||||||
|  |  | ||||||
|  | @ -75,14 +75,7 @@ | ||||||
|       ], |       ], | ||||||
|       "hideUnderlayingFeaturesMinPercentage": 10, |       "hideUnderlayingFeaturesMinPercentage": 10, | ||||||
|       "icon": { |       "icon": { | ||||||
|         "render": "./assets/themes/buurtnatuur/nature_reserve.svg", |         "render": "circle:#ffffff;./assets/themes/buurtnatuur/nature_reserve.svg" | ||||||
|         "mappings": [ |  | ||||||
|           { |  | ||||||
|             "#": "This is a little bit a hack to force a circle to be shown while keeping the icon in the 'new' menu", |  | ||||||
|             "if": "id~node/[0-9]*", |  | ||||||
|             "then": "$circle" |  | ||||||
|           } |  | ||||||
|         ] |  | ||||||
|       }, |       }, | ||||||
|       "width": { |       "width": { | ||||||
|         "render": "5" |         "render": "5" | ||||||
|  | @ -179,14 +172,7 @@ | ||||||
|       ], |       ], | ||||||
|       "hideUnderlayingFeaturesMinPercentage": 10, |       "hideUnderlayingFeaturesMinPercentage": 10, | ||||||
|       "icon": { |       "icon": { | ||||||
|         "render": "./assets/themes/buurtnatuur/park.svg", |         "render": "circle:#ffffff;./assets/themes/buurtnatuur/park.svg" | ||||||
|         "mappings": [ |  | ||||||
|           { |  | ||||||
|             "#": "This is a little bit a hack to force a circle to be shown while keeping the icon in the 'new' menu", |  | ||||||
|             "if": "id~node/[0-9]*", |  | ||||||
|             "then": "$circle" |  | ||||||
|           } |  | ||||||
|         ] |  | ||||||
|       }, |       }, | ||||||
|       "width": { |       "width": { | ||||||
|         "render": "5" |         "render": "5" | ||||||
|  | @ -271,14 +257,7 @@ | ||||||
|       ], |       ], | ||||||
|       "hideUnderlayingFeaturesMinPercentage": 0, |       "hideUnderlayingFeaturesMinPercentage": 0, | ||||||
|       "icon": { |       "icon": { | ||||||
|         "render": "./assets/themes/buurtnatuur/forest.svg", |         "render": "circle:#ffffff;./assets/themes/buurtnatuur/forest.svg" | ||||||
|         "mappings": [ |  | ||||||
|           { |  | ||||||
|             "#": "This is a little bit a hack to force a circle to be shown while keeping the icon in the 'new' menu", |  | ||||||
|             "if": "id~node/[0-9]*", |  | ||||||
|             "then": "$circle" |  | ||||||
|           } |  | ||||||
|         ] |  | ||||||
|       }, |       }, | ||||||
|       "width": { |       "width": { | ||||||
|         "render": "5" |         "render": "5" | ||||||
|  |  | ||||||
|  | @ -3,9 +3,12 @@ | ||||||
|   "version": "0.0.5", |   "version": "0.0.5", | ||||||
|   "repository": "https://github.com/pietervdvn/MapComplete", |   "repository": "https://github.com/pietervdvn/MapComplete", | ||||||
|   "description": "A small website to edit OSM easily", |   "description": "A small website to edit OSM easily", | ||||||
|  |   "bugs": "https://github.com/pietervdvn/MapComplete/issues", | ||||||
|  |   "homepage": "https://mapcomplete.osm.be", | ||||||
|   "main": "index.js", |   "main": "index.js", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "start": "parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", |     "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", | ||||||
|  |     "start": "npm run increase-memory && parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", | ||||||
|     "test": "ts-node test/*", |     "test": "ts-node test/*", | ||||||
|     "generate:editor-layer-index": "cd assets/ && wget https://osmlab.github.io/editor-layer-index/imagery.geojson --output-document=editor-layer-index.json", |     "generate:editor-layer-index": "cd assets/ && wget https://osmlab.github.io/editor-layer-index/imagery.geojson --output-document=editor-layer-index.json", | ||||||
|     "generate:images": "ts-node scripts/generateIncludedImages.ts", |     "generate:images": "ts-node scripts/generateIncludedImages.ts", | ||||||
|  | @ -24,7 +27,7 @@ | ||||||
|     "Editor" |     "Editor" | ||||||
|   ], |   ], | ||||||
|   "author": "pietervdvn", |   "author": "pietervdvn", | ||||||
|   "license": "MIT", |   "license": "GPL", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@types/leaflet-providers": "^1.2.0", |     "@types/leaflet-providers": "^1.2.0", | ||||||
|     "country-language": "^0.1.7", |     "country-language": "^0.1.7", | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue