forked from MapComplete/MapComplete
		
	Huge refactoring: split readonly and writable stores
This commit is contained in:
		
							parent
							
								
									0946d8ac9c
								
							
						
					
					
						commit
						4283b76f36
					
				
					 95 changed files with 819 additions and 625 deletions
				
			
		|  | @ -1,14 +1,14 @@ | |||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {ImmutableStore, Store, UIEventSource} from "../UIEventSource"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| 
 | ||||
| export interface AvailableBaseLayersObj { | ||||
|     readonly osmCarto: BaseLayer; | ||||
|     layerOverview: BaseLayer[]; | ||||
| 
 | ||||
|     AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> | ||||
|     AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> | ||||
| 
 | ||||
|     SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer>; | ||||
|     SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: Store<string | string[]>): Store<BaseLayer>; | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
|  | @ -24,12 +24,12 @@ export default class AvailableBaseLayers { | |||
| 
 | ||||
|     private static implementation: AvailableBaseLayersObj | ||||
| 
 | ||||
|     static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> { | ||||
|         return AvailableBaseLayers.implementation?.AvailableLayersAt(location) ?? new UIEventSource<BaseLayer[]>([]); | ||||
|     static AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> { | ||||
|         return AvailableBaseLayers.implementation?.AvailableLayersAt(location) ?? new ImmutableStore<BaseLayer[]>([]); | ||||
|     } | ||||
| 
 | ||||
|     static SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> { | ||||
|         return AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo(location, preferedCategory) ?? new UIEventSource<BaseLayer>(undefined); | ||||
|     static SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: UIEventSource<string | string[]>): Store<BaseLayer> { | ||||
|         return AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo(location, preferedCategory) ?? new ImmutableStore<BaseLayer>(undefined); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Store, Stores} from "../UIEventSource"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import {GeoOperations} from "../GeoOperations"; | ||||
| import * as editorlayerindex from "../../assets/editor-layer-index.json"; | ||||
|  | @ -29,7 +29,7 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL | |||
| 
 | ||||
|     public readonly layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat(AvailableBaseLayersImplementation.LoadProviderIndex()); | ||||
|     public readonly globalLayers = this.layerOverview.filter(layer => layer.feature?.geometry === undefined || layer.feature?.geometry === null) | ||||
|     public readonly localLayers = this.layerOverview.filter(layer => layer.feature?.geometry !== undefined && layer.featuer?.geometry !== null) | ||||
|     public readonly localLayers = this.layerOverview.filter(layer => layer.feature?.geometry !== undefined && layer.feature?.geometry !== null) | ||||
| 
 | ||||
|     private static LoadRasterIndex(): BaseLayer[] { | ||||
|         const layers: BaseLayer[] = [] | ||||
|  | @ -202,8 +202,8 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL | |||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     public AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> { | ||||
|         return UIEventSource.ListStabilized(location.map( | ||||
|     public AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> { | ||||
|         return Stores.ListStabilized(location.map( | ||||
|             (currentLocation) => { | ||||
|                 if (currentLocation === undefined) { | ||||
|                     return this.layerOverview; | ||||
|  | @ -212,7 +212,7 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL | |||
|             })); | ||||
|     } | ||||
| 
 | ||||
|     public SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> { | ||||
|     public SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: Store<string | string[]>): Store<BaseLayer> { | ||||
|         return this.AvailableLayersAt(location) | ||||
|             .map(available => { | ||||
|                 // First float all 'best layers' to the top
 | ||||
|  | @ -264,7 +264,7 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL | |||
|         if (lon === undefined || lat === undefined) { | ||||
|             return availableLayers.concat(this.globalLayers); | ||||
|         } | ||||
|         const lonlat = [lon, lat]; | ||||
|         const lonlat : [number, number] = [lon, lat]; | ||||
|         for (const layerOverviewItem of this.localLayers) { | ||||
|             const layer = layerOverviewItem; | ||||
|             const bbox = BBox.get(layer.feature) | ||||
|  |  | |||
|  | @ -1,12 +1,12 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import Svg from "../../Svg"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| import {VariableUiElement} from "../../UI/Base/VariableUIElement"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {QueryParameters} from "../Web/QueryParameters"; | ||||
| import FeatureSource from "../FeatureSource/FeatureSource"; | ||||
| import {BBox} from "../BBox"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"; | ||||
| 
 | ||||
| export interface GeoLocationPointProperties { | ||||
|     id: "gps", | ||||
|  | @ -22,7 +22,7 @@ export interface GeoLocationPointProperties { | |||
| 
 | ||||
| export default class GeoLocationHandler extends VariableUiElement { | ||||
| 
 | ||||
|     private readonly currentLocation?: FeatureSource | ||||
|     private readonly currentLocation?: SimpleFeatureSource | ||||
| 
 | ||||
|     /** | ||||
|      * Wether or not the geolocation is active, aka the user requested the current location | ||||
|  | @ -43,7 +43,7 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|      * Literally: _currentGPSLocation.data != undefined | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _hasLocation: UIEventSource<boolean>; | ||||
|     private readonly _hasLocation: Store<boolean>; | ||||
|     private readonly _currentGPSLocation: UIEventSource<Coordinates>; | ||||
|     /** | ||||
|      * Kept in order to update the marker | ||||
|  | @ -70,7 +70,7 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|     constructor( | ||||
|         state: { | ||||
|             selectedElement: UIEventSource<any>; | ||||
|             currentUserLocation?: FeatureSource, | ||||
|             currentUserLocation?: SimpleFeatureSource, | ||||
|             leafletMap: UIEventSource<any>, | ||||
|             layoutToUse: LayoutConfig, | ||||
|             featureSwitchGeolocation: UIEventSource<boolean> | ||||
|  | @ -236,12 +236,9 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
| 
 | ||||
|             self.currentLocation?.features?.setData([{feature, freshness: new Date()}]) | ||||
| 
 | ||||
|             const timeSinceRequest = | ||||
|                 (new Date().getTime() - (self._lastUserRequest.data?.getTime() ?? 0)) / 1000; | ||||
| 
 | ||||
|             if (willFocus.data) { | ||||
|                 console.log("Zooming to user location: willFocus is set") | ||||
|                 willFocus.setData(false) | ||||
|                 lastClick.setData(undefined); | ||||
|                 autozoomDone = true; | ||||
|                 self.MoveToCurrentLocation(16); | ||||
|             } else if (self._isLocked.data) { | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import {Or} from "../Tags/Or"; | ||||
| import {Overpass} from "../Osm/Overpass"; | ||||
| import FeatureSource from "../FeatureSource/FeatureSource"; | ||||
|  | @ -34,13 +34,13 @@ export default class OverpassFeatureSource implements FeatureSource { | |||
|     private readonly retries: UIEventSource<number> = new UIEventSource<number>(0); | ||||
| 
 | ||||
|     private readonly state: { | ||||
|         readonly locationControl: UIEventSource<Loc>, | ||||
|         readonly locationControl: Store<Loc>, | ||||
|         readonly layoutToUse: LayoutConfig, | ||||
|         readonly overpassUrl: UIEventSource<string[]>; | ||||
|         readonly overpassTimeout: UIEventSource<number>; | ||||
|         readonly currentBounds: UIEventSource<BBox> | ||||
|         readonly overpassUrl: Store<string[]>; | ||||
|         readonly overpassTimeout: Store<number>; | ||||
|         readonly currentBounds: Store<BBox> | ||||
|     } | ||||
|     private readonly _isActive: UIEventSource<boolean> | ||||
|     private readonly _isActive: Store<boolean> | ||||
|     /** | ||||
|      * Callback to handle all the data | ||||
|      */ | ||||
|  | @ -54,16 +54,16 @@ export default class OverpassFeatureSource implements FeatureSource { | |||
| 
 | ||||
|     constructor( | ||||
|         state: { | ||||
|             readonly locationControl: UIEventSource<Loc>, | ||||
|             readonly locationControl: Store<Loc>, | ||||
|             readonly layoutToUse: LayoutConfig, | ||||
|             readonly overpassUrl: UIEventSource<string[]>; | ||||
|             readonly overpassTimeout: UIEventSource<number>; | ||||
|             readonly overpassMaxZoom: UIEventSource<number>, | ||||
|             readonly currentBounds: UIEventSource<BBox> | ||||
|             readonly overpassUrl: Store<string[]>; | ||||
|             readonly overpassTimeout: Store<number>; | ||||
|             readonly overpassMaxZoom: Store<number>, | ||||
|             readonly currentBounds: Store<BBox> | ||||
|         }, | ||||
|         options: { | ||||
|             padToTiles: UIEventSource<number>, | ||||
|             isActive?: UIEventSource<boolean>, | ||||
|             padToTiles: Store<number>, | ||||
|             isActive?: Store<boolean>, | ||||
|             relationTracker: RelationsTracker, | ||||
|             onBboxLoaded?: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void, | ||||
|             freshnesses?: Map<string, TileFreshnessCalculator> | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import Translations from "../../UI/i18n/Translations"; | ||||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import Locale from "../../UI/i18n/Locale"; | ||||
| import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer"; | ||||
| import Combine from "../../UI/Base/Combine"; | ||||
|  | @ -9,11 +8,11 @@ import {Utils} from "../../Utils"; | |||
| 
 | ||||
| export default class TitleHandler { | ||||
|     constructor(state: { | ||||
|         selectedElement: UIEventSource<any>, | ||||
|         selectedElement: Store<any>, | ||||
|         layoutToUse: LayoutConfig, | ||||
|         allElements: ElementStorage | ||||
|     }) { | ||||
|         const currentTitle: UIEventSource<string> = state.selectedElement.map( | ||||
|         const currentTitle: Store<string> = state.selectedElement.map( | ||||
|             selected => { | ||||
|                 const layout = state.layoutToUse | ||||
|                 const defaultTitle = layout?.title?.txt ?? "MapComplete" | ||||
|  |  | |||
|  | @ -1,12 +1,12 @@ | |||
| import FeatureSource from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import {Store} from "../../UIEventSource"; | ||||
| import {ElementStorage} from "../../ElementStorage"; | ||||
| 
 | ||||
| /** | ||||
|  * Makes sure that every feature is added to the ElementsStorage, so that the tags-eventsource can be retrieved | ||||
|  */ | ||||
| export default class RegisteringAllFromFeatureSourceActor { | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly features: Store<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly name; | ||||
| 
 | ||||
|     constructor(source: FeatureSource, allElements: ElementStorage) { | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import FilteringFeatureSource from "./Sources/FilteringFeatureSource"; | |||
| import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter"; | ||||
| import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "./FeatureSource"; | ||||
| import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import {TileHierarchyTools} from "./TiledFeatureSource/TileHierarchy"; | ||||
| import RememberingSource from "./Sources/RememberingSource"; | ||||
| import OverpassFeatureSource from "../Actors/OverpassFeatureSource"; | ||||
|  | @ -38,8 +38,8 @@ import {ElementStorage} from "../ElementStorage"; | |||
|  */ | ||||
| export default class FeaturePipeline { | ||||
| 
 | ||||
|     public readonly sufficientlyZoomed: UIEventSource<boolean>; | ||||
|     public readonly runningQuery: UIEventSource<boolean>; | ||||
|     public readonly sufficientlyZoomed: Store<boolean>; | ||||
|     public readonly runningQuery: Store<boolean>; | ||||
|     public readonly timeout: UIEventSource<number>; | ||||
|     public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||
|     public readonly newDataLoadedSignal: UIEventSource<FeatureSource> = new UIEventSource<FeatureSource>(undefined) | ||||
|  | @ -314,7 +314,7 @@ export default class FeaturePipeline { | |||
|                 // We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this
 | ||||
|                 perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer) | ||||
|                 // AT last, we always apply the metatags whenever possible
 | ||||
|                 perLayer.features.addCallbackAndRunD(feats => { | ||||
|                 perLayer.features.addCallbackAndRunD(_ => { | ||||
|                     self.onNewDataLoaded(perLayer); | ||||
|                 }) | ||||
| 
 | ||||
|  | @ -417,7 +417,7 @@ export default class FeaturePipeline { | |||
|     /* | ||||
|     * Gives an UIEventSource containing the tileIndexes of the tiles that should be loaded from OSM | ||||
|     * */ | ||||
|     private getNeededTilesFromOsm(isSufficientlyZoomed: UIEventSource<boolean>): UIEventSource<number[]> { | ||||
|     private getNeededTilesFromOsm(isSufficientlyZoomed: Store<boolean>): Store<number[]> { | ||||
|         const self = this | ||||
|         return this.state.currentBounds.map(bbox => { | ||||
|             if (bbox === undefined) { | ||||
|  | @ -450,12 +450,12 @@ export default class FeaturePipeline { | |||
|     private initOverpassUpdater(state: { | ||||
|         allElements: ElementStorage; | ||||
|         layoutToUse: LayoutConfig, | ||||
|         currentBounds: UIEventSource<BBox>, | ||||
|         locationControl: UIEventSource<Loc>, | ||||
|         readonly overpassUrl: UIEventSource<string[]>; | ||||
|         readonly overpassTimeout: UIEventSource<number>; | ||||
|         readonly overpassMaxZoom: UIEventSource<number>, | ||||
|     }, useOsmApi: UIEventSource<boolean>): OverpassFeatureSource { | ||||
|         currentBounds: Store<BBox>, | ||||
|         locationControl: Store<Loc>, | ||||
|         readonly overpassUrl: Store<string[]>; | ||||
|         readonly overpassTimeout: Store<number>; | ||||
|         readonly overpassMaxZoom: Store<number>, | ||||
|     }, useOsmApi: Store<boolean>): OverpassFeatureSource { | ||||
|         const minzoom = Math.min(...state.layoutToUse.layers.map(layer => layer.minzoom)) | ||||
|         const overpassIsActive = state.currentBounds.map(bbox => { | ||||
|             if (bbox === undefined) { | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import {BBox} from "../BBox"; | ||||
| 
 | ||||
| export default interface FeatureSource { | ||||
|     features: UIEventSource<{ feature: any, freshness: Date }[]>; | ||||
|     features: Store<{ feature: any, freshness: Date }[]>; | ||||
|     /** | ||||
|      * Mainly used for debuging | ||||
|      */ | ||||
|  | @ -26,14 +26,14 @@ export interface FeatureSourceForLayer extends FeatureSource { | |||
|  * A feature source which is aware of the indexes it contains | ||||
|  */ | ||||
| export interface IndexedFeatureSource extends FeatureSource { | ||||
|     readonly containedIds: UIEventSource<Set<string>> | ||||
|     readonly containedIds: Store<Set<string>> | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * A feature source which has some extra data about it's state | ||||
|  */ | ||||
| export interface FeatureSourceState { | ||||
|     readonly sufficientlyZoomed: UIEventSource<boolean>; | ||||
|     readonly runningQuery: UIEventSource<boolean>; | ||||
|     readonly timeout: UIEventSource<number>; | ||||
|     readonly sufficientlyZoomed: Store<boolean>; | ||||
|     readonly runningQuery: Store<boolean>; | ||||
|     readonly timeout: Store<number>; | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import FeatureSource, {FeatureSourceForLayer, Tiled} from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Store} from "../UIEventSource"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import SimpleFeatureSource from "./Sources/SimpleFeatureSource"; | ||||
| 
 | ||||
|  | @ -11,7 +11,7 @@ import SimpleFeatureSource from "./Sources/SimpleFeatureSource"; | |||
|  */ | ||||
| export default class PerLayerFeatureSourceSplitter { | ||||
| 
 | ||||
|     constructor(layers: UIEventSource<FilteredLayer[]>, | ||||
|     constructor(layers: Store<FilteredLayer[]>, | ||||
|                 handleLayerData: (source: FeatureSourceForLayer & Tiled) => void, | ||||
|                 upstream: FeatureSource, | ||||
|                 options?: { | ||||
|  | @ -19,7 +19,7 @@ export default class PerLayerFeatureSourceSplitter { | |||
|                     handleLeftovers?: (featuresWithoutLayer: any[]) => void | ||||
|                 }) { | ||||
| 
 | ||||
|         const knownLayers = new Map<string, FeatureSourceForLayer & Tiled>() | ||||
|         const knownLayers = new Map<string, SimpleFeatureSource>() | ||||
| 
 | ||||
|         function update() { | ||||
|             const features = upstream.features?.data; | ||||
|  |  | |||
|  | @ -3,12 +3,12 @@ | |||
|  * Data coming from upstream will always overwrite a previous value | ||||
|  */ | ||||
| import FeatureSource, {Tiled} from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../UIEventSource"; | ||||
| import {BBox} from "../../BBox"; | ||||
| 
 | ||||
| export default class RememberingSource implements FeatureSource, Tiled { | ||||
| 
 | ||||
|     public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>; | ||||
|     public readonly features: Store<{ feature: any, freshness: Date }[]>; | ||||
|     public readonly name; | ||||
|     public readonly tileIndex: number | ||||
|     public readonly bbox: BBox | ||||
|  |  | |||
|  | @ -1,14 +1,14 @@ | |||
| /** | ||||
|  * This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indicates with what renderConfig it should be rendered. | ||||
|  */ | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../UIEventSource"; | ||||
| import {GeoOperations} from "../../GeoOperations"; | ||||
| import FeatureSource from "../FeatureSource"; | ||||
| import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig"; | ||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||
| 
 | ||||
| export default class RenderingMultiPlexerFeatureSource { | ||||
|     public readonly features: UIEventSource<(any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[]>; | ||||
|     public readonly features: Store<(any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[]>; | ||||
| 
 | ||||
|     constructor(upstream: FeatureSource, layer: LayerConfig) { | ||||
|          | ||||
|  | @ -27,7 +27,7 @@ export default class RenderingMultiPlexerFeatureSource { | |||
|         this.features = upstream.features.map( | ||||
|             features => { | ||||
|                 if (features === undefined) { | ||||
|                     return; | ||||
|                     return undefined; | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -48,15 +48,22 @@ export default class RenderingMultiPlexerFeatureSource { | |||
| 
 | ||||
|                 for (const f of features) { | ||||
|                     const feat = f.feature; | ||||
|                     if(feat === undefined){ | ||||
|                         continue | ||||
|                     } | ||||
|                     if(feat.geometry === undefined){ | ||||
|                         console.error("No geometry in ", feat,"provided by", upstream.features.tag, upstream.name) | ||||
|                     } | ||||
|                     if (feat.geometry.type === "Point") { | ||||
| 
 | ||||
|                         for (const rendering of pointRenderings) { | ||||
|                             withIndex.push({ | ||||
|                                 ...feat, | ||||
|                                 pointRenderingIndex: rendering.index | ||||
|                             }) | ||||
|                         } | ||||
|                     } else { | ||||
|                         continue | ||||
|                     } | ||||
|                      | ||||
|                     // This is a a line: add the centroids
 | ||||
|                     let centerpoint: [number, number] = undefined; | ||||
|                     let projectedCenterPoint: [number, number] = undefined | ||||
|  | @ -100,8 +107,6 @@ export default class RenderingMultiPlexerFeatureSource { | |||
|                             lineRenderingIndex: i | ||||
|                         }) | ||||
|                     } | ||||
| 
 | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,31 +1,55 @@ | |||
| import FeatureSource from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {ImmutableStore, Store, UIEventSource} from "../../UIEventSource"; | ||||
| import {stat} from "fs"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {BBox} from "../../BBox"; | ||||
| 
 | ||||
| /** | ||||
|  * A simple dummy implementation for whenever it is needed | ||||
|  * A simple, read only feature store. | ||||
|  */ | ||||
| export default class StaticFeatureSource implements FeatureSource { | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly name: string = "StaticFeatureSource" | ||||
|     public readonly features: Store<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly name: string | ||||
| 
 | ||||
|     constructor(features: any[] | UIEventSource<any[] | UIEventSource<{ feature: any, freshness: Date }>>, useFeaturesDirectly) { | ||||
|         const now = new Date(); | ||||
|     constructor(features: Store<{ feature: any, freshness: Date }[]>, name = "StaticFeatureSource") { | ||||
|         if (features === undefined) { | ||||
|             throw "Static feature source received undefined as source" | ||||
|         } | ||||
|         if (useFeaturesDirectly) { | ||||
|             // @ts-ignore
 | ||||
|             this.features = features | ||||
|         } else if (features instanceof UIEventSource) { | ||||
|             // @ts-ignore
 | ||||
|             this.features = features.map(features => features?.map(f => ({feature: f, freshness: now}) ?? [])) | ||||
|         } else { | ||||
|             this.features = new UIEventSource(features?.map(f => ({ | ||||
|                 feature: f, | ||||
|         this.name = name; | ||||
|         this.features = features; | ||||
|     } | ||||
| 
 | ||||
|     public static fromGeojsonAndDate(features: { feature: any, freshness: Date }[], name = "StaticFeatureSourceFromGeojsonAndDate"): StaticFeatureSource { | ||||
|         return new StaticFeatureSource(new ImmutableStore(features), name); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public static fromGeojson(geojson: any[], name = "StaticFeatureSourceFromGeojson"): StaticFeatureSource { | ||||
|         const now = new Date(); | ||||
|         return StaticFeatureSource.fromGeojsonAndDate(geojson.map(feature => ({feature, freshness: now})), name); | ||||
|     } | ||||
| 
 | ||||
|     static fromDateless(featureSource: Store<{ feature: any }[]>, name = "StaticFeatureSourceFromDateless") { | ||||
|         const now = new Date(); | ||||
|         return new StaticFeatureSource(featureSource.map(features => features.map(feature => ({ | ||||
|             feature: feature.feature, | ||||
|             freshness: now | ||||
|             }))??[]) | ||||
|         }))), name); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class TiledStaticFeatureSource extends StaticFeatureSource implements Tiled, FeatureSourceForLayer{ | ||||
| 
 | ||||
|     public readonly bbox: BBox = BBox.global; | ||||
|     public readonly tileIndex: number;    | ||||
|     public readonly layer: FilteredLayer; | ||||
| 
 | ||||
|     constructor(features: Store<{ feature: any, freshness: Date }[]>, layer: FilteredLayer ,tileIndex : number = 0) { | ||||
|         super(features); | ||||
|         this.tileIndex = tileIndex ; | ||||
|         this.layer=  layer; | ||||
|         this.bbox = BBox.fromTileIndex(this.tileIndex) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -2,7 +2,7 @@ import {Utils} from "../../../Utils"; | |||
| import * as OsmToGeoJson from "osmtogeojson"; | ||||
| import StaticFeatureSource from "../Sources/StaticFeatureSource"; | ||||
| import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../UIEventSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
|  | @ -20,13 +20,13 @@ export default class OsmFeatureSource { | |||
|     public readonly downloadedTiles = new Set<number>() | ||||
|     public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = [] | ||||
|     private readonly _backend: string; | ||||
|     private readonly filteredLayers: UIEventSource<FilteredLayer[]>; | ||||
|     private readonly filteredLayers: Store<FilteredLayer[]>; | ||||
|     private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void; | ||||
|     private isActive: UIEventSource<boolean>; | ||||
|     private isActive: Store<boolean>; | ||||
|     private options: { | ||||
|         handleTile: (tile: FeatureSourceForLayer & Tiled) => void; | ||||
|         isActive: UIEventSource<boolean>, | ||||
|         neededTiles: UIEventSource<number[]>, | ||||
|         isActive: Store<boolean>, | ||||
|         neededTiles: Store<number[]>, | ||||
|         state: { | ||||
|             readonly osmConnection: OsmConnection; | ||||
|         }, | ||||
|  | @ -36,8 +36,8 @@ export default class OsmFeatureSource { | |||
| 
 | ||||
|     constructor(options: { | ||||
|         handleTile: (tile: FeatureSourceForLayer & Tiled) => void; | ||||
|         isActive: UIEventSource<boolean>, | ||||
|         neededTiles: UIEventSource<number[]>, | ||||
|         isActive: Store<boolean>, | ||||
|         neededTiles: Store<number[]>, | ||||
|         state: { | ||||
|             readonly filteredLayers: UIEventSource<FilteredLayer[]>; | ||||
|             readonly osmConnection: OsmConnection; | ||||
|  | @ -119,7 +119,7 @@ export default class OsmFeatureSource { | |||
|                 const index = Tiles.tile_index(z, x, y); | ||||
|                 new PerLayerFeatureSourceSplitter(this.filteredLayers, | ||||
|                     this.handleTile, | ||||
|                     new StaticFeatureSource(geojson.features, false), | ||||
|                     StaticFeatureSource.fromGeojson(geojson.features), | ||||
|                     { | ||||
|                         tileIndex: index | ||||
|                     } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../UIEventSource"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import TileHierarchy from "./TileHierarchy"; | ||||
| import {Tiles} from "../../../Models/TileRange"; | ||||
|  | @ -24,7 +24,7 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, | |||
|     public readonly maxFeatureCount: number; | ||||
|     public readonly name; | ||||
|     public readonly features: UIEventSource<{ feature: any, freshness: Date }[]> | ||||
|     public readonly containedIds: UIEventSource<Set<string>> | ||||
|     public readonly containedIds: Store<Set<string>> | ||||
| 
 | ||||
|     public readonly bbox: BBox; | ||||
|     public readonly tileIndex: number; | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import {Mapillary} from "./Mapillary"; | |||
| import {WikimediaImageProvider} from "./WikimediaImageProvider"; | ||||
| import {Imgur} from "./Imgur"; | ||||
| import GenericImageProvider from "./GenericImageProvider"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import ImageProvider, {ProvidedImage} from "./ImageProvider"; | ||||
| import {WikidataImageProvider} from "./WikidataImageProvider"; | ||||
| 
 | ||||
|  | @ -37,7 +37,7 @@ export default class AllImageProviders { | |||
| 
 | ||||
|     private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map<string, UIEventSource<ProvidedImage[]>>() | ||||
| 
 | ||||
|     public static LoadImagesFor(tags: UIEventSource<any>, tagKey?: string[]): UIEventSource<ProvidedImage[]> { | ||||
|     public static LoadImagesFor(tags: Store<any>, tagKey?: string[]): Store<ProvidedImage[]> { | ||||
|         if (tags.data.id === undefined) { | ||||
|             return undefined; | ||||
|         } | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Store, Stores, UIEventSource} from "../UIEventSource"; | ||||
| import BaseUIElement from "../../UI/BaseUIElement"; | ||||
| import {LicenseInfo} from "./LicenseInfo"; | ||||
| import {Utils} from "../../Utils"; | ||||
|  | @ -13,14 +13,14 @@ export default abstract class ImageProvider { | |||
| 
 | ||||
|     public abstract readonly defaultKeyPrefixes: string[] | ||||
| 
 | ||||
|     private _cache = new Map<string, UIEventSource<LicenseInfo>>() | ||||
|     private _cache = new Map<string, Store<LicenseInfo>>() | ||||
| 
 | ||||
|     GetAttributionFor(url: string): UIEventSource<LicenseInfo> { | ||||
|     GetAttributionFor(url: string): Store<LicenseInfo> { | ||||
|         const cached = this._cache.get(url); | ||||
|         if (cached !== undefined) { | ||||
|             return cached; | ||||
|         } | ||||
|         const src = UIEventSource.FromPromise(this.DownloadAttribution(url)) | ||||
|         const src = Stores.FromPromise(this.DownloadAttribution(url)) | ||||
|         this._cache.set(url, src) | ||||
|         return src; | ||||
|     } | ||||
|  | @ -30,7 +30,7 @@ export default abstract class ImageProvider { | |||
|     /** | ||||
|      * Given a properies object, maps it onto _all_ the available pictures for this imageProvider | ||||
|      */ | ||||
|     public GetRelevantUrls(allTags: UIEventSource<any>, options?: { | ||||
|     public GetRelevantUrls(allTags: Store<any>, options?: { | ||||
|         prefixes?: string[] | ||||
|     }): UIEventSource<ProvidedImage[]> { | ||||
|         const prefixes = options?.prefixes ?? this.defaultKeyPrefixes | ||||
|  |  | |||
|  | @ -182,7 +182,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { | |||
|             features.push(newGeometry) | ||||
| 
 | ||||
|         } | ||||
|         return new StaticFeatureSource(features, false) | ||||
|         return StaticFeatureSource.fromGeojson(features) | ||||
|     } | ||||
| 
 | ||||
|     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
|  |  | |||
|  | @ -159,7 +159,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         return new StaticFeatureSource(Utils.NoNull(preview), false) | ||||
|         return StaticFeatureSource.fromGeojson(Utils.NoNull(preview)) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -327,7 +327,7 @@ export class Changes { | |||
|             const successes = await Promise.all(Array.from(pendingPerTheme, | ||||
|                 async ([theme, pendingChanges]) => { | ||||
|                     try { | ||||
|                         const openChangeset = this.state.osmConnection.GetPreference("current-open-changeset-" + theme).map( | ||||
|                         const openChangeset = this.state.osmConnection.GetPreference("current-open-changeset-" + theme).sync( | ||||
|                             str => { | ||||
|                                 const n = Number(str); | ||||
|                                 if (isNaN(n)) { | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import osmAuth from "osm-auth"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Stores, UIEventSource} from "../UIEventSource"; | ||||
| import {OsmPreferences} from "./OsmPreferences"; | ||||
| import {ChangesetHandler} from "./ChangesetHandler"; | ||||
| import {ElementStorage} from "../ElementStorage"; | ||||
|  | @ -228,7 +228,7 @@ export class OsmConnection { | |||
|         } | ||||
|         if (this._dryRun.data) { | ||||
|             console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text) | ||||
|             return new Promise((ok, error) => { | ||||
|             return new Promise((ok) => { | ||||
|                 ok() | ||||
|             }); | ||||
|         } | ||||
|  | @ -236,7 +236,7 @@ export class OsmConnection { | |||
|             this.auth.xhr({ | ||||
|                 method: 'POST', | ||||
|                 path: `/api/0.6/notes/${id}/close${textSuffix}`, | ||||
|             }, function (err, response) { | ||||
|             }, function (err, _) { | ||||
|                 if (err !== null) { | ||||
|                     error(err) | ||||
|                 } else { | ||||
|  | @ -251,7 +251,7 @@ export class OsmConnection { | |||
|     public reopenNote(id: number | string, text?: string): Promise<void> { | ||||
|         if (this._dryRun.data) { | ||||
|             console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text) | ||||
|             return new Promise((ok, error) => { | ||||
|             return new Promise((ok) => { | ||||
|                 ok() | ||||
|             }); | ||||
|         } | ||||
|  | @ -263,7 +263,7 @@ export class OsmConnection { | |||
|             this.auth.xhr({ | ||||
|                 method: 'POST', | ||||
|                 path: `/api/0.6/notes/${id}/reopen${textSuffix}` | ||||
|             }, function (err, response) { | ||||
|             }, function (err, _) { | ||||
|                 if (err !== null) { | ||||
|                     error(err) | ||||
|                 } else { | ||||
|  | @ -278,7 +278,7 @@ export class OsmConnection { | |||
|     public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> { | ||||
|         if (this._dryRun.data) { | ||||
|             console.warn("Dryrun enabled - not actually opening note with text ", text) | ||||
|             return new Promise<{ id: number }>((ok, error) => { | ||||
|             return new Promise<{ id: number }>((ok) => { | ||||
|                 window.setTimeout(() => ok({id: Math.floor(Math.random() * 1000)}), Math.random() * 5000) | ||||
|             }); | ||||
|         } | ||||
|  | @ -315,7 +315,7 @@ export class OsmConnection { | |||
|     public addCommentToNode(id: number | string, text: string): Promise<void> { | ||||
|         if (this._dryRun.data) { | ||||
|             console.warn("Dryrun enabled - not actually adding comment ", text, "to  note ", id) | ||||
|             return new Promise((ok, error) => { | ||||
|             return new Promise((ok) => { | ||||
|                 ok() | ||||
|             }); | ||||
|         } | ||||
|  | @ -328,7 +328,7 @@ export class OsmConnection { | |||
|                 method: 'POST', | ||||
| 
 | ||||
|                 path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}` | ||||
|             }, function (err, response) { | ||||
|             }, function (err, _) { | ||||
|                 if (err !== null) { | ||||
|                     error(err) | ||||
|                 } else { | ||||
|  | @ -374,7 +374,7 @@ export class OsmConnection { | |||
|             return; | ||||
|         } | ||||
|         this.isChecking = true; | ||||
|         UIEventSource.Chronic(5 * 60 * 1000).addCallback(_ => { | ||||
|         Stores.Chronic(5 * 60 * 1000).addCallback(_ => { | ||||
|             if (self.isLoggedIn.data) { | ||||
|                 console.log("Checking for messages") | ||||
|                 self.AttemptLogin(); | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import {Utils} from "../../Utils"; | ||||
| import * as polygon_features from "../../assets/polygon-features.json"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Store, Stores, UIEventSource} from "../UIEventSource"; | ||||
| import {BBox} from "../BBox"; | ||||
| 
 | ||||
| 
 | ||||
|  | @ -40,7 +40,7 @@ export abstract class OsmObject { | |||
|         this.backendURL = url; | ||||
|     } | ||||
| 
 | ||||
|     public static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> { | ||||
|     public static DownloadObject(id: string, forceRefresh: boolean = false): Store<OsmObject> { | ||||
|         let src: UIEventSource<OsmObject>; | ||||
|         if (OsmObject.objectCache.has(id)) { | ||||
|             src = OsmObject.objectCache.get(id) | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import {TagsFilter} from "../Tags/TagsFilter"; | ||||
| import RelationsTracker from "./RelationsTracker"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {ImmutableStore, Store} from "../UIEventSource"; | ||||
| import {BBox} from "../BBox"; | ||||
| import * as osmtogeojson from "osmtogeojson"; | ||||
| import {FeatureCollection} from "@turf/turf"; | ||||
|  | @ -12,7 +12,7 @@ import {FeatureCollection} from "@turf/turf"; | |||
| export class Overpass { | ||||
|     private _filter: TagsFilter | ||||
|     private readonly _interpreterUrl: string; | ||||
|     private readonly _timeout: UIEventSource<number>; | ||||
|     private readonly _timeout: Store<number>; | ||||
|     private readonly _extraScripts: string[]; | ||||
|     private _includeMeta: boolean; | ||||
|     private _relationTracker: RelationsTracker; | ||||
|  | @ -20,10 +20,10 @@ export class Overpass { | |||
|     constructor(filter: TagsFilter, | ||||
|                 extraScripts: string[], | ||||
|                 interpreterUrl: string, | ||||
|                 timeout?: UIEventSource<number>, | ||||
|                 timeout?: Store<number>, | ||||
|                 relationTracker?: RelationsTracker, | ||||
|                 includeMeta = true) { | ||||
|         this._timeout = timeout ?? new UIEventSource<number>(90); | ||||
|         this._timeout = timeout ?? new ImmutableStore<number>(90); | ||||
|         this._interpreterUrl = interpreterUrl; | ||||
|         const optimized = filter.optimize() | ||||
|         if(optimized === true || optimized === false){ | ||||
|  |  | |||
|  | @ -56,8 +56,9 @@ export default class FeatureSwitchState { | |||
|             ); | ||||
| 
 | ||||
|             // It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened
 | ||||
|             return queryParam.map((str) => | ||||
|                 str === undefined ? defaultValue : str !== "false" | ||||
|             return queryParam.sync((str) => | ||||
|                 str === undefined ? defaultValue : str !== "false", [], | ||||
|                 b => b == defaultValue ? undefined : (""+b) | ||||
|             ) | ||||
| 
 | ||||
|         } | ||||
|  | @ -163,7 +164,7 @@ export default class FeatureSwitchState { | |||
|         this.overpassUrl = QueryParameters.GetQueryParameter("overpassUrl", | ||||
|             (layoutToUse?.overpassUrl ?? Constants.defaultOverpassUrls).join(","), | ||||
|             "Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter" | ||||
|         ).map(param => param.split(","), [], urls => urls.join(",")) | ||||
|         ).sync(param => param.split(","), [], urls => urls.join(",")) | ||||
| 
 | ||||
|         this.overpassTimeout = UIEventSource.asFloat(QueryParameters.GetQueryParameter("overpassTimeout", | ||||
|             "" + layoutToUse?.overpassTimeout, | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import UserRelatedState from "./UserRelatedState"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Store, Stores, UIEventSource} from "../UIEventSource"; | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import AvailableBaseLayers from "../Actors/AvailableBaseLayers"; | ||||
|  | @ -18,6 +18,7 @@ import {GeoOperations} from "../GeoOperations"; | |||
| import TitleHandler from "../Actors/TitleHandler"; | ||||
| import {BBox} from "../BBox"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import {TiledStaticFeatureSource} from "../FeatureSource/Sources/StaticFeatureSource"; | ||||
| 
 | ||||
| /** | ||||
|  * Contains all the leaflet-map related state | ||||
|  | @ -31,7 +32,7 @@ export default class MapState extends UserRelatedState { | |||
|     /** | ||||
|      * A list of currently available background layers | ||||
|      */ | ||||
|     public availableBackgroundLayers: UIEventSource<BaseLayer[]>; | ||||
|     public availableBackgroundLayers: Store<BaseLayer[]>; | ||||
| 
 | ||||
|     /** | ||||
|      * The current background layer | ||||
|  | @ -52,12 +53,12 @@ export default class MapState extends UserRelatedState { | |||
|     /** | ||||
|      * The location as delivered by the GPS | ||||
|      */ | ||||
|     public currentUserLocation: FeatureSourceForLayer & Tiled; | ||||
|     public currentUserLocation: SimpleFeatureSource; | ||||
| 
 | ||||
|     /** | ||||
|      * All previously visited points | ||||
|      */ | ||||
|     public historicalUserLocations: FeatureSourceForLayer & Tiled; | ||||
|     public historicalUserLocations: SimpleFeatureSource; | ||||
|     /** | ||||
|      * The number of seconds that the GPS-locations are stored in memory. | ||||
|      * Time in seconds | ||||
|  | @ -176,7 +177,7 @@ export default class MapState extends UserRelatedState { | |||
| 
 | ||||
|         let i = 0 | ||||
|         const self = this; | ||||
|         const features: UIEventSource<{ feature: any, freshness: Date }[]> = this.currentBounds.map(bounds => { | ||||
|         const features: Store<{ feature: any, freshness: Date }[]> = this.currentBounds.map(bounds => { | ||||
|             if (bounds === undefined) { | ||||
|                 return [] | ||||
|             } | ||||
|  | @ -205,7 +206,7 @@ export default class MapState extends UserRelatedState { | |||
|             return [feature] | ||||
|         }) | ||||
| 
 | ||||
|         this.currentView = new SimpleFeatureSource(currentViewLayer, 0, features) | ||||
|         this.currentView = new TiledStaticFeatureSource(features,  currentViewLayer); | ||||
|     } | ||||
| 
 | ||||
|     private initGpsLocation() { | ||||
|  | @ -289,13 +290,13 @@ export default class MapState extends UserRelatedState { | |||
|         }) | ||||
|         let gpsLineLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_track")[0] | ||||
|         if (gpsLineLayerDef !== undefined) { | ||||
|             this.historicalUserLocationsTrack = new SimpleFeatureSource(gpsLineLayerDef, Tiles.tile_index(0, 0, 0), asLine); | ||||
|             this.historicalUserLocationsTrack = new TiledStaticFeatureSource(asLine, gpsLineLayerDef); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private initHomeLocation() { | ||||
|         const empty = [] | ||||
|         const feature = UIEventSource.ListStabilized(this.osmConnection.userDetails.map(userDetails => { | ||||
|         const feature = Stores.ListStabilized(this.osmConnection.userDetails.map(userDetails => { | ||||
| 
 | ||||
|             if (userDetails === undefined) { | ||||
|                 return undefined; | ||||
|  | @ -328,7 +329,7 @@ export default class MapState extends UserRelatedState { | |||
| 
 | ||||
|         const flayer = this.filteredLayers.data.filter(l => l.layerDef.id === "home_location")[0] | ||||
|         if (flayer !== undefined) { | ||||
|             this.homeLocation = new SimpleFeatureSource(flayer, Tiles.tile_index(0, 0, 0), feature) | ||||
|             this.homeLocation = new TiledStaticFeatureSource(feature, flayer) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
|  | @ -336,7 +337,7 @@ export default class MapState extends UserRelatedState { | |||
|     private getPref(key: string, layer: LayerConfig): UIEventSource<boolean> { | ||||
|       const pref = this.osmConnection | ||||
|             .GetPreference(key) | ||||
|             .map(v => { | ||||
|             .sync(v => { | ||||
|                 if(v === undefined){ | ||||
|                     return undefined | ||||
|                 } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {OsmConnection} from "../Osm/OsmConnection"; | ||||
| import {MangroveIdentity} from "../Web/MangroveReviews"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Store, UIEventSource} from "../UIEventSource"; | ||||
| import {QueryParameters} from "../Web/QueryParameters"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
|  | @ -37,7 +37,7 @@ export default class UserRelatedState extends ElementsState { | |||
|      */ | ||||
|     public favouriteLayers: UIEventSource<string[]>; | ||||
| 
 | ||||
|     public readonly isTranslator : UIEventSource<boolean>; | ||||
|     public readonly isTranslator : Store<boolean>; | ||||
|      | ||||
|     constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) { | ||||
|         super(layoutToUse); | ||||
|  | @ -53,7 +53,7 @@ export default class UserRelatedState extends ElementsState { | |||
|             osmConfiguration: <'osm' | 'osm-test'>this.featureSwitchApiURL.data, | ||||
|             attemptLogin: options?.attemptLogin | ||||
|         }) | ||||
|         const translationMode  = this.osmConnection.GetPreference("translation-mode").map(str => str === undefined ? undefined : str === "true", [], b => b === undefined ? undefined : b+"") | ||||
|         const translationMode  = this.osmConnection.GetPreference("translation-mode").sync(str => str === undefined ? undefined : str === "true", [], b => b === undefined ? undefined : b+"") | ||||
|              | ||||
|         translationMode.syncWith(Locale.showLinkToWeblate) | ||||
|          | ||||
|  | @ -108,7 +108,7 @@ export default class UserRelatedState extends ElementsState { | |||
|         // Important: the favourite layers are initialized _after_ the installed themes, as these might contain an installedTheme
 | ||||
|         this.favouriteLayers = LocalStorageSource.Get("favouriteLayers") | ||||
|             .syncWith(this.osmConnection.GetLongPreference("favouriteLayers")) | ||||
|             .map( | ||||
|             .sync( | ||||
|                 (str) => Utils.Dedup(str?.split(";")) ?? [], | ||||
|                 [], | ||||
|                 (layers) => Utils.Dedup(layers)?.join(";") | ||||
|  |  | |||
|  | @ -1,64 +1,10 @@ | |||
| import {Utils} from "../Utils"; | ||||
| 
 | ||||
| export class UIEventSource<T> { | ||||
| 
 | ||||
|     private static allSources: UIEventSource<any>[] = UIEventSource.PrepPerf(); | ||||
|     public data: T; | ||||
|     public trace: boolean; | ||||
|     private readonly tag: string; | ||||
|     private _callbacks: ((t: T) => (boolean | void | any)) [] = []; | ||||
| 
 | ||||
|     constructor(data: T, tag: string = "") { | ||||
|         this.tag = tag; | ||||
|         this.data = data; | ||||
|         if (tag === undefined || tag === "") { | ||||
|             const callstack = new Error().stack.split("\n") | ||||
|             this.tag = callstack[1] | ||||
|         } | ||||
|         UIEventSource.allSources.push(this); | ||||
|     } | ||||
| 
 | ||||
|     static PrepPerf(): UIEventSource<any>[] { | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return []; | ||||
|         } | ||||
|         // @ts-ignore
 | ||||
|         window.mapcomplete_performance = () => { | ||||
|             console.log(UIEventSource.allSources.length, "uieventsources created"); | ||||
|             const copy = [...UIEventSource.allSources]; | ||||
|             copy.sort((a, b) => b._callbacks.length - a._callbacks.length); | ||||
|             console.log("Topten is:") | ||||
|             for (let i = 0; i < 10; i++) { | ||||
|                 console.log(copy[i].tag, copy[i]); | ||||
|             } | ||||
|             return UIEventSource.allSources; | ||||
|         } | ||||
|         return []; | ||||
|     } | ||||
| 
 | ||||
|     public static flatten<X>(source: UIEventSource<UIEventSource<X>>, possibleSources?: UIEventSource<any>[]): UIEventSource<X> { | ||||
|         const sink = new UIEventSource<X>(source.data?.data); | ||||
| 
 | ||||
|         source.addCallback((latestData) => { | ||||
|             sink.setData(latestData?.data); | ||||
|             latestData.addCallback(data => { | ||||
|                 if (source.data !== latestData) { | ||||
|                     return true; | ||||
|                 } | ||||
|                 sink.setData(data) | ||||
|             }) | ||||
|         }); | ||||
| 
 | ||||
|         for (const possibleSource of possibleSources ?? []) { | ||||
|             possibleSource?.addCallback(() => { | ||||
|                 sink.setData(source.data?.data); | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         return sink; | ||||
|     } | ||||
| 
 | ||||
|     public static Chronic(millis: number, asLong: () => boolean = undefined): UIEventSource<Date> { | ||||
| /** | ||||
|  * Various static utils | ||||
|  */ | ||||
| export class Stores { | ||||
|     public static Chronic(millis: number, asLong: () => boolean = undefined): Store<Date> { | ||||
|         const source = new UIEventSource<Date>(undefined); | ||||
| 
 | ||||
|         function run() { | ||||
|  | @ -72,17 +18,8 @@ export class UIEventSource<T> { | |||
|         return source; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated. | ||||
|      * If the promise fails, the value will stay undefined | ||||
|      * @param promise | ||||
|      * @constructor | ||||
|      */ | ||||
|     public static FromPromise<T>(promise: Promise<T>): UIEventSource<T> { | ||||
|         const src = new UIEventSource<T>(undefined) | ||||
|         promise?.then(d => src.setData(d)) | ||||
|         promise?.catch(err => console.warn("Promise failed:", err)) | ||||
|         return src | ||||
|     public static FromPromiseWithErr<T>(promise: Promise<T>): Store<{ success: T } | { error: any }>{ | ||||
|         return UIEventSource.FromPromiseWithErr(promise); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -91,13 +28,17 @@ export class UIEventSource<T> { | |||
|      * @param promise | ||||
|      * @constructor | ||||
|      */ | ||||
|     public static FromPromiseWithErr<T>(promise: Promise<T>): UIEventSource<{ success: T } | { error: any }> { | ||||
|         const src = new UIEventSource<{ success: T } | { error: any }>(undefined) | ||||
|         promise?.then(d => src.setData({success: d})) | ||||
|         promise?.catch(err => src.setData({error: err})) | ||||
|     public static FromPromise<T>(promise: Promise<T>): Store<T> { | ||||
|         const src = new UIEventSource<T>(undefined) | ||||
|         promise?.then(d => src.setData(d)) | ||||
|         promise?.catch(err => console.warn("Promise failed:", err)) | ||||
|         return src | ||||
|     } | ||||
| 
 | ||||
|     public static flatten<X>(source: Store<Store<X>>, possibleSources?: Store<any>[]): Store<X> { | ||||
|         return UIEventSource.flatten(source, possibleSources); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given a UIEVentSource with a list, returns a new UIEventSource which is only updated if the _contents_ of the list are different. | ||||
|      * E.g. | ||||
|  | @ -112,7 +53,7 @@ export class UIEventSource<T> { | |||
|      * @param src | ||||
|      * @constructor | ||||
|      */ | ||||
|     public static ListStabilized<T>(src: UIEventSource<T[]>): UIEventSource<T[]> { | ||||
|     public static ListStabilized<T>(src: Store<T[]>): Store<T[]> { | ||||
| 
 | ||||
|         const stable = new UIEventSource<T[]>(src.data) | ||||
|         src.addCallback(list => { | ||||
|  | @ -141,46 +82,58 @@ export class UIEventSource<T> { | |||
|         }) | ||||
|         return stable | ||||
|     } | ||||
| 
 | ||||
|     public static asFloat(source: UIEventSource<string>): UIEventSource<number> { | ||||
|         return source.map( | ||||
|             (str) => { | ||||
|                 let parsed = parseFloat(str); | ||||
|                 return isNaN(parsed) ? undefined : parsed; | ||||
|             }, | ||||
|             [], | ||||
|             (fl) => { | ||||
|                 if (fl === undefined || isNaN(fl)) { | ||||
|                     return undefined; | ||||
|                 } | ||||
|                 return ("" + fl).substr(0, 8); | ||||
|             } | ||||
|         ) | ||||
| } | ||||
| 
 | ||||
|     public AsPromise(condition?: ((t: T )=> boolean)): Promise<T> { | ||||
|         const self = this; | ||||
|         condition = condition ?? (t => t !== undefined) | ||||
|         return new Promise((resolve, reject) => { | ||||
|             if (condition(self.data)) { | ||||
|                 resolve(self.data) | ||||
|             } else { | ||||
|                 self.addCallbackD(data => { | ||||
|                     resolve(data) | ||||
|                     return true; // return true to unregister as we only need to be called once
 | ||||
|                 }) | ||||
| export abstract class Store<T> { | ||||
|     abstract readonly data: T; | ||||
| 
 | ||||
|     /** | ||||
|      * OPtional value giving a title to the UIEventSource, mainly used for debugging | ||||
|      */ | ||||
|     public readonly tag: string | undefined; | ||||
|      | ||||
| 
 | ||||
|     constructor(tag: string = undefined) { | ||||
|         this.tag = tag; | ||||
|         if ((tag === undefined || tag === "")) { | ||||
|             let createStack = Utils.runningFromConsole; | ||||
|             if(!Utils.runningFromConsole) { | ||||
|                 createStack = window.location.hostname === "127.0.0.1" | ||||
|             } | ||||
|             if(createStack) { | ||||
|                 const callstack = new Error().stack.split("\n") | ||||
|                 this.tag = callstack[1] | ||||
|             } | ||||
|         } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public WaitForPromise(promise: Promise<T>, onFail: ((any) => void)): UIEventSource<T> { | ||||
|         const self = this; | ||||
|         promise?.then(d => self.setData(d)) | ||||
|         promise?.catch(err => onFail(err)) | ||||
|         return this | ||||
|     } | ||||
|     abstract map<J>(f: ((t: T) => J)): Store<J> | ||||
|     abstract map<J>(f: ((t: T) => J), extraStoresToWatch: Store<any>[]): Store<J> | ||||
| 
 | ||||
|     public withEqualityStabilized(comparator: (t: T | undefined, t1: T | undefined) => boolean): UIEventSource<T> { | ||||
|     /** | ||||
|      * Add a callback function which will run on future data changes | ||||
|      */ | ||||
|     abstract addCallback(callback: (data: T) => void); | ||||
| 
 | ||||
|     /** | ||||
|      * Adds a callback function, which will be run immediately. | ||||
|      * Only triggers if the current data is defined | ||||
|      */ | ||||
|     abstract addCallbackAndRunD(callback: (data: T) => void); | ||||
| 
 | ||||
|     /** | ||||
|      * Add a callback function which will run on future data changes | ||||
|      * Only triggers if the data is defined | ||||
|      */ | ||||
|     abstract addCallbackD(callback: (data: T) => void); | ||||
| 
 | ||||
|     /** | ||||
|      * Adds a callback function, which will be run immediately. | ||||
|      * Only triggers if the current data is defined | ||||
|      */ | ||||
|     abstract addCallbackAndRun(callback: (data: T) => void); | ||||
| 
 | ||||
|     public withEqualityStabilized(comparator: (t: T | undefined, t1: T | undefined) => boolean): Store<T> { | ||||
|         let oldValue = undefined; | ||||
|         return this.map(v => { | ||||
|             if (v == oldValue) { | ||||
|  | @ -194,6 +147,205 @@ export class UIEventSource<T> { | |||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Monadic bind function | ||||
|      */ | ||||
|     public bind<X>(f: ((t: T) => Store<X>)): Store<X> { | ||||
|         const mapped = this.map(f) | ||||
|         const sink = new UIEventSource<X>(undefined) | ||||
|         const seenEventSources = new Set<Store<X>>(); | ||||
|         mapped.addCallbackAndRun(newEventSource => { | ||||
|             if (newEventSource === null) { | ||||
|                 sink.setData(null) | ||||
|             } else if (newEventSource === undefined) { | ||||
|                 sink.setData(undefined) | ||||
|             } else if (!seenEventSources.has(newEventSource)) { | ||||
|                 seenEventSources.add(newEventSource) | ||||
|                 newEventSource.addCallbackAndRun(resultData => { | ||||
|                     if (mapped.data === newEventSource) { | ||||
|                         sink.setData(resultData); | ||||
|                     } | ||||
|                 }) | ||||
|             } else { | ||||
|                 // Already seen, so we don't have to add a callback, just update the value
 | ||||
|                 sink.setData(newEventSource.data) | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         return sink; | ||||
|     } | ||||
| 
 | ||||
|     public stabilized(millisToStabilize): Store<T> { | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         const newSource = new UIEventSource<T>(this.data); | ||||
| 
 | ||||
|         let currentCallback = 0; | ||||
|         this.addCallback(latestData => { | ||||
|             currentCallback++; | ||||
|             const thisCallback = currentCallback; | ||||
|             window.setTimeout(() => { | ||||
|                 if (thisCallback === currentCallback) { | ||||
|                     newSource.setData(latestData); | ||||
|                 } | ||||
|             }, millisToStabilize) | ||||
|         }); | ||||
| 
 | ||||
|         return newSource; | ||||
|     } | ||||
|     public AsPromise(condition?: ((t: T) => boolean)): Promise<T> { | ||||
|         const self = this; | ||||
|         condition = condition ?? (t => t !== undefined) | ||||
|         return new Promise((resolve) => { | ||||
|             if (condition(self.data)) { | ||||
|                 resolve(self.data) | ||||
|             } else { | ||||
|                 self.addCallbackD(data => { | ||||
|                     resolve(data) | ||||
|                     return true; // return true to unregister as we only need to be called once
 | ||||
|                 }) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|      | ||||
| } | ||||
| 
 | ||||
| export class ImmutableStore<T> extends Store<T> { | ||||
|     public readonly data: T; | ||||
| 
 | ||||
|     constructor(data: T) { | ||||
|         super(); | ||||
|         this.data = data; | ||||
|     } | ||||
| 
 | ||||
|     addCallback(callback: (data: T) => void) { | ||||
|         // pass: data will never change
 | ||||
|     } | ||||
| 
 | ||||
|     addCallbackAndRun(callback: (data: T) => void) { | ||||
|         callback(this.data) | ||||
|         // no callback registry: data will never change
 | ||||
|     } | ||||
| 
 | ||||
|     addCallbackAndRunD(callback: (data: T) => void) { | ||||
|         if(this.data !== undefined){ | ||||
|             callback(this.data) | ||||
|         } | ||||
|         // no callback registry: data will never change
 | ||||
|     } | ||||
| 
 | ||||
|     addCallbackD(callback: (data: T) => void) { | ||||
|         // pass: data will never change
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     map<J>(f: (t: T) => J): ImmutableStore<J> { | ||||
|         return new ImmutableStore<J>(f(this.data)); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export class UIEventSource<T> extends Store<T> { | ||||
| 
 | ||||
|     private static allSources: UIEventSource<any>[] = UIEventSource.PrepPerf(); | ||||
|     public data: T; | ||||
|     private _callbacks: ((t: T) => (boolean | void | any)) [] = []; | ||||
| 
 | ||||
|     constructor(data: T, tag: string = "") { | ||||
|         super(tag); | ||||
|         this.data = data; | ||||
|         UIEventSource.allSources.push(this); | ||||
|     } | ||||
| 
 | ||||
|     static PrepPerf(): UIEventSource<any>[] { | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return []; | ||||
|         } | ||||
|         // @ts-ignore
 | ||||
|         window.mapcomplete_performance = () => { | ||||
|             console.log(UIEventSource.allSources.length, "uieventsources created"); | ||||
|             const copy = [...UIEventSource.allSources]; | ||||
|             copy.sort((a, b) => b._callbacks.length - a._callbacks.length); | ||||
|             console.log("Topten is:") | ||||
|             for (let i = 0; i < 10; i++) { | ||||
|                 console.log(copy[i].tag, copy[i]); | ||||
|             } | ||||
|             return UIEventSource.allSources; | ||||
|         } | ||||
|         return []; | ||||
|     } | ||||
| 
 | ||||
|     public static flatten<X>(source: Store<Store<X>>, possibleSources?: Store<any>[]): UIEventSource<X> { | ||||
|         const sink = new UIEventSource<X>(source.data?.data); | ||||
| 
 | ||||
|         source.addCallback((latestData) => { | ||||
|             sink.setData(latestData?.data); | ||||
|             latestData.addCallback(data => { | ||||
|                 if (source.data !== latestData) { | ||||
|                     return true; | ||||
|                 } | ||||
|                 sink.setData(data) | ||||
|             }) | ||||
|         }); | ||||
| 
 | ||||
|         for (const possibleSource of possibleSources ?? []) { | ||||
|             possibleSource?.addCallback(() => { | ||||
|                 sink.setData(source.data?.data); | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         return sink; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated. | ||||
|      * If the promise fails, the value will stay undefined, but 'onError' will be called | ||||
|      */ | ||||
|     public static FromPromise<T>(promise: Promise<T>, onError :( (e: any) => void) = undefined): UIEventSource<T> { | ||||
|         const src = new UIEventSource<T>(undefined) | ||||
|         promise?.then(d => src.setData(d)) | ||||
|         promise?.catch(err => { | ||||
|             if(onError !== undefined){ | ||||
|                 onError(err) | ||||
|             }else{ | ||||
|                 console.warn("Promise failed:", err); | ||||
|             } | ||||
|         }) | ||||
|         return src | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated. | ||||
|      * If the promise fails, the value will stay undefined | ||||
|      * @param promise | ||||
|      * @constructor | ||||
|      */ | ||||
|     public static FromPromiseWithErr<T>(promise: Promise<T>): UIEventSource<{ success: T } | { error: any }> { | ||||
|         const src = new UIEventSource<{ success: T } | { error: any }>(undefined) | ||||
|         promise?.then(d => src.setData({success: d})) | ||||
|         promise?.catch(err => src.setData({error: err})) | ||||
|         return src | ||||
|     } | ||||
| 
 | ||||
|     public static asFloat(source: UIEventSource<string>): UIEventSource<number> { | ||||
|         return source.sync( | ||||
|             (str) => { | ||||
|                 let parsed = parseFloat(str); | ||||
|                 return isNaN(parsed) ? undefined : parsed; | ||||
|             }, | ||||
|             [], | ||||
|             (fl) => { | ||||
|                 if (fl === undefined || isNaN(fl)) { | ||||
|                     return undefined; | ||||
|                 } | ||||
|                 return ("" + fl).substr(0, 8); | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Adds a callback | ||||
|      * | ||||
|  | @ -205,9 +357,6 @@ export class UIEventSource<T> { | |||
|             // This ^^^ actually works!
 | ||||
|             throw "Don't add console.log directly as a callback - you'll won't be able to find it afterwards. Wrap it in a lambda instead." | ||||
|         } | ||||
|         if (this.trace) { | ||||
|             console.trace("Added a callback") | ||||
|         } | ||||
|         this._callbacks.push(callback); | ||||
|         return this; | ||||
|     } | ||||
|  | @ -255,44 +404,47 @@ export class UIEventSource<T> { | |||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Monadic bind function | ||||
|      * Monoidal map which results in a read-only store | ||||
|      * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' | ||||
|      * @param f: The transforming function | ||||
|      * @param extraSources: also trigger the update if one of these sources change | ||||
|      */ | ||||
|     public bind<X>(f: ((t: T) => UIEventSource<X>)): UIEventSource<X> { | ||||
|         const mapped = this.map(f) | ||||
|         const sink = new UIEventSource<X>(undefined) | ||||
|         const seenEventSources = new Set<UIEventSource<X>>(); | ||||
|         mapped.addCallbackAndRun(newEventSource => { | ||||
|             if (newEventSource === null) { | ||||
|                 sink.setData(null) | ||||
|             } else if (newEventSource === undefined) { | ||||
|                 sink.setData(undefined) | ||||
|             } else if (!seenEventSources.has(newEventSource)) { | ||||
|                 seenEventSources.add(newEventSource) | ||||
|                 newEventSource.addCallbackAndRun(resultData => { | ||||
|                     if (mapped.data === newEventSource) { | ||||
|                         sink.setData(resultData); | ||||
|                     } | ||||
|                 }) | ||||
|             } else { | ||||
|                 // Already seen, so we don't have to add a callback, just update the value
 | ||||
|                 sink.setData(newEventSource.data) | ||||
|             } | ||||
|         }) | ||||
|     public map<J>(f: ((t: T) => J), | ||||
|                    extraSources: Store<any>[] = []): Store<J> { | ||||
|         const self = this; | ||||
| 
 | ||||
|         return sink; | ||||
|         const stack = new Error().stack.split("\n"); | ||||
|         const callee = stack[1] | ||||
| 
 | ||||
|         const newSource = new UIEventSource<J>( | ||||
|             f(this.data), | ||||
|             "map(" + this.tag + ")@" + callee | ||||
|         ); | ||||
| 
 | ||||
|         const update = function () { | ||||
|             newSource.setData(f(self.data)); | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         this.addCallback(update); | ||||
|         for (const extraSource of extraSources) { | ||||
|             extraSource?.addCallback(update); | ||||
|         } | ||||
| 
 | ||||
|         return newSource; | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * Monoidal map: | ||||
|      * Two way sync with functions in both directions | ||||
|      * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' | ||||
|      * @param f: The transforming function | ||||
|      * @param extraSources: also trigger the update if one of these sources change | ||||
|      * @param g: a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData | ||||
|      * @param allowUnregister: if set, the update will be halted if no listeners are registered | ||||
|      */ | ||||
|     public map<J>(f: ((t: T) => J), | ||||
|                   extraSources: UIEventSource<any>[] = [], | ||||
|                   g: ((j: J, t: T) => T) = undefined, | ||||
|     public sync<J>(f: ((t: T) => J), | ||||
|                   extraSources: Store<any>[], | ||||
|                   g: ((j: J, t: T) => T) , | ||||
|                   allowUnregister = false): UIEventSource<J> { | ||||
|         const self = this; | ||||
| 
 | ||||
|  | @ -339,27 +491,6 @@ export class UIEventSource<T> { | |||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     public stabilized(millisToStabilize): UIEventSource<T> { | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         const newSource = new UIEventSource<T>(this.data); | ||||
| 
 | ||||
|         let currentCallback = 0; | ||||
|         this.addCallback(latestData => { | ||||
|             currentCallback++; | ||||
|             const thisCallback = currentCallback; | ||||
|             window.setTimeout(() => { | ||||
|                 if (thisCallback === currentCallback) { | ||||
|                     newSource.setData(latestData); | ||||
|                 } | ||||
|             }, millisToStabilize) | ||||
|         }); | ||||
| 
 | ||||
|         return newSource; | ||||
|     } | ||||
| 
 | ||||
|     addCallbackAndRunD(callback: (data: T) => void) { | ||||
|         this.addCallbackAndRun(data => { | ||||
|             if (data !== undefined && data !== null) { | ||||
|  | @ -375,4 +506,5 @@ export class UIEventSource<T> { | |||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -6,7 +6,7 @@ import {UIEventSource} from "../UIEventSource"; | |||
| export class LocalStorageSource { | ||||
| 
 | ||||
|     static GetParsed<T>(key: string, defaultValue: T): UIEventSource<T> { | ||||
|         return LocalStorageSource.Get(key).map( | ||||
|         return LocalStorageSource.Get(key).sync( | ||||
|             str => { | ||||
|                 if (str === undefined) { | ||||
|                     return defaultValue | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ export class QueryParameters { | |||
|     } | ||||
| 
 | ||||
|     public static GetBooleanQueryParameter(key: string, deflt: boolean, documentation?: string): UIEventSource<boolean> { | ||||
|         return QueryParameters.GetQueryParameter(key, ""+ deflt, documentation).map(str => str === "true", [], b => "" + b) | ||||
|         return QueryParameters.GetQueryParameter(key, ""+ deflt, documentation).sync(str => str === "true", [], b => "" + b) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Store} from "../UIEventSource"; | ||||
| 
 | ||||
| export interface Review { | ||||
|     comment?: string, | ||||
|  | @ -9,5 +9,5 @@ export interface Review { | |||
|     /** | ||||
|      * True if the current logged in user is the creator of this comment | ||||
|      */ | ||||
|     made_by_user: UIEventSource<boolean> | ||||
|     made_by_user: Store<boolean> | ||||
| } | ||||
|  | @ -1,7 +1,7 @@ | |||
| import {Translation} from "../UI/i18n/Translation"; | ||||
| import {ApplicableUnitJson} from "./ThemeConfig/Json/UnitConfigJson"; | ||||
| import Translations from "../UI/i18n/Translations"; | ||||
| import {UIEventSource} from "../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../Logic/UIEventSource"; | ||||
| import BaseUIElement from "../UI/BaseUIElement"; | ||||
| import Toggle from "../UI/Input/Toggle"; | ||||
| 
 | ||||
|  | @ -49,7 +49,7 @@ export class Denomination { | |||
|         return (this._humanSingular ?? this._human).Clone() | ||||
|     } | ||||
| 
 | ||||
|     getToggledHuman(isSingular: UIEventSource<boolean>): BaseUIElement { | ||||
|     getToggledHuman(isSingular: Store<boolean>): BaseUIElement { | ||||
|         if (this._humanSingular === undefined) { | ||||
|             return this.human | ||||
|         } | ||||
|  |  | |||
|  | @ -129,7 +129,7 @@ export default class FilterConfig { | |||
|             })) | ||||
| 
 | ||||
|             // We map the query parameter for this case
 | ||||
|             return qp.map(str => { | ||||
|             return qp.sync(str => { | ||||
|                 const parsed = Number(str) | ||||
|                 if (isNaN(parsed)) { | ||||
|                     // Nope, not a correct number!
 | ||||
|  | @ -143,7 +143,7 @@ export default class FilterConfig { | |||
|         const option = this.options[0] | ||||
| 
 | ||||
|         if (option.fields.length > 0) { | ||||
|             return qp.map(str => { | ||||
|             return qp.sync(str => { | ||||
|                 // There are variables in play!
 | ||||
|                 // str should encode a json-hash
 | ||||
|                 try { | ||||
|  | @ -178,7 +178,7 @@ export default class FilterConfig { | |||
|             currentFilter: option.osmTags, | ||||
|             state: "true" | ||||
|         } | ||||
|         return qp.map( | ||||
|         return qp.sync( | ||||
|             str => { | ||||
|                 // Only a single option exists here
 | ||||
|                 if (str === "true") { | ||||
|  |  | |||
|  | @ -65,7 +65,6 @@ class AutomationPanel extends Combine { | |||
|             console.warn("Triggered map on nextTileToHandle", tileIndex) | ||||
|             const start = new Date() | ||||
|             return AutomationPanel.TileHandler(layoutToUse, tileIndex, layerId, tagRenderingToAutomate.tagRendering, extraCommentText, | ||||
|                 openChangeset, | ||||
|                 (result, logMessage) => { | ||||
|                     const end = new Date() | ||||
|                     const timeNeeded = (end.getTime() - start.getTime()) / 1000; | ||||
|  | @ -118,7 +117,6 @@ class AutomationPanel extends Combine { | |||
|     } | ||||
| 
 | ||||
|     private static TileHandler(layoutToUse: LayoutConfig, tileIndex: number, targetLayer: string, targetAction: TagRenderingConfig, extraCommentText: UIEventSource<string>, | ||||
|                                openChangeset: UIEventSource<number>, | ||||
|                                whenDone: ((result: string, logMessage?: string) => void)): BaseUIElement { | ||||
| 
 | ||||
|         const state = new MapState(layoutToUse, {attemptLogin: false}) | ||||
|  | @ -204,7 +202,7 @@ class AutomationPanel extends Combine { | |||
|                     whenDone("no-action", "Inspected " + inspected + " elements: " + log.join("; ")) | ||||
|                 } else { | ||||
|                     state.osmConnection.AttemptLogin() | ||||
|                     state.changes.flushChanges("handled tile automatically, time to flush!", openChangeset) | ||||
|                     state.changes.flushChanges("handled tile automatically, time to flush!") | ||||
|                     whenDone("fixed", "Updated " + handled + " elements, inspected " + inspected + ": " + log.join("; ")) | ||||
|                 } | ||||
|                 return true; | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {VariableUiElement} from "./VariableUIElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Stores, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Loading from "./Loading"; | ||||
| 
 | ||||
| export default class AsyncLazy extends BaseUIElement { | ||||
|  | @ -15,7 +15,7 @@ export default class AsyncLazy extends BaseUIElement { | |||
|         // The caching of the BaseUIElement will guarantee that _f will only be called once
 | ||||
| 
 | ||||
|         return new VariableUiElement( | ||||
|             UIEventSource.FromPromise(this._f()).map(el => { | ||||
|             Stores.FromPromise(this._f()).map(el => { | ||||
|                 if (el === undefined) { | ||||
|                     return new Loading() | ||||
|                 } | ||||
|  |  | |||
|  | @ -1,14 +1,14 @@ | |||
| import Translations from "../i18n/Translations"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| 
 | ||||
| 
 | ||||
| export default class Link extends BaseUIElement { | ||||
|     private readonly _href: string | UIEventSource<string>; | ||||
|     private readonly _href: string | Store<string>; | ||||
|     private readonly _embeddedShow: BaseUIElement; | ||||
|     private readonly _newTab: boolean; | ||||
| 
 | ||||
|     constructor(embeddedShow: BaseUIElement | string, href: string | UIEventSource<string>, newTab: boolean = false) { | ||||
|     constructor(embeddedShow: BaseUIElement | string, href: string | Store<string>, newTab: boolean = false) { | ||||
|         super(); | ||||
|         this._embeddedShow = Translations.W(embeddedShow); | ||||
|         this._href = href; | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import Combine from "./Combine"; | |||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import Link from "./Link"; | ||||
| import Img from "./Img"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {UIElement} from "../UIElement"; | ||||
| import {VariableUiElement} from "./VariableUIElement"; | ||||
| import Lazy from "./Lazy"; | ||||
|  | @ -13,11 +13,11 @@ import Loading from "./Loading"; | |||
| export class SubtleButton extends UIElement { | ||||
|     private readonly imageUrl: string | BaseUIElement; | ||||
|     private readonly message: string | BaseUIElement; | ||||
|     private readonly options: { url?: string | UIEventSource<string>; newTab?: boolean ; imgSize?: string}; | ||||
|     private readonly options: { url?: string | Store<string>; newTab?: boolean ; imgSize?: string}; | ||||
| 
 | ||||
| 
 | ||||
|     constructor(imageUrl: string | BaseUIElement, message: string | BaseUIElement, options: {  | ||||
|         url?: string | UIEventSource<string>,  | ||||
|         url?: string | Store<string>,  | ||||
|         newTab?: boolean, | ||||
|         imgSize?: "h-11 w-11" | string | ||||
|     } = undefined) { | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store} from "../../Logic/UIEventSource"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import Combine from "./Combine"; | ||||
| 
 | ||||
| export class VariableUiElement extends BaseUIElement { | ||||
|     private readonly _contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>; | ||||
|     private readonly _contents: Store<string | BaseUIElement | BaseUIElement[]>; | ||||
| 
 | ||||
|     constructor(contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>) { | ||||
|     constructor(contents: Store<string | BaseUIElement | BaseUIElement[]>) { | ||||
|         super(); | ||||
|         this._contents = contents; | ||||
|     } | ||||
|  |  | |||
|  | @ -2,12 +2,12 @@ import {Utils} from "../../Utils"; | |||
| import {FixedInputElement} from "../Input/FixedInputElement"; | ||||
| import {RadioButton} from "../Input/RadioButton"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import Toggle, {ClickableToggle} from "../Input/Toggle"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import {Translation} from "../i18n/Translation"; | ||||
| import Svg from "../../Svg"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import State from "../../State"; | ||||
| import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; | ||||
|  | @ -20,7 +20,6 @@ import {QueryParameters} from "../../Logic/Web/QueryParameters"; | |||
| import {TagUtils} from "../../Logic/Tags/TagUtils"; | ||||
| import {InputElement} from "../Input/InputElement"; | ||||
| import {DropDown} from "../Input/DropDown"; | ||||
| import {UIElement} from "../UIElement"; | ||||
| 
 | ||||
| export default class FilterView extends VariableUiElement { | ||||
|     constructor(filteredLayer: UIEventSource<FilteredLayer[]>,  | ||||
|  | @ -180,7 +179,8 @@ export default class FilterView extends VariableUiElement { | |||
| 
 | ||||
|         const filter = filterConfig.options[0] | ||||
|         const mappings = new Map<string, BaseUIElement>() | ||||
|         let allValid = new UIEventSource(true) | ||||
|         let allValid: Store<boolean> = new ImmutableStore(true) | ||||
|         var allFields: InputElement<string>[] = [] | ||||
|         const properties = new UIEventSource<any>({}) | ||||
|         for (const {name, type} of filter.fields) { | ||||
|             const value = QueryParameters.GetQueryParameter("filter-" + filterConfig.id + "-" + name, "", "Value for filter " + filterConfig.id) | ||||
|  | @ -193,10 +193,11 @@ export default class FilterView extends VariableUiElement { | |||
|                 properties.data[name] = v.toLowerCase(); | ||||
|                 properties.ping() | ||||
|             }) | ||||
|             allFields.push(field) | ||||
|             allValid = allValid.map(previous => previous && field.IsValid(stable.data) && stable.data !== "", [stable]) | ||||
|         } | ||||
|         const tr = new SubstitutedTranslation(filter.question, new UIEventSource<any>({id: filterConfig.id}), State.state, mappings) | ||||
|         const trigger: UIEventSource<FilterState> = allValid.map(isValid => { | ||||
|         const trigger: Store<FilterState> = allValid.map(isValid => { | ||||
|             if (!isValid) { | ||||
|                 return undefined | ||||
|             } | ||||
|  | @ -222,7 +223,15 @@ export default class FilterView extends VariableUiElement { | |||
|             } | ||||
|         }, [properties]) | ||||
|          | ||||
|         return [tr, trigger]; | ||||
|         const settableFilter = new UIEventSource<FilterState>(undefined) | ||||
|         trigger.addCallbackAndRun(state => settableFilter.setData(state)) | ||||
|         settableFilter.addCallback(state => { | ||||
|             if(state.currentFilter === undefined){ | ||||
|                 allFields.forEach(f => f.GetValue().setData(undefined)); | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         return [tr, settableFilter]; | ||||
|     } | ||||
| 
 | ||||
|     private static createCheckboxFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] { | ||||
|  | @ -231,14 +240,14 @@ export default class FilterView extends VariableUiElement { | |||
|         const icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6"); | ||||
|         const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2 w-6"); | ||||
| 
 | ||||
|         const toggle = new Toggle( | ||||
|         const toggle = new ClickableToggle( | ||||
|             new Combine([icon, option.question.Clone().SetClass("block")]).SetClass("flex"), | ||||
|             new Combine([iconUnselected, option.question.Clone().SetClass("block")]).SetClass("flex") | ||||
|         ) | ||||
|             .ToggleOnClick() | ||||
|             .SetClass("block m-1") | ||||
| 
 | ||||
|         return [toggle, toggle.isEnabled.map(enabled => enabled ? { | ||||
|         return [toggle, toggle.isEnabled.sync(enabled => enabled ? { | ||||
|                 currentFilter: option.osmTags, | ||||
|                 state: "true" | ||||
|             } : undefined, [], | ||||
|  | @ -272,7 +281,7 @@ export default class FilterView extends VariableUiElement { | |||
|         } | ||||
| 
 | ||||
|         return [filterPicker, | ||||
|             filterPicker.GetValue().map( | ||||
|             filterPicker.GetValue().sync( | ||||
|                 i => values[i], | ||||
|                 [], | ||||
|                 selected => { | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Table from "../Base/Table"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
|  | @ -19,7 +19,7 @@ export default class Histogram<T> extends VariableUiElement { | |||
|         "#fa61fa" | ||||
|     ] | ||||
| 
 | ||||
|     constructor(values: UIEventSource<string[]>, | ||||
|     constructor(values: Store<string[]>, | ||||
|                 title: string | BaseUIElement, | ||||
|                 countTitle: string | BaseUIElement, | ||||
|                 options?: { | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import * as personal from "../../assets/themes/personal/personal.json" | |||
| import Constants from "../../Models/Constants"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {ImmutableStore, Store, Stores, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import {OsmConnection} from "../../Logic/Osm/OsmConnection"; | ||||
| import UserRelatedState from "../../Logic/State/UserRelatedState"; | ||||
|  | @ -117,7 +117,7 @@ export default class MoreScreen extends Combine { | |||
|     private static createUrlFor(layout: { id: string, definition?: string }, | ||||
|                                 isCustom: boolean, | ||||
|                                 state?: { locationControl?: UIEventSource<{ lat, lon, zoom }>, layoutToUse?: { id } } | ||||
|     ): UIEventSource<string> { | ||||
|     ): Store<string> { | ||||
|         if (layout === undefined) { | ||||
|             return undefined; | ||||
|         } | ||||
|  | @ -163,7 +163,7 @@ export default class MoreScreen extends Combine { | |||
|                 .map(part => part[0] + "=" + part[1]) | ||||
|                 .join("&") | ||||
|             return `${linkPrefix}${params}${hash}`; | ||||
|         }) ?? new UIEventSource<string>(`${linkPrefix}`) | ||||
|         }) ?? new ImmutableStore<string>(`${linkPrefix}`) | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
|  | @ -237,7 +237,7 @@ export default class MoreScreen extends Combine { | |||
|     private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState, themeListClasses: string, search: UIEventSource<string>): BaseUIElement { | ||||
|         const prefix = "mapcomplete-unofficial-theme-"; | ||||
| 
 | ||||
|         var currentIds: UIEventSource<string[]> = state.osmConnection.preferencesHandler.preferences | ||||
|         var currentIds: Store<string[]> = state.osmConnection.preferencesHandler.preferences | ||||
|             .map(allPreferences => { | ||||
|                 const ids: string[] = [] | ||||
| 
 | ||||
|  | @ -250,7 +250,7 @@ export default class MoreScreen extends Combine { | |||
|                 return ids | ||||
|             }); | ||||
| 
 | ||||
|         var stableIds = UIEventSource.ListStabilized<string>(currentIds) | ||||
|         var stableIds = Stores.ListStabilized<string>(currentIds) | ||||
|         return new VariableUiElement( | ||||
|             stableIds.map(ids => { | ||||
|                 const allThemes: { element: BaseUIElement, predicate?: (s: string) => boolean }[] = [] | ||||
|  |  | |||
|  | @ -2,9 +2,8 @@ import {VariableUiElement} from "../Base/VariableUIElement"; | |||
| import {Translation} from "../i18n/Translation"; | ||||
| import Svg from "../../Svg"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
|  | @ -13,7 +12,7 @@ import Loc from "../../Models/Loc"; | |||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import {InputElement} from "../Input/InputElement"; | ||||
| import CheckBoxes, {CheckBox} from "../Input/Checkboxes"; | ||||
| import {CheckBox} from "../Input/Checkboxes"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
| import LZString from "lz-string"; | ||||
| 
 | ||||
|  | @ -24,7 +23,7 @@ export default class ShareScreen extends Combine { | |||
|         const tr = Translations.t.general.sharescreen; | ||||
| 
 | ||||
|         const optionCheckboxes: InputElement<boolean>[] = [] | ||||
|         const optionParts: (UIEventSource<string>)[] = []; | ||||
|         const optionParts: (Store<string>)[] = []; | ||||
| 
 | ||||
|         const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true) | ||||
|         optionCheckboxes.push(includeLocation); | ||||
|  |  | |||
|  | @ -124,7 +124,7 @@ export default class TranslatorsPanel extends Toggle { | |||
|         const completeness = new Map<string, number>() | ||||
|         const untranslated = new Map<string, string[]>() | ||||
| 
 | ||||
|         Utils.WalkObject(layout, (o, path) => { | ||||
|         Utils.WalkObject(layout, (o) => { | ||||
|             const translation = <Translation><any>o; | ||||
|             if (translation.translations["*"] !== undefined) { | ||||
|                 return | ||||
|  |  | |||
|  | @ -2,13 +2,13 @@ import Combine from "../Base/Combine"; | |||
| import Translations from "../i18n/Translations"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {LicenseInfo} from "../../Logic/ImageProviders/LicenseInfo"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| 
 | ||||
| export default class Attribution extends VariableUiElement { | ||||
| 
 | ||||
|     constructor(license: UIEventSource<LicenseInfo>, icon: BaseUIElement, date?: Date) { | ||||
|     constructor(license: Store<LicenseInfo>, icon: BaseUIElement, date?: Date) { | ||||
|         if (license === undefined) { | ||||
|             throw "No license source given in the attribution element" | ||||
|         } | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store} from "../../Logic/UIEventSource"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import Toggle, {ClickableToggle} from "../Input/Toggle"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import Svg from "../../Svg"; | ||||
| import {Tag} from "../../Logic/Tags/Tag"; | ||||
|  | @ -11,7 +11,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | |||
| 
 | ||||
| export default class DeleteImage extends Toggle { | ||||
| 
 | ||||
|     constructor(key: string, tags: UIEventSource<any>, state: { layoutToUse: LayoutConfig, changes?: Changes, osmConnection?: OsmConnection }) { | ||||
|     constructor(key: string, tags: Store<any>, state: { layoutToUse: LayoutConfig, changes?: Changes, osmConnection?: OsmConnection }) { | ||||
|         const oldValue = tags.data[key] | ||||
|         const isDeletedBadge = Translations.t.image.isDeleted.Clone() | ||||
|             .SetClass("rounded-full p-1") | ||||
|  | @ -37,7 +37,7 @@ export default class DeleteImage extends Toggle { | |||
| 
 | ||||
|         const cancelButton = Translations.t.general.cancel.Clone().SetClass("bg-white pl-4 pr-4").SetStyle("border-bottom-left-radius:30rem; border-bottom-right-radius: 30rem;"); | ||||
|         const openDelete = Svg.delete_icon_svg().SetStyle("width: 2em; height: 2em; display:block;") | ||||
|         const deleteDialog = new Toggle( | ||||
|         const deleteDialog = new ClickableToggle( | ||||
|             new Combine([ | ||||
|                 deleteButton, | ||||
|                 cancelButton | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import {SlideShow} from "./SlideShow"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store} from "../../Logic/UIEventSource"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import DeleteImage from "./DeleteImage"; | ||||
| import {AttributedImage} from "./AttributedImage"; | ||||
|  | @ -12,8 +12,8 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | |||
| 
 | ||||
| export class ImageCarousel extends Toggle { | ||||
| 
 | ||||
|     constructor(images: UIEventSource<{ key: string, url: string, provider: ImageProvider }[]>, | ||||
|                 tags: UIEventSource<any>, | ||||
|     constructor(images: Store<{ key: string, url: string, provider: ImageProvider }[]>, | ||||
|                 tags: Store<any>, | ||||
|                 state: { osmConnection?: OsmConnection, changes?: Changes, layoutToUse: LayoutConfig }) { | ||||
|         const uiElements = images.map((imageURLS: { key: string, url: string, provider: ImageProvider }[]) => { | ||||
|             const uiElements: BaseUIElement[] = []; | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import Svg from "../../Svg"; | ||||
|  | @ -22,12 +22,12 @@ export class ImageUploadFlow extends Toggle { | |||
| 
 | ||||
|     private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>() | ||||
| 
 | ||||
|     constructor(tagsSource: UIEventSource<any>, | ||||
|     constructor(tagsSource: Store<any>, | ||||
|                 state: { | ||||
|                     osmConnection: OsmConnection; | ||||
|                     layoutToUse: LayoutConfig; | ||||
|                     changes: Changes, | ||||
|                     featureSwitchUserbadge: UIEventSource<boolean>; | ||||
|                     featureSwitchUserbadge: Store<boolean>; | ||||
|                 }, | ||||
|                 imagePrefix: string = "image", text: string = undefined) { | ||||
|         const perId = ImageUploadFlow.uploadCountsPerId | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import Combine from "../Base/Combine"; | ||||
|  | @ -6,9 +6,9 @@ import Combine from "../Base/Combine"; | |||
| export class SlideShow extends BaseUIElement { | ||||
| 
 | ||||
| 
 | ||||
|     private readonly embeddedElements: UIEventSource<BaseUIElement[]>; | ||||
|     private readonly embeddedElements: Store<BaseUIElement[]>; | ||||
| 
 | ||||
|     constructor(embeddedElements: UIEventSource<BaseUIElement[]>) { | ||||
|     constructor(embeddedElements: Store<BaseUIElement[]>) { | ||||
|         super() | ||||
|         this.embeddedElements = embeddedElements; | ||||
|         this.SetStyle("scroll-snap-type: x mandatory; overflow-x: auto") | ||||
|  |  | |||
|  | @ -1,12 +1,11 @@ | |||
| import Combine from "../Base/Combine"; | ||||
| import {FlowStep} from "./FlowStep"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store} from "../../Logic/UIEventSource"; | ||||
| import ValidatedTextField from "../Input/ValidatedTextField"; | ||||
| import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource"; | ||||
| import Title from "../Base/Title"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
| import Svg from "../../Svg"; | ||||
| import {Utils} from "../../Utils"; | ||||
|  | @ -19,14 +18,14 @@ export class AskMetadata extends Combine implements FlowStep<{ | |||
|     theme: string | ||||
| }> { | ||||
| 
 | ||||
|     public readonly Value: UIEventSource<{ | ||||
|     public readonly Value: Store<{ | ||||
|         features: any[], | ||||
|         wikilink: string, | ||||
|         intro: string, | ||||
|         source: string, | ||||
|         theme: string | ||||
|     }>; | ||||
|     public readonly IsValid: UIEventSource<boolean>; | ||||
|     public readonly IsValid: Store<boolean>; | ||||
| 
 | ||||
|     constructor(params: ({ features: any[], theme: string })) { | ||||
|         const t = Translations.t.importHelper.askMetadata | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import Combine from "../Base/Combine"; | |||
| import {FlowStep} from "./FlowStep"; | ||||
| import {BBox} from "../../Logic/BBox"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import CreateNoteImportLayer from "../../Models/ThemeConfig/Conversion/CreateNoteImportLayer"; | ||||
| import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; | ||||
| import GeoJsonSource from "../../Logic/FeatureSource/Sources/GeoJsonSource"; | ||||
|  | @ -17,7 +17,6 @@ import * as import_candidate from "../../assets/layers/import_candidate/import_c | |||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
| import Title from "../Base/Title"; | ||||
| import Loading from "../Base/Loading"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import * as known_layers from "../../assets/generated/known_layers.json" | ||||
| import {LayerConfigJson} from "../../Models/ThemeConfig/Json/LayerConfigJson"; | ||||
|  | @ -28,8 +27,8 @@ import Translations from "../i18n/Translations"; | |||
|  */ | ||||
| export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, features: any[], theme: string }> { | ||||
| 
 | ||||
|     public IsValid: UIEventSource<boolean> | ||||
|     public Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, features: any[], theme: string }> | ||||
|     public IsValid: Store<boolean> | ||||
|     public Value: Store<{ bbox: BBox, layer: LayerConfig, features: any[], theme: string }> | ||||
| 
 | ||||
| 
 | ||||
|     constructor(state, params: { bbox: BBox, layer: LayerConfig, features: any[], theme: string }) { | ||||
|  | @ -94,7 +93,7 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{ | |||
|             state, | ||||
|             zoomToFeatures: true, | ||||
|             leafletMap: comparisonMap.leafletMap, | ||||
|             features: new StaticFeatureSource(partitionedImportPoints.map(p => p.hasNearby), false), | ||||
|             features: new StaticFeatureSource(partitionedImportPoints.map(p => p.hasNearby)), | ||||
|             popup: (tags, layer) => new FeatureInfoBox(tags, layer, state) | ||||
|         }) | ||||
| 
 | ||||
|  | @ -103,7 +102,8 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{ | |||
|             new VariableUiElement( | ||||
|                 alreadyOpenImportNotes.features.map(notesWithImport => { | ||||
|                     if (allNotesWithinBbox.state.data !== undefined && allNotesWithinBbox.state.data["error"] !== undefined) { | ||||
|                         t.loadingFailed.Subs(allNotesWithinBbox.state.data) | ||||
|                         const error = allNotesWithinBbox.state.data["error"] | ||||
|                         t.loadingFailed.Subs({error}) | ||||
|                     } | ||||
|                     if (allNotesWithinBbox.features.data === undefined || allNotesWithinBbox.features.data.length === 0) { | ||||
|                         return new Loading(t.loading) | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import Combine from "../Base/Combine"; | ||||
| import {FlowStep} from "./FlowStep"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Link from "../Base/Link"; | ||||
| import CheckBoxes from "../Input/Checkboxes"; | ||||
| import Title from "../Base/Title"; | ||||
|  | @ -8,8 +8,8 @@ import Translations from "../i18n/Translations"; | |||
| 
 | ||||
| export class ConfirmProcess extends Combine implements FlowStep<{ features: any[], theme: string }> { | ||||
| 
 | ||||
|     public IsValid: UIEventSource<boolean> | ||||
|     public Value: UIEventSource<{ features: any[], theme: string }> | ||||
|     public IsValid: Store<boolean> | ||||
|     public Value: Store<{ features: any[], theme: string }> | ||||
| 
 | ||||
|     constructor(v: { features: any[], theme: string }) { | ||||
|         const t = Translations.t.importHelper.confirmProcess; | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | |||
| import Combine from "../Base/Combine"; | ||||
| import Title from "../Base/Title"; | ||||
| import {Overpass} from "../../Logic/Osm/Overpass"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import RelationsTracker from "../../Logic/Osm/RelationsTracker"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
|  | @ -35,7 +35,7 @@ import Translations from "../i18n/Translations"; | |||
| export default class ConflationChecker extends Combine implements FlowStep<{ features: any[], theme: string }> { | ||||
| 
 | ||||
|     public readonly IsValid | ||||
|     public readonly Value: UIEventSource<{ features: any[], theme: string }> | ||||
|     public readonly Value: Store<{ features: any[], theme: string }> | ||||
| 
 | ||||
|     constructor( | ||||
|         state, | ||||
|  | @ -89,7 +89,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | |||
|         }); | ||||
| 
 | ||||
| 
 | ||||
|         const geojson: UIEventSource<any> = fromLocalStorage.map(d => { | ||||
|         const geojson: Store<any> = fromLocalStorage.map(d => { | ||||
|             if (d === undefined) { | ||||
|                 return undefined | ||||
|             } | ||||
|  | @ -120,7 +120,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | |||
|             } | ||||
|             const bounds = osmLiveData.bounds.data | ||||
|             return geojson.features.filter(f => BBox.get(f).overlapsWith(bounds)) | ||||
|         }, [osmLiveData.bounds, zoomLevel.GetValue()]), false); | ||||
|         }, [osmLiveData.bounds, zoomLevel.GetValue()])); | ||||
| 
 | ||||
| 
 | ||||
|         new ShowDataLayer({ | ||||
|  | @ -129,9 +129,9 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | |||
|             leafletMap: osmLiveData.leafletMap, | ||||
|             popup: undefined, | ||||
|             zoomToFeatures: true, | ||||
|             features: new StaticFeatureSource([ | ||||
|             features: StaticFeatureSource.fromGeojson([ | ||||
|                 bbox.asGeoJson({}) | ||||
|             ], false) | ||||
|             ]) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -150,7 +150,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | |||
|             leafletMap: osmLiveData.leafletMap, | ||||
|             popup: (tags, layer) => new FeatureInfoBox(tags, layer, state), | ||||
|             zoomToFeatures: false, | ||||
|             features: new StaticFeatureSource(toImport.features, false) | ||||
|             features: StaticFeatureSource.fromGeojson(toImport.features) | ||||
|         }) | ||||
| 
 | ||||
|         const nearbyCutoff = ValidatedTextField.ForType("pnat").ConstructInputElement() | ||||
|  | @ -172,11 +172,11 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | |||
|             return osmData.features.filter(f => | ||||
|                 toImport.features.some(imp => | ||||
|                     maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f)))) | ||||
|         }, [nearbyCutoff.GetValue().stabilized(500)]), false); | ||||
|         }, [nearbyCutoff.GetValue().stabilized(500)])); | ||||
|         const paritionedImport = ImportUtils.partitionFeaturesIfNearby(toImport, geojson, nearbyCutoff.GetValue().map(Number)); | ||||
| 
 | ||||
|         // Featuresource showing OSM-features which are nearby a toImport-feature 
 | ||||
|         const toImportWithNearby = new StaticFeatureSource(paritionedImport.map(els => els?.hasNearby ?? []), false); | ||||
|         const toImportWithNearby = new StaticFeatureSource(paritionedImport.map(els => els?.hasNearby ?? [])); | ||||
| 
 | ||||
|         new ShowDataLayer({ | ||||
|             layerToShow: layer, | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
|  | @ -10,8 +10,8 @@ import {UIElement} from "../UIElement"; | |||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| 
 | ||||
| export interface FlowStep<T> extends BaseUIElement { | ||||
|     readonly IsValid: UIEventSource<boolean> | ||||
|     readonly Value: UIEventSource<T> | ||||
|     readonly IsValid: Store<boolean> | ||||
|     readonly Value: Store<T> | ||||
| } | ||||
| 
 | ||||
| export class FlowPanelFactory<T> { | ||||
|  |  | |||
|  | @ -12,7 +12,6 @@ import ConflationChecker from "./ConflationChecker"; | |||
| import {AskMetadata} from "./AskMetadata"; | ||||
| import {ConfirmProcess} from "./ConfirmProcess"; | ||||
| import {CreateNotes} from "./CreateNotes"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import List from "../Base/List"; | ||||
| import {CompareToAlreadyExistingNotes} from "./CompareToAlreadyExistingNotes"; | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store} from "../../Logic/UIEventSource"; | ||||
| import {GeoOperations} from "../../Logic/GeoOperations"; | ||||
| 
 | ||||
| export class ImportUtils { | ||||
|     public static partitionFeaturesIfNearby(toPartitionFeatureCollection: ({ features: any[] }), compareWith: UIEventSource<{ features: any[] }>, cutoffDistanceInMeters: UIEventSource<number>): UIEventSource<{ hasNearby: any[], noNearby: any[] }> { | ||||
|     public static partitionFeaturesIfNearby(toPartitionFeatureCollection: ({ features: any[] }), compareWith: Store<{ features: any[] }>, cutoffDistanceInMeters: Store<number>): Store<{ hasNearby: any[], noNearby: any[] }> { | ||||
|         return compareWith.map(osmData => { | ||||
|             if (osmData?.features === undefined) { | ||||
|                 return undefined | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ import BaseUIElement from "../BaseUIElement"; | |||
| import ValidatedTextField from "../Input/ValidatedTextField"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
| import Svg from "../../Svg"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import Toggle, {ClickableToggle} from "../Input/Toggle"; | ||||
| import Table from "../Base/Table"; | ||||
| import LeftIndex from "../Base/LeftIndex"; | ||||
| import Toggleable, {Accordeon} from "../Base/Toggleable"; | ||||
|  | @ -271,7 +271,7 @@ class BatchView extends Toggleable { | |||
|             const selected = new Combine([BatchView.icons[status]().SetClass("h-6 m-1"), count + " " + status]) | ||||
|                 .SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border-4 border-black animate-pulse") | ||||
| 
 | ||||
|             const toggle = new Toggle(selected, normal, filterOn.map(f => f === status, [], (selected, previous) => { | ||||
|             const toggle = new ClickableToggle(selected, normal, filterOn.sync(f => f === status, [], (selected, previous) => { | ||||
|                 if (selected) { | ||||
|                     return status; | ||||
|                 } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import Combine from "../Base/Combine"; | ||||
| import {FlowStep} from "./FlowStep"; | ||||
| import UserRelatedState from "../../Logic/State/UserRelatedState"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import Title from "../Base/Title"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
|  | @ -15,8 +15,8 @@ import MoreScreen from "../BigComponents/MoreScreen"; | |||
| import CheckBoxes from "../Input/Checkboxes"; | ||||
| 
 | ||||
| export default class LoginToImport extends Combine implements FlowStep<UserRelatedState> { | ||||
|     readonly IsValid: UIEventSource<boolean>; | ||||
|     readonly Value: UIEventSource<UserRelatedState>; | ||||
|     readonly IsValid: Store<boolean>; | ||||
|     readonly Value: Store<UserRelatedState>; | ||||
| 
 | ||||
|     private static readonly whitelist = [15015689]; | ||||
|      | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import Combine from "../Base/Combine"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {BBox} from "../../Logic/BBox"; | ||||
| import UserRelatedState from "../../Logic/State/UserRelatedState"; | ||||
| import Translations from "../i18n/Translations"; | ||||
|  | @ -27,7 +27,7 @@ import {AllTagsPanel} from "../AllTagsPanel"; | |||
| 
 | ||||
| class PreviewPanel extends ScrollableFullScreen { | ||||
| 
 | ||||
|     constructor(tags: UIEventSource<any>, layer) { | ||||
|     constructor(tags: UIEventSource<any>) { | ||||
|         super( | ||||
|             _ => new FixedUiElement("Element to import"), | ||||
|             _ => new Combine(["The tags are:", | ||||
|  | @ -43,8 +43,8 @@ class PreviewPanel extends ScrollableFullScreen { | |||
|  * Shows the data to import on a map, asks for the correct layer to be selected | ||||
|  */ | ||||
| export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, features: any[] }> { | ||||
|     public readonly IsValid: UIEventSource<boolean>; | ||||
|     public readonly Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, features: any[] }> | ||||
|     public readonly IsValid: Store<boolean>; | ||||
|     public readonly Value: Store<{ bbox: BBox, layer: LayerConfig, features: any[] }> | ||||
| 
 | ||||
|     constructor( | ||||
|         state: UserRelatedState, | ||||
|  | @ -85,7 +85,7 @@ export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer: | |||
|             return copy | ||||
|         }) | ||||
| 
 | ||||
|         const matching: UIEventSource<{ properties: any, geometry: { coordinates: [number, number] } }[]> = layerPicker.GetValue().map((layer: LayerConfig) => { | ||||
|         const matching: Store<{ properties: any, geometry: { coordinates: [number, number] } }[]> = layerPicker.GetValue().map((layer: LayerConfig) => { | ||||
|             if (layer === undefined) { | ||||
|                 return []; | ||||
|             } | ||||
|  | @ -120,9 +120,9 @@ export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer: | |||
|                     appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined) | ||||
|                 }))), | ||||
|             zoomToFeatures: true, | ||||
|             features: new StaticFeatureSource(matching, false), | ||||
|             features: StaticFeatureSource.fromDateless(matching.map(features => features.map(feature => ({feature})))), | ||||
|             leafletMap: map.leafletMap, | ||||
|             popup: (tag, layer) => new PreviewPanel(tag, layer).SetClass("font-lg") | ||||
|             popup: (tag) => new PreviewPanel(tag).SetClass("font-lg") | ||||
|         }) | ||||
|         var bbox = matching.map(feats => BBox.bboxAroundAll(feats.map(f => new BBox([f.geometry.coordinates])))) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import Combine from "../Base/Combine"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import UserRelatedState from "../../Logic/State/UserRelatedState"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import {Utils} from "../../Utils"; | ||||
|  | @ -15,8 +15,8 @@ import CheckBoxes from "../Input/Checkboxes"; | |||
|  * Shows the attributes by value, requests to check them of | ||||
|  */ | ||||
| export class PreviewAttributesPanel extends Combine implements FlowStep<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }> { | ||||
|     public readonly IsValid: UIEventSource<boolean>; | ||||
|     public readonly Value: UIEventSource<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }> | ||||
|     public readonly IsValid: Store<boolean>; | ||||
|     public readonly Value: Store<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }> | ||||
| 
 | ||||
|     constructor( | ||||
|         state: UserRelatedState, | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import Combine from "../Base/Combine"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, Stores} from "../../Logic/UIEventSource"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
|  | @ -10,7 +10,6 @@ import FileSelectorButton from "../Input/FileSelectorButton"; | |||
| import {FlowStep} from "./FlowStep"; | ||||
| import {parse} from "papaparse"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import {del} from "idb-keyval"; | ||||
| import {TagUtils} from "../../Logic/Tags/TagUtils"; | ||||
| 
 | ||||
| class FileSelector extends InputElementMap<FileList, { name: string, contents: Promise<string> }> { | ||||
|  | @ -38,11 +37,11 @@ class FileSelector extends InputElementMap<FileList, { name: string, contents: P | |||
|  */ | ||||
| export class RequestFile extends Combine implements FlowStep<{features: any[]}> { | ||||
| 
 | ||||
|     public readonly IsValid: UIEventSource<boolean> | ||||
|     public readonly IsValid: Store<boolean> | ||||
|     /** | ||||
|      * The loaded GeoJSON | ||||
|      */ | ||||
|     public readonly Value: UIEventSource<{features: any[]}> | ||||
|     public readonly Value: Store<{features: any[]}> | ||||
| 
 | ||||
|     constructor() { | ||||
|         const t = Translations.t.importHelper.selectFile; | ||||
|  | @ -54,15 +53,15 @@ export class RequestFile extends Combine implements FlowStep<{features: any[]}> | |||
|             return t.loadedFilesAre.Subs({file: file.name}).SetClass("thanks") | ||||
|         })) | ||||
| 
 | ||||
|         const text = UIEventSource.flatten( | ||||
|         const text = Stores.flatten( | ||||
|             csvSelector.GetValue().map(v => { | ||||
|                 if (v === undefined) { | ||||
|                     return undefined | ||||
|                 } | ||||
|                 return UIEventSource.FromPromise(v.contents) | ||||
|                 return Stores.FromPromise(v.contents) | ||||
|             })) | ||||
| 
 | ||||
|         const asGeoJson: UIEventSource<any | { error: string | BaseUIElement }> = text.map(src => { | ||||
|         const asGeoJson: Store<any | { error: string | BaseUIElement }> = text.map((src: string) => { | ||||
|             if (src === undefined) { | ||||
|                 return undefined | ||||
|             } | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import {FlowStep} from "./FlowStep"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store} from "../../Logic/UIEventSource"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import {InputElement} from "../Input/InputElement"; | ||||
| import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts"; | ||||
|  | @ -24,13 +24,13 @@ export default class SelectTheme extends Combine implements FlowStep<{ | |||
|     bbox: BBox, | ||||
| }> { | ||||
| 
 | ||||
|     public readonly Value: UIEventSource<{ | ||||
|     public readonly Value: Store<{ | ||||
|         features: any[], | ||||
|         theme: string, | ||||
|         layer: LayerConfig, | ||||
|         bbox: BBox, | ||||
|     }>; | ||||
|     public readonly IsValid: UIEventSource<boolean>; | ||||
|     public readonly IsValid: Store<boolean>; | ||||
| 
 | ||||
|     constructor(params: ({ features: any[], layer: LayerConfig, bbox: BBox, })) { | ||||
|         const t = Translations.t.importHelper.selectTheme | ||||
|  |  | |||
|  | @ -1,11 +1,12 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| 
 | ||||
| export abstract class InputElement<T> extends BaseUIElement { | ||||
| 
 | ||||
| 
 | ||||
|     abstract GetValue(): UIEventSource<T>; | ||||
| 
 | ||||
|     abstract IsValid(t: T): boolean; | ||||
| 
 | ||||
| export interface ReadonlyInputElement<T> extends BaseUIElement{ | ||||
|     GetValue(): Store<T>; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export abstract class InputElement<T> extends BaseUIElement implements ReadonlyInputElement<any>{ | ||||
|     abstract GetValue(): UIEventSource<T>; | ||||
|     abstract IsValid(t: T): boolean; | ||||
| } | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ export default class InputElementMap<T, X> extends InputElement<X> { | |||
|         this.toX = toX; | ||||
|         this._inputElement = inputElement; | ||||
|         const self = this; | ||||
|         this._value = inputElement.GetValue().map( | ||||
|         this._value = inputElement.GetValue().sync( | ||||
|             (t => { | ||||
|                 const newX = toX(t); | ||||
|                 const currentX = self.GetValue()?.data; | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import {InputElement} from "./InputElement"; | ||||
| import {ReadonlyInputElement} from "./InputElement"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Minimap, {MinimapObj} from "../Base/Minimap"; | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import Combine from "../Base/Combine"; | ||||
|  | @ -17,11 +17,10 @@ import BaseUIElement from "../BaseUIElement"; | |||
| import Toggle from "./Toggle"; | ||||
| import * as matchpoint from "../../assets/layers/matchpoint/matchpoint.json" | ||||
| 
 | ||||
| export default class LocationInput extends InputElement<Loc> implements MinimapObj { | ||||
| export default class LocationInput extends BaseUIElement implements ReadonlyInputElement<Loc>, MinimapObj { | ||||
| 
 | ||||
|     private static readonly matchLayer = new LayerConfig(matchpoint, "LocationInput.matchpoint", true) | ||||
| 
 | ||||
|     IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||
|     public readonly snappedOnto: UIEventSource<any> = new UIEventSource<any>(undefined) | ||||
|     public readonly _matching_layer: LayerConfig; | ||||
|     public readonly leafletMap: UIEventSource<any> | ||||
|  | @ -33,9 +32,9 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO | |||
|      * The features to which the input should be snapped | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _snapTo: UIEventSource<{ feature: any }[]> | ||||
|     private readonly _value: UIEventSource<Loc> | ||||
|     private readonly _snappedPoint: UIEventSource<any> | ||||
|     private readonly _snapTo: Store<{ feature: any }[]> | ||||
|     private readonly _value: Store<Loc> | ||||
|     private readonly _snappedPoint: Store<any> | ||||
|     private readonly _maxSnapDistance: number | ||||
|     private readonly _snappedPointTags: any; | ||||
|     private readonly _bounds: UIEventSource<BBox>; | ||||
|  | @ -151,7 +150,7 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO | |||
|         this.location = this.map.location; | ||||
|     } | ||||
| 
 | ||||
|     GetValue(): UIEventSource<Loc> { | ||||
|     GetValue(): Store<Loc> { | ||||
|         return this._value; | ||||
|     } | ||||
| 
 | ||||
|  | @ -188,7 +187,7 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO | |||
|                 // Show the lines to snap to
 | ||||
|                 console.log("Constructing the snap-to layer", this._snapTo) | ||||
|                 new ShowDataMultiLayer({ | ||||
|                         features: new StaticFeatureSource(this._snapTo, true), | ||||
|                         features: StaticFeatureSource.fromDateless(this._snapTo), | ||||
|                         zoomToFeatures: false, | ||||
|                         leafletMap: this.map.leafletMap, | ||||
|                         layers: State.state.filteredLayers | ||||
|  | @ -201,8 +200,10 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO | |||
|                     } | ||||
|                     return [{feature: loc}]; | ||||
|                 }) | ||||
|                 console.log("Constructing the match layer", matchPoint) | ||||
| 
 | ||||
|                 new ShowDataLayer({ | ||||
|                     features: new StaticFeatureSource(matchPoint, true), | ||||
|                     features: StaticFeatureSource.fromDateless(matchPoint), | ||||
|                     zoomToFeatures: false, | ||||
|                     leafletMap: this.map.leafletMap, | ||||
|                     layerToShow: this._matching_layer, | ||||
|  |  | |||
|  | @ -152,7 +152,7 @@ export class RadioButton<T> extends InputElement<T> { | |||
|             form.appendChild(block); | ||||
|         } | ||||
| 
 | ||||
|         value.addCallbackAndRun((selected) => { | ||||
|         value.addCallbackAndRun((selected:T) => { | ||||
|             let somethingChecked = false; | ||||
|             for (let i = 0; i < inputs.length; i++) { | ||||
|                 let input = inputs[i]; | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import Lazy from "../Base/Lazy"; | ||||
|  | @ -9,16 +9,16 @@ import Lazy from "../Base/Lazy"; | |||
|  */ | ||||
| export default class Toggle extends VariableUiElement { | ||||
| 
 | ||||
|     public readonly isEnabled: UIEventSource<boolean>; | ||||
|     public readonly isEnabled: Store<boolean>; | ||||
| 
 | ||||
|     constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, isEnabled: UIEventSource<boolean> = new UIEventSource<boolean>(false)) { | ||||
|     constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, isEnabled: Store<boolean> = new UIEventSource<boolean>(false)) { | ||||
|         super( | ||||
|             isEnabled?.map(isEnabled => isEnabled ? showEnabled : showDisabled) | ||||
|         ); | ||||
|         this.isEnabled = isEnabled | ||||
|     } | ||||
| 
 | ||||
|     public static If(condition: UIEventSource<boolean>, constructor: () => BaseUIElement): BaseUIElement { | ||||
|     public static If(condition: Store<boolean>, constructor: () => BaseUIElement): BaseUIElement { | ||||
|         if (constructor === undefined) { | ||||
|             return undefined | ||||
|         } | ||||
|  | @ -30,7 +30,23 @@ export default class Toggle extends VariableUiElement { | |||
| 
 | ||||
|     } | ||||
|      | ||||
|     public ToggleOnClick(): Toggle { | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Same as `Toggle`, but will swap on click | ||||
|  */ | ||||
| export class ClickableToggle extends Toggle { | ||||
| 
 | ||||
|     public readonly isEnabled: UIEventSource<boolean>; | ||||
| 
 | ||||
|     constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, isEnabled: UIEventSource<boolean> = new UIEventSource<boolean>(false)) { | ||||
|         super( | ||||
|           showEnabled, showDisabled, isEnabled | ||||
|         ); | ||||
|         this.isEnabled = isEnabled | ||||
|     } | ||||
|      | ||||
|     public ToggleOnClick(): ClickableToggle { | ||||
|         const self = this; | ||||
|         this.onClick(() => { | ||||
|             self.isEnabled.setData(!self.isEnabled.data); | ||||
|  |  | |||
|  | @ -1,23 +1,22 @@ | |||
| import {InputElement} from "./InputElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {InputElement, ReadonlyInputElement} from "./InputElement"; | ||||
| import {Store} from "../../Logic/UIEventSource"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| 
 | ||||
| export default class VariableInputElement<T> extends InputElement<T> { | ||||
| export default class VariableInputElement<T> extends BaseUIElement implements ReadonlyInputElement<T> { | ||||
| 
 | ||||
|     private readonly value: UIEventSource<T>; | ||||
|     private readonly value: Store<T>; | ||||
|     private readonly element: BaseUIElement | ||||
|     private readonly upstream: UIEventSource<InputElement<T>>; | ||||
| 
 | ||||
|     constructor(upstream: UIEventSource<InputElement<T>>) { | ||||
|     private readonly upstream: Store<InputElement<T>>; | ||||
| 
 | ||||
|     constructor(upstream: Store<InputElement<T>>) { | ||||
|         super() | ||||
|         this.upstream = upstream; | ||||
|         this.value = upstream.bind(v => v.GetValue()) | ||||
|         this.element = new VariableUiElement(upstream) | ||||
|     } | ||||
| 
 | ||||
|     GetValue(): UIEventSource<T> { | ||||
|     GetValue(): Store<T> { | ||||
|         return this.value; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -367,6 +367,9 @@ export class OH { | |||
|         return OH.ToString(OH.MergeTimes(OH.Parse(str))) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parses a string into Opening Hours | ||||
|      */ | ||||
|     public static Parse(rules: string): OpeningHour[] { | ||||
|         if (rules === undefined || rules === "") { | ||||
|             return [] | ||||
|  |  | |||
|  | @ -4,15 +4,14 @@ | |||
|  * Exports everything conventiently as a string, for direct use | ||||
|  */ | ||||
| import OpeningHoursPicker from "./OpeningHoursPicker"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import {OH} from "./OpeningHours"; | ||||
| import {OH, OpeningHour} from "./OpeningHours"; | ||||
| import {InputElement} from "../Input/InputElement"; | ||||
| import PublicHolidayInput from "./PublicHolidayInput"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| 
 | ||||
| 
 | ||||
|  | @ -28,8 +27,7 @@ export default class OpeningHoursInput extends InputElement<string> { | |||
|         this._value = value; | ||||
|         let valueWithoutPrefix = value | ||||
|         if (prefix !== "" && postfix !== "") { | ||||
| 
 | ||||
|             valueWithoutPrefix = value.map(str => { | ||||
|             valueWithoutPrefix = value.sync(str => { | ||||
|                 if (str === undefined) { | ||||
|                     return undefined; | ||||
|                 } | ||||
|  | @ -55,7 +53,7 @@ export default class OpeningHoursInput extends InputElement<string> { | |||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         const leftoverRules = valueWithoutPrefix.map<string[]>(str => { | ||||
|         const leftoverRules: Store<string[]> = valueWithoutPrefix.map(str => { | ||||
|             if (str === undefined) { | ||||
|                 return [] | ||||
|             } | ||||
|  | @ -72,35 +70,40 @@ export default class OpeningHoursInput extends InputElement<string> { | |||
|             } | ||||
|             return leftOvers; | ||||
|         }) | ||||
|         // Note: MUST be bound AFTER the leftover rules!
 | ||||
|         const rulesFromOhPicker = valueWithoutPrefix.map(OH.Parse); | ||||
|         | ||||
|         const ph = valueWithoutPrefix.map<string>(str => { | ||||
|             if (str === undefined) { | ||||
|                 return "" | ||||
|             } | ||||
|             const rules = str.split(";"); | ||||
|         let ph = ""; | ||||
|         const rules = valueWithoutPrefix.data?.split(";") ?? []; | ||||
|         for (const rule of rules) { | ||||
|             if (OH.ParsePHRule(rule) !== null) { | ||||
|                     return rule; | ||||
|                 ph = rule; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|             return ""; | ||||
|         }) | ||||
|         const phSelector = new PublicHolidayInput(ph); | ||||
|         const phSelector = new PublicHolidayInput(new UIEventSource<string>(ph)); | ||||
|          | ||||
|         function update() { | ||||
|             const regular = OH.ToString(rulesFromOhPicker.data); | ||||
|             const rules: string[] = [ | ||||
|                 regular, | ||||
|                 ...leftoverRules.data, | ||||
|                 ph.data | ||||
|             ] | ||||
|             valueWithoutPrefix.setData(Utils.NoEmpty(rules).join(";")); | ||||
|          | ||||
|         // Note: MUST be bound AFTER the leftover rules!
 | ||||
|         const rulesFromOhPicker: UIEventSource<OpeningHour[]> = valueWithoutPrefix.sync(str => { | ||||
|             console.log(">> Parsing '"+ str+"'") | ||||
|             return OH.Parse(str); | ||||
|         }, [leftoverRules, phSelector.GetValue()], (rules, oldString) => { | ||||
|             let str = OH.ToString(rules); | ||||
|             const ph = phSelector.GetValue().data; | ||||
|             if(ph){ | ||||
|                str += "; "+ph  | ||||
|             } | ||||
|              | ||||
|         rulesFromOhPicker.addCallback(update); | ||||
|         ph.addCallback(update); | ||||
|             str += leftoverRules.data.join("; ") | ||||
|             if(!str.endsWith(";")){ | ||||
|                 str += ";" | ||||
|             } | ||||
|             if(str === oldString){ | ||||
|                 return oldString; // We pass a reference to the old string to stabilize the EventSource
 | ||||
|             } | ||||
|             console.log("Reconstructed '"+ str+"'") | ||||
|             return str; | ||||
|         }); | ||||
| 
 | ||||
| 
 | ||||
|         const leftoverWarning = new VariableUiElement(leftoverRules.map((leftovers: string[]) => { | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import {SpecialVisualization} from "../SpecialVisualizations"; | ||||
| import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Stores, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {DefaultGuiState} from "../DefaultGuiState"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
| import Img from "../Base/Img"; | ||||
|  | @ -97,7 +97,7 @@ class ApplyButton extends UIElement { | |||
|         new ShowDataLayer({ | ||||
|             leafletMap: previewMap.leafletMap, | ||||
|             zoomToFeatures: true, | ||||
|             features: new StaticFeatureSource(features, false), | ||||
|             features: StaticFeatureSource.fromGeojson(features), | ||||
|             state: this.state, | ||||
|             layerToShow: this.layer.layerDef, | ||||
|         }) | ||||
|  | @ -218,7 +218,7 @@ export default class AutoApplyButton implements SpecialVisualization { | |||
|             return new Lazy(() => { | ||||
|                 const to_parse = new UIEventSource(undefined) | ||||
|                 // Very ugly hack: read the value every 500ms
 | ||||
|                 UIEventSource.Chronic(500, () => to_parse.data === undefined).addCallback(() => { | ||||
|                 Stores.Chronic(500, () => to_parse.data === undefined).addCallback(() => { | ||||
|                     const applicable = tagSource.data[argument[1]] | ||||
|                     to_parse.setData(applicable) | ||||
|                 }) | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import Toggle from "../Input/Toggle"; | |||
| import Translations from "../i18n/Translations"; | ||||
| import Svg from "../../Svg"; | ||||
| import DeleteAction from "../../Logic/Osm/Actions/DeleteAction"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
|  | @ -106,7 +106,7 @@ export default class DeleteWizard extends Toggle { | |||
|                 } | ||||
|             ) | ||||
| 
 | ||||
|         const isShown: UIEventSource<boolean> = tagsSource.map(tgs => tgs.id.indexOf("-") < 0) | ||||
|         const isShown: Store<boolean> = tagsSource.map(tgs => tgs.id.indexOf("-") < 0) | ||||
| 
 | ||||
|         const deleteOptionPicker = DeleteWizard.constructMultipleChoice(options, tagsSource, state); | ||||
|         const deleteDialog = new Combine([ | ||||
|  | @ -350,8 +350,10 @@ class DeleteabilityChecker { | |||
| 
 | ||||
|             if (allByMyself.data === null && useTheInternet) { | ||||
|                 // We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above
 | ||||
|                 OsmObject.DownloadHistory(id).map(versions => versions.map(version => version.tags["_last_edit:contributor:uid"])).syncWith(previousEditors) | ||||
|                 const hist = OsmObject.DownloadHistory(id).map(versions => versions.map(version => version.tags["_last_edit:contributor:uid"])) | ||||
|                 hist.addCallbackAndRunD(hist => previousEditors.setData(hist)) | ||||
|             } | ||||
|              | ||||
|             if (allByMyself.data === true) { | ||||
|                 // Yay! We can download!
 | ||||
|                 return true; | ||||
|  |  | |||
|  | @ -241,7 +241,7 @@ ${Utils.special_visualizations_importRequirementDocs} | |||
|         new ShowDataMultiLayer({ | ||||
|             leafletMap: confirmationMap.leafletMap, | ||||
|             zoomToFeatures: true, | ||||
|             features: new StaticFeatureSource([feature], false), | ||||
|             features: StaticFeatureSource.fromGeojson([feature]), | ||||
|             state: state, | ||||
|             layers: state.filteredLayers | ||||
|         }) | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store} from "../../Logic/UIEventSource"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
|  | @ -16,12 +16,12 @@ import {OsmConnection} from "../../Logic/Osm/OsmConnection"; | |||
| 
 | ||||
| 
 | ||||
| export interface MultiApplyParams { | ||||
|     featureIds: UIEventSource<string[]>, | ||||
|     featureIds: Store<string[]>, | ||||
|     keysToApply: string[], | ||||
|     text: string, | ||||
|     autoapply: boolean, | ||||
|     overwrite: boolean, | ||||
|     tagsSource: UIEventSource<any>, | ||||
|     tagsSource: Store<any>, | ||||
|     state: { | ||||
|         changes: Changes, | ||||
|         allElements: ElementStorage, | ||||
|  | @ -145,7 +145,7 @@ export default class MultiApply extends Toggle { | |||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const isShown: UIEventSource<boolean> = p.state.osmConnection.isLoggedIn.map(loggedIn => { | ||||
|         const isShown: Store<boolean> = p.state.osmConnection.isLoggedIn.map(loggedIn => { | ||||
|             return loggedIn && p.featureIds.data.length > 0 | ||||
|         }, [p.featureIds]) | ||||
|         super(new Combine(elems), undefined, isShown); | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import Combine from "../Base/Combine"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, Stores, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {SlideShow} from "../Image/SlideShow"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import {ClickableToggle} from "../Input/Toggle"; | ||||
| import Loading from "../Base/Loading"; | ||||
| import {AttributedImage} from "../Image/AttributedImage"; | ||||
| import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"; | ||||
|  | @ -15,8 +15,6 @@ import {SubtleButton} from "../Base/SubtleButton"; | |||
| import {GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import {ElementStorage} from "../../Logic/ElementStorage"; | ||||
| import Lazy from "../Base/Lazy"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import beginningOfLine = Mocha.reporters.Base.cursor.beginningOfLine; | ||||
| 
 | ||||
| export interface P4CPicture { | ||||
|     pictureUrl: string, | ||||
|  | @ -42,7 +40,7 @@ export interface NearbyImageOptions { | |||
|     // Radius of the upstream search
 | ||||
|     searchRadius?: 500 | number, | ||||
|     maxDaysOld?: 1095 | number, | ||||
|     blacklist: UIEventSource<{ url: string }[]>, | ||||
|     blacklist: Store<{ url: string }[]>, | ||||
|     shownImagesCount?: UIEventSource<number>, | ||||
|     towardscenter?: UIEventSource<boolean>; | ||||
|     allowSpherical?: UIEventSource<boolean> | ||||
|  | @ -173,7 +171,7 @@ export default class NearbyImages extends Lazy { | |||
|         const nearbyImages = state !== undefined ? new ImagesInLoadedDataFetcher(state).fetchAround(options) : [] | ||||
| 
 | ||||
| 
 | ||||
|         return UIEventSource.FromPromise<P4CPicture[]>( | ||||
|         return Stores.FromPromise<P4CPicture[]>( | ||||
|             picManager.startPicsRetrievalAround(new P4C.LatLng(options.lat, options.lon), options.searchRadius ?? 500, { | ||||
|                 mindate: new Date().getTime() - (options.maxDaysOld ?? (3 * 365)) * 24 * 60 * 60 * 1000, | ||||
|                 towardscenter: false | ||||
|  | @ -234,7 +232,7 @@ export default class NearbyImages extends Lazy { | |||
|         return new AttributedImage({url: info.thumbUrl, provider, date: new Date(info.date)}) | ||||
|     } | ||||
| 
 | ||||
|     protected asToggle(info: P4CPicture): Toggle { | ||||
|     protected asToggle(info: P4CPicture): ClickableToggle { | ||||
|         const imgNonSelected = NearbyImages.asAttributedImage(info); | ||||
|         const imageSelected = NearbyImages.asAttributedImage(info); | ||||
| 
 | ||||
|  | @ -246,7 +244,7 @@ export default class NearbyImages extends Lazy { | |||
|             hoveringCheckmark, | ||||
|         ]).SetClass("relative block") | ||||
| 
 | ||||
|         return new Toggle(selected, nonSelected).SetClass("").ToggleOnClick(); | ||||
|         return new ClickableToggle(selected, nonSelected).SetClass("").ToggleOnClick(); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,9 +7,8 @@ import Translations from "../i18n/Translations"; | |||
| import {Utils} from "../../Utils"; | ||||
| import Img from "../Base/Img"; | ||||
| import {SlideShow} from "../Image/SlideShow"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Stores, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {OsmConnection} from "../../Logic/Osm/OsmConnection"; | ||||
| import {UIElement} from "../UIElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| 
 | ||||
| export default class NoteCommentElement extends Combine { | ||||
|  | @ -25,7 +24,7 @@ export default class NoteCommentElement extends Combine { | |||
|     }) { | ||||
|         const t = Translations.t.notes; | ||||
| 
 | ||||
|         let actionIcon: BaseUIElement = undefined; | ||||
|         let actionIcon: BaseUIElement; | ||||
|         if (comment.action === "opened" || comment.action === "reopened") { | ||||
|             actionIcon = Svg.note_svg() | ||||
|         } else if (comment.action === "closed") { | ||||
|  | @ -41,7 +40,7 @@ export default class NoteCommentElement extends Combine { | |||
|             user = new Link(comment.user, comment.user_url ?? "", true) | ||||
|         } | ||||
| 
 | ||||
|         let userinfo = UIEventSource.FromPromise( Utils.downloadJsonCached("https://www.openstreetmap.org/api/0.6/user/"+comment.uid, 24*60*60*1000)) | ||||
|         let userinfo = Stores.FromPromise( Utils.downloadJsonCached("https://www.openstreetmap.org/api/0.6/user/"+comment.uid, 24*60*60*1000)) | ||||
|         let userImg = new VariableUiElement( userinfo.map(userinfo => { | ||||
|             const href = userinfo?.user?.img?.href; | ||||
|             if(href !== undefined){ | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import TagRenderingQuestion from "./TagRenderingQuestion"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import Combine from "../Base/Combine"; | ||||
|  | @ -14,7 +14,7 @@ import Lazy from "../Base/Lazy"; | |||
|  */ | ||||
| export default class QuestionBox extends VariableUiElement { | ||||
|     public readonly skippedQuestions: UIEventSource<number[]>; | ||||
|     public readonly restingQuestions: UIEventSource<BaseUIElement[]>; | ||||
|     public readonly restingQuestions: Store<BaseUIElement[]>; | ||||
| 
 | ||||
|     constructor(state, options: { | ||||
|         tagsSource: UIEventSource<any>, | ||||
|  | @ -81,7 +81,7 @@ export default class QuestionBox extends VariableUiElement { | |||
|             return undefined; // The questions are depleted
 | ||||
|         }, [skippedQuestions]); | ||||
| 
 | ||||
|         const questionsToAsk: UIEventSource<BaseUIElement[]> = tagsSource.map(tags => { | ||||
|         const questionsToAsk: Store<BaseUIElement[]> = tagsSource.map(tags => { | ||||
|             if (tags === undefined) { | ||||
|                 return []; | ||||
|             } | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import {OsmConnection} from "../../Logic/Osm/OsmConnection"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
|  | @ -6,7 +6,7 @@ import BaseUIElement from "../BaseUIElement"; | |||
| 
 | ||||
| export class SaveButton extends Toggle { | ||||
| 
 | ||||
|     constructor(value: UIEventSource<any>, osmConnection: OsmConnection, textEnabled ?: BaseUIElement, textDisabled ?: BaseUIElement) { | ||||
|     constructor(value: Store<any>, osmConnection: OsmConnection, textEnabled ?: BaseUIElement, textDisabled ?: BaseUIElement) { | ||||
|         if (value === undefined) { | ||||
|             throw "No event source for savebutton, something is wrong" | ||||
|         } | ||||
|  |  | |||
|  | @ -78,7 +78,7 @@ export default class SplitRoadWizard extends Toggle { | |||
| 
 | ||||
|         // Datalayer displaying the road and the cut points (if any)
 | ||||
|         new ShowDataMultiLayer({ | ||||
|             features: new StaticFeatureSource([roadElement], false), | ||||
|             features: StaticFeatureSource.fromGeojson([roadElement]), | ||||
|             layers: state.filteredLayers, | ||||
|             leafletMap: miniMap.leafletMap, | ||||
|             zoomToFeatures: true, | ||||
|  | @ -86,7 +86,7 @@ export default class SplitRoadWizard extends Toggle { | |||
|         }) | ||||
| 
 | ||||
|         new ShowDataLayer({ | ||||
|             features: new StaticFeatureSource(splitPoints, true), | ||||
|             features: new StaticFeatureSource(splitPoints), | ||||
|             leafletMap: miniMap.leafletMap, | ||||
|             zoomToFeatures: false, | ||||
|             layerToShow: SplitRoadWizard.splitLayerStyling, | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import Translations from "../i18n/Translations"; | |||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||
|  | @ -40,7 +40,7 @@ export default class TagApplyButton implements AutoAction { | |||
|     ]; | ||||
|     public readonly example = "`{tag_apply(survey_date=$_now:date, Surveyed today!)}`, `{tag_apply(addr:street=$addr:street, Apply the address, apply_icon.svg, _closest_osm_id)"; | ||||
| 
 | ||||
|     public static generateTagsToApply(spec: string, tagSource: UIEventSource<any>): UIEventSource<Tag[]> { | ||||
|     public static generateTagsToApply(spec: string, tagSource: Store<any>): Store<Tag[]> { | ||||
| 
 | ||||
|         const tgsSpec = spec.split(";").map(spec => { | ||||
|             const kv = spec.split("=").map(s => s.trim()); | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ import {UIEventSource} from "../../Logic/UIEventSource"; | |||
| import {Utils} from "../../Utils"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import List from "../Base/List"; | ||||
| import {SubstitutedTranslation} from "../SubstitutedTranslation"; | ||||
| import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; | ||||
| import Combine from "../Base/Combine"; | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, Stores, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {InputElement} from "../Input/InputElement"; | ||||
| import {InputElement, ReadonlyInputElement} from "../Input/InputElement"; | ||||
| import ValidatedTextField from "../Input/ValidatedTextField"; | ||||
| import {FixedInputElement} from "../Input/FixedInputElement"; | ||||
| import {RadioButton} from "../Input/RadioButton"; | ||||
|  | @ -45,14 +45,14 @@ export default class TagRenderingQuestion extends Combine { | |||
|                     units?: Unit[], | ||||
|                     afterSave?: () => void, | ||||
|                     cancelButton?: BaseUIElement, | ||||
|                     saveButtonConstr?: (src: UIEventSource<TagsFilter>) => BaseUIElement, | ||||
|                     bottomText?: (src: UIEventSource<TagsFilter>) => BaseUIElement | ||||
|                     saveButtonConstr?: (src: Store<TagsFilter>) => BaseUIElement, | ||||
|                     bottomText?: (src: Store<TagsFilter>) => BaseUIElement | ||||
|                 } | ||||
|     ) { | ||||
| 
 | ||||
| 
 | ||||
|         const applicableMappingsSrc = | ||||
|             UIEventSource.ListStabilized(tags.map(tags => { | ||||
|             Stores.ListStabilized(tags.map(tags => { | ||||
|                 const applicableMappings: { if: TagsFilter, icon?: string, then: TypedTranslation<object>, ifnot?: TagsFilter, addExtraTags: Tag[] }[] = [] | ||||
|                 for (const mapping of configuration.mappings ?? []) { | ||||
|                     if (mapping.hideInAnswer === true) { | ||||
|  | @ -81,7 +81,7 @@ export default class TagRenderingQuestion extends Combine { | |||
| 
 | ||||
| 
 | ||||
|         const feedback = new UIEventSource<Translation>(undefined) | ||||
|         const inputElement: InputElement<TagsFilter> = | ||||
|         const inputElement: ReadonlyInputElement<TagsFilter> = | ||||
|             new VariableInputElement(applicableMappingsSrc.map(applicableMappings => | ||||
|                 TagRenderingQuestion.GenerateInputElement(state, configuration, applicableMappings, applicableUnit, tags, feedback) | ||||
|             )) | ||||
|  | @ -452,8 +452,8 @@ export default class TagRenderingQuestion extends Combine { | |||
| 
 | ||||
|     } | ||||
|      | ||||
|     public static CreateTagExplanation(selectedValue: UIEventSource<TagsFilter>, | ||||
|                                        tags: UIEventSource<object>, | ||||
|     public static CreateTagExplanation(selectedValue: Store<TagsFilter>, | ||||
|                                        tags: Store<object>, | ||||
|                                        state?: {osmConnection?: OsmConnection}){ | ||||
|         return new VariableUiElement( | ||||
|             selectedValue.map( | ||||
|  |  | |||
|  | @ -37,7 +37,6 @@ export default class ReviewForm extends InputElement<Review> { | |||
|         const comment = new TextField({ | ||||
|             placeholder: Translations.t.reviews.write_a_comment.Clone(), | ||||
|             htmlType: "area", | ||||
|             value: this._value.map(r => r?.comment), | ||||
|             textAreaRows: 5 | ||||
|         }) | ||||
|         comment.GetValue().addCallback(comment => { | ||||
|  | @ -62,7 +61,7 @@ export default class ReviewForm extends InputElement<Review> { | |||
|                     new SaveButton( | ||||
|                         this._value.map(r => self.IsValid(r)), osmConnection | ||||
|                     ).onClick(() => { | ||||
|                         reviewIsSaving.setData(true), | ||||
|                         reviewIsSaving.setData(true); | ||||
|                         onSave(this._value.data, () => { | ||||
|                             reviewIsSaved.setData(true) | ||||
|                         }); | ||||
|  |  | |||
|  | @ -195,7 +195,7 @@ export default class ShowDataLayerImplementation { | |||
|                     const tagsSource = this.allElements?.addOrGetElement(feat) ?? new UIEventSource<any>(feat.properties); | ||||
|                     let offsettedLine; | ||||
|                     tagsSource | ||||
|                         .map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags), [], undefined, true) | ||||
|                         .map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags)) | ||||
|                         .withEqualityStabilized((a, b) => { | ||||
|                             if (a === b) { | ||||
|                                 return true | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {ElementStorage} from "../../Logic/ElementStorage"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | ||||
|  | @ -10,6 +10,6 @@ export interface ShowDataLayerOptions { | |||
|     leafletMap: UIEventSource<L.Map>, | ||||
|     popup?: undefined | ((tags: UIEventSource<any>, layer: LayerConfig) => ScrollableFullScreen), | ||||
|     zoomToFeatures?: false | boolean, | ||||
|     doShowLayer?: UIEventSource<boolean>, | ||||
|     doShowLayer?: Store<boolean>, | ||||
|     state?: { allElements?: ElementStorage } | ||||
| } | ||||
|  | @ -1,5 +1,5 @@ | |||
| import FeatureSource, {Tiled} from "../../Logic/FeatureSource/FeatureSource"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import ShowDataLayer from "./ShowDataLayer"; | ||||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
|  | @ -18,7 +18,7 @@ export default class ShowTileInfo { | |||
| 
 | ||||
| 
 | ||||
|         const source = options.source | ||||
|         const metaFeature: UIEventSource<any[]> = | ||||
|         const metaFeature: Store<{feature, freshness: Date}[]> = | ||||
|             source.features.map(features => { | ||||
|                 const bbox = source.bbox | ||||
|                 const [z, x, y] = Tiles.tile_from_index(source.tileIndex) | ||||
|  | @ -47,12 +47,12 @@ export default class ShowTileInfo { | |||
|                     } | ||||
|                 } | ||||
|                 const center = GeoOperations.centerpoint(box) | ||||
|                 return [box, center] | ||||
|                 return [box, center].map(feature => ({feature, freshness: new Date()})) | ||||
|             }) | ||||
| 
 | ||||
|         new ShowDataLayer({ | ||||
|             layerToShow: ShowTileInfo.styling, | ||||
|             features: new StaticFeatureSource(metaFeature, false), | ||||
|             features: new StaticFeatureSource(metaFeature), | ||||
|             leafletMap: options.leafletMap, | ||||
|             doShowLayer: options.doShowLayer, | ||||
|             state: State.state, | ||||
|  |  | |||
|  | @ -141,7 +141,7 @@ export class TileHierarchyAggregator implements FeatureSource { | |||
|                 return empty | ||||
|             } | ||||
| 
 | ||||
|             const features = [] | ||||
|             const features: {feature: any, freshness: Date}[] = [] | ||||
|             self.visitSubTiles(aggr => { | ||||
|                 if (aggr.showCount < cutoff) { | ||||
|                     return false | ||||
|  | @ -156,7 +156,7 @@ export class TileHierarchyAggregator implements FeatureSource { | |||
|             return features | ||||
|         }, [this.updateSignal.stabilized(500)]) | ||||
| 
 | ||||
|         return new StaticFeatureSource(features, true); | ||||
|         return new StaticFeatureSource(features); | ||||
|     } | ||||
| 
 | ||||
|     private update() { | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {UIEventSource} from "../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../Logic/UIEventSource"; | ||||
| import {VariableUiElement} from "./Base/VariableUIElement"; | ||||
| import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"; | ||||
| import {ImageCarousel} from "./Image/ImageCarousel"; | ||||
|  | @ -207,7 +207,7 @@ class NearbyImageVis implements SpecialVisualization { | |||
|         const nearby = new Lazy(() => { | ||||
|             const towardsCenter = new CheckBox(t.onlyTowards, false) | ||||
|              | ||||
|             const radiusValue=   state?.osmConnection?.GetPreference("nearby-images-radius","300").map(s => Number(s), [], i => ""+i) ?? new UIEventSource(300); | ||||
|             const radiusValue=   state?.osmConnection?.GetPreference("nearby-images-radius","300").sync(s => Number(s), [], i => ""+i) ?? new UIEventSource(300); | ||||
| 
 | ||||
|             const radius = new Slider(25, 500, {value:  | ||||
|                     radiusValue, step: 25}) | ||||
|  | @ -453,7 +453,7 @@ export default class SpecialVisualizations { | |||
|                         const keys = [...args] | ||||
|                         keys.splice(0, 1) | ||||
|                         const featureStore = state.allElements.ContainingFeatures | ||||
|                         const featuresToShow: UIEventSource<{ freshness: Date, feature: any }[]> = tagSource.map(properties => { | ||||
|                         const featuresToShow: Store<{ freshness: Date, feature: any }[]> = tagSource.map(properties => { | ||||
|                             const values: string[] = Utils.NoNull(keys.map(key => properties[key])) | ||||
|                             const features: { freshness: Date, feature: any }[] = [] | ||||
|                             for (const value of values) { | ||||
|  | @ -507,7 +507,7 @@ export default class SpecialVisualizations { | |||
|                                 leafletMap: minimap["leafletMap"], | ||||
|                                 zoomToFeatures: true, | ||||
|                                 layers: state.filteredLayers, | ||||
|                                 features: new StaticFeatureSource(featuresToShow, true) | ||||
|                                 features: new StaticFeatureSource(featuresToShow) | ||||
|                             } | ||||
|                         ) | ||||
| 
 | ||||
|  | @ -553,7 +553,7 @@ export default class SpecialVisualizations { | |||
|                                 leafletMap: minimap["leafletMap"], | ||||
|                                 zoomToFeatures: true, | ||||
|                                 layerToShow: new LayerConfig(left_right_style_json, "all_known_layers", true), | ||||
|                                 features: new StaticFeatureSource([copy], false), | ||||
|                                 features: StaticFeatureSource.fromGeojson([copy]), | ||||
|                                 state | ||||
|                             } | ||||
|                         ) | ||||
|  | @ -683,7 +683,7 @@ export default class SpecialVisualizations { | |||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         const listSource: UIEventSource<string[]> = tagSource | ||||
|                         const listSource: Store<string[]> = tagSource | ||||
|                             .map(tags => { | ||||
|                                 try { | ||||
|                                     const value = tags[args[0]] | ||||
|  | @ -801,7 +801,7 @@ export default class SpecialVisualizations { | |||
|                         const text = args[2] | ||||
|                         const autoapply = args[3]?.toLowerCase() === "true" | ||||
|                         const overwrite = args[4]?.toLowerCase() === "true" | ||||
|                         const featureIds: UIEventSource<string[]> = tagsSource.map(tags => { | ||||
|                         const featureIds: Store<string[]> = tagsSource.map(tags => { | ||||
|                             const ids = tags[featureIdsKey] | ||||
|                             try { | ||||
|                                 if (ids === undefined) { | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {UIEventSource} from "../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../Logic/UIEventSource"; | ||||
| import {Translation} from "./i18n/Translation"; | ||||
| import Locale from "./i18n/Locale"; | ||||
| import {FixedUiElement} from "./Base/FixedUiElement"; | ||||
|  |  | |||
|  | @ -91,7 +91,7 @@ export default class WikidataPreviewBox extends VariableUiElement { | |||
|         let link = new Link( | ||||
|             new Combine([ | ||||
|                 wikidata.id, | ||||
|                 options.noImages ? wikidata.id : Svg.wikidata_svg().SetStyle("width: 2.5rem").SetClass("block") | ||||
|                 options?.noImages ? wikidata.id : Svg.wikidata_svg().SetStyle("width: 2.5rem").SetClass("block") | ||||
|             ]).SetClass("flex"), | ||||
|             Wikidata.IdToArticle(wikidata.id), true)?.SetClass("must-link") | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import Combine from "../Base/Combine"; | |||
| import {InputElement} from "../Input/InputElement"; | ||||
| import {TextField} from "../Input/TextField"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {ImmutableStore, Store, Stores, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata"; | ||||
| import Locale from "../i18n/Locale"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
|  | @ -10,6 +10,7 @@ import WikidataPreviewBox from "./WikidataPreviewBox"; | |||
| import Title from "../Base/Title"; | ||||
| import WikipediaBox from "./WikipediaBox"; | ||||
| import Svg from "../../Svg"; | ||||
| import Loading from "../Base/Loading"; | ||||
| 
 | ||||
| export default class WikidataSearchBox extends InputElement<string> { | ||||
| 
 | ||||
|  | @ -51,14 +52,12 @@ export default class WikidataSearchBox extends InputElement<string> { | |||
|         }) | ||||
|         const selectedWikidataId = this.wikidataId | ||||
| 
 | ||||
|         const lastSearchResults = new UIEventSource<WikidataResponse[]>([]) | ||||
|         const searchFailMessage = new UIEventSource(undefined) | ||||
|         searchField.GetValue().addCallbackAndRunD(searchText => { | ||||
|         const tooShort = new ImmutableStore<{success: WikidataResponse[]}>({success: undefined}) | ||||
|         const searchResult: Store<{success?: WikidataResponse[], error?: any}> = searchField.GetValue().bind( | ||||
|             searchText => { | ||||
|                 if (searchText.length < 3) { | ||||
|                 return; | ||||
|                     return tooShort; | ||||
|                 } | ||||
|             searchFailMessage.setData(undefined) | ||||
| 
 | ||||
|                 const lang = Locale.language.data | ||||
|                 const key = lang + ":" + searchText | ||||
|                 let promise = WikidataSearchBox._searchCache.get(key) | ||||
|  | @ -72,27 +71,36 @@ export default class WikidataSearchBox extends InputElement<string> { | |||
|                     ) | ||||
|                     WikidataSearchBox._searchCache.set(key, promise) | ||||
|                 } | ||||
| 
 | ||||
|             lastSearchResults.WaitForPromise(promise, err => searchFailMessage.setData(err)) | ||||
| 
 | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         const previews = new VariableUiElement(lastSearchResults.map(searchResults => { | ||||
|             if (searchFailMessage.data !== undefined) { | ||||
|                 return new Combine([Translations.t.general.wikipedia.failed.Clone().SetClass("alert"), searchFailMessage.data]) | ||||
|                 return Stores.FromPromiseWithErr(promise) | ||||
|             } | ||||
|         ) | ||||
|    | ||||
| 
 | ||||
|         const previews = new VariableUiElement(searchResult.map(searchResultsOrFail => { | ||||
| 
 | ||||
|             if (searchField.GetValue().data.length === 0) { | ||||
|                 return Translations.t.general.wikipedia.doSearch | ||||
|             } | ||||
| 
 | ||||
|             if (searchField.GetValue().data.length < 3) { | ||||
|                 return Translations.t.general.wikipedia.searchToShort | ||||
|             } | ||||
|              | ||||
|             if( searchResultsOrFail === undefined) { | ||||
|                 return new Loading(Translations.t.general.loading) | ||||
|             } | ||||
| 
 | ||||
|             if (searchResultsOrFail.error !== undefined) { | ||||
|                 return new Combine([Translations.t.general.wikipedia.failed.Clone().SetClass("alert"), searchResultsOrFail.error]) | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             const searchResults = searchResultsOrFail.success; | ||||
|             if (searchResults.length === 0) { | ||||
|                 return Translations.t.general.wikipedia.noResults.Subs({search: searchField.GetValue().data ?? ""}) | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|             return new Combine(searchResults.map(wikidataresponse => { | ||||
|                 const el = WikidataPreviewBox.WikidataResponsePreview(wikidataresponse).SetClass("rounded-xl p-1 sm:p-2 md:p-3 m-px border-2 sm:border-4 transition-colors") | ||||
|                 el.onClick(() => { | ||||
|  | @ -110,7 +118,7 @@ export default class WikidataSearchBox extends InputElement<string> { | |||
| 
 | ||||
|             })).SetClass("flex flex-col") | ||||
| 
 | ||||
|         }, [searchFailMessage])) | ||||
|         }, [searchField.GetValue()])) | ||||
| 
 | ||||
|         const full = new Combine([ | ||||
|             new Title(Translations.t.general.wikipedia.searchWikidata, 3).SetClass("m-2"), | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ import Title from "../Base/Title"; | |||
| import Wikipedia from "../../Logic/Web/Wikipedia"; | ||||
| import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata"; | ||||
| import {TabbedComponent} from "../Base/TabbedComponent"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Loading from "../Base/Loading"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import Translations from "../i18n/Translations"; | ||||
|  | @ -128,7 +128,7 @@ export default class WikipediaBox extends Combine { | |||
| 
 | ||||
|         const wp = Translations.t.general.wikipedia; | ||||
| 
 | ||||
|         const wikiLink: UIEventSource<[string, string, WikidataResponse] | "loading" | "failed" | ["no page", WikidataResponse]> = | ||||
|         const wikiLink: Store<[string, string, WikidataResponse] | "loading" | "failed" | ["no page", WikidataResponse]> = | ||||
|             Wikidata.LoadWikidataEntry(wikidataId) | ||||
|                 .map(maybewikidata => { | ||||
|                     if (maybewikidata === undefined) { | ||||
|  |  | |||
|  | @ -211,6 +211,10 @@ | |||
|                 "if": "theme=maps", | ||||
|                 "then": "./assets/themes/maps/logo.svg" | ||||
|               }, | ||||
|               { | ||||
|                 "if": "theme=maxspeed", | ||||
|                 "then": "./assets/themes/maxspeed/maxspeed_logo.svg" | ||||
|               }, | ||||
|               { | ||||
|                 "if": "theme=nature", | ||||
|                 "then": "./assets/themes/nature/logo.svg" | ||||
|  |  | |||
|  | @ -112,7 +112,7 @@ async function downloadRaw(targetdir: string, r: TileRange, theme: LayoutConfig, | |||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 console.log("Got the response - writing to ", filename) | ||||
|                 console.log("Got the response - writing ",json.elements.length," elements to ", filename) | ||||
|                 writeFileSync(filename, JSON.stringify(json, null, "  ")); | ||||
|             } catch (err) { | ||||
|                 console.log(url) | ||||
|  | @ -172,7 +172,7 @@ function loadAllTiles(targetdir: string, r: TileRange, theme: LayoutConfig, extr | |||
|             allFeatures.push(...geojson.features) | ||||
|         } | ||||
|     } | ||||
|     return new StaticFeatureSource(allFeatures, false) | ||||
|     return StaticFeatureSource.fromGeojson(allFeatures) | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
|  | @ -127,7 +127,7 @@ async function main(args: string[]) { | |||
|         delete f.bbox | ||||
|     } | ||||
|     TiledFeatureSource.createHierarchy( | ||||
|         new StaticFeatureSource(allFeatures, false), | ||||
|         StaticFeatureSource.fromGeojson(allFeatures), | ||||
|         { | ||||
|             minZoomLevel: zoomlevel, | ||||
|             maxZoomLevel: zoomlevel, | ||||
|  |  | |||
|  | @ -43,7 +43,7 @@ describe("GenerateCache", () => { | |||
|                 "51.15423567022531", "3.250579833984375", "51.162821593316934", "3.262810707092285", | ||||
|                 "--generate-point-overview", "nature_reserve,visitor_information_centre" | ||||
|             ]) | ||||
|             await ScriptUtils.sleep(100) | ||||
|             await ScriptUtils.sleep(500) | ||||
|             const birdhides = JSON.parse(readFileSync("/tmp/np-cache/natuurpunt_birdhide_12_2085_1368.geojson", "UTF8")) | ||||
|             expect(birdhides.features.length).deep.equal(5) | ||||
|             expect(birdhides.features.some(f => f.properties.id === "node/5158056232"), "Didn't find birdhide node/5158056232 ").true | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue