diff --git a/Logic/Actors/AvailableBaseLayers.ts b/Logic/Actors/AvailableBaseLayers.ts index c7b84247ba..db0d6ebe65 100644 --- a/Logic/Actors/AvailableBaseLayers.ts +++ b/Logic/Actors/AvailableBaseLayers.ts @@ -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): UIEventSource + AvailableLayersAt(location: Store): Store - SelectBestLayerAccordingTo(location: UIEventSource, preferedCategory: UIEventSource): UIEventSource; + SelectBestLayerAccordingTo(location: Store, preferedCategory: Store): Store; } @@ -24,12 +24,12 @@ export default class AvailableBaseLayers { private static implementation: AvailableBaseLayersObj - static AvailableLayersAt(location: UIEventSource): UIEventSource { - return AvailableBaseLayers.implementation?.AvailableLayersAt(location) ?? new UIEventSource([]); + static AvailableLayersAt(location: Store): Store { + return AvailableBaseLayers.implementation?.AvailableLayersAt(location) ?? new ImmutableStore([]); } - static SelectBestLayerAccordingTo(location: UIEventSource, preferedCategory: UIEventSource): UIEventSource { - return AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo(location, preferedCategory) ?? new UIEventSource(undefined); + static SelectBestLayerAccordingTo(location: Store, preferedCategory: UIEventSource): Store { + return AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo(location, preferedCategory) ?? new ImmutableStore(undefined); } diff --git a/Logic/Actors/AvailableBaseLayersImplementation.ts b/Logic/Actors/AvailableBaseLayersImplementation.ts index 6b12013feb..67e2bb2c3c 100644 --- a/Logic/Actors/AvailableBaseLayersImplementation.ts +++ b/Logic/Actors/AvailableBaseLayersImplementation.ts @@ -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): UIEventSource { - return UIEventSource.ListStabilized(location.map( + public AvailableLayersAt(location: Store): Store { + 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, preferedCategory: UIEventSource): UIEventSource { + public SelectBestLayerAccordingTo(location: Store, preferedCategory: Store): Store { 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) diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index 24e8e5268a..f000095d8e 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -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; + private readonly _hasLocation: Store; private readonly _currentGPSLocation: UIEventSource; /** * Kept in order to update the marker @@ -70,7 +70,7 @@ export default class GeoLocationHandler extends VariableUiElement { constructor( state: { selectedElement: UIEventSource; - currentUserLocation?: FeatureSource, + currentUserLocation?: SimpleFeatureSource, leafletMap: UIEventSource, layoutToUse: LayoutConfig, featureSwitchGeolocation: UIEventSource @@ -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) { diff --git a/Logic/Actors/OverpassFeatureSource.ts b/Logic/Actors/OverpassFeatureSource.ts index fbd4bd911b..d6db8be4a1 100644 --- a/Logic/Actors/OverpassFeatureSource.ts +++ b/Logic/Actors/OverpassFeatureSource.ts @@ -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 = new UIEventSource(0); private readonly state: { - readonly locationControl: UIEventSource, + readonly locationControl: Store, readonly layoutToUse: LayoutConfig, - readonly overpassUrl: UIEventSource; - readonly overpassTimeout: UIEventSource; - readonly currentBounds: UIEventSource + readonly overpassUrl: Store; + readonly overpassTimeout: Store; + readonly currentBounds: Store } - private readonly _isActive: UIEventSource + private readonly _isActive: Store /** * Callback to handle all the data */ @@ -54,16 +54,16 @@ export default class OverpassFeatureSource implements FeatureSource { constructor( state: { - readonly locationControl: UIEventSource, + readonly locationControl: Store, readonly layoutToUse: LayoutConfig, - readonly overpassUrl: UIEventSource; - readonly overpassTimeout: UIEventSource; - readonly overpassMaxZoom: UIEventSource, - readonly currentBounds: UIEventSource + readonly overpassUrl: Store; + readonly overpassTimeout: Store; + readonly overpassMaxZoom: Store, + readonly currentBounds: Store }, options: { - padToTiles: UIEventSource, - isActive?: UIEventSource, + padToTiles: Store, + isActive?: Store, relationTracker: RelationsTracker, onBboxLoaded?: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void, freshnesses?: Map diff --git a/Logic/Actors/TitleHandler.ts b/Logic/Actors/TitleHandler.ts index 8593ecf184..de822a5107 100644 --- a/Logic/Actors/TitleHandler.ts +++ b/Logic/Actors/TitleHandler.ts @@ -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, + selectedElement: Store, layoutToUse: LayoutConfig, allElements: ElementStorage }) { - const currentTitle: UIEventSource = state.selectedElement.map( + const currentTitle: Store = state.selectedElement.map( selected => { const layout = state.layoutToUse const defaultTitle = layout?.title?.txt ?? "MapComplete" diff --git a/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts b/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts index 7ee072d403..d7b3768d1f 100644 --- a/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts +++ b/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts @@ -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) { diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index 87d4aedda7..ea9fa871e3 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -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; - public readonly runningQuery: UIEventSource; + public readonly sufficientlyZoomed: Store; + public readonly runningQuery: Store; public readonly timeout: UIEventSource; public readonly somethingLoaded: UIEventSource = new UIEventSource(false) public readonly newDataLoadedSignal: UIEventSource = new UIEventSource(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): UIEventSource { + private getNeededTilesFromOsm(isSufficientlyZoomed: Store): Store { 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, - locationControl: UIEventSource, - readonly overpassUrl: UIEventSource; - readonly overpassTimeout: UIEventSource; - readonly overpassMaxZoom: UIEventSource, - }, useOsmApi: UIEventSource): OverpassFeatureSource { + currentBounds: Store, + locationControl: Store, + readonly overpassUrl: Store; + readonly overpassTimeout: Store; + readonly overpassMaxZoom: Store, + }, useOsmApi: Store): OverpassFeatureSource { const minzoom = Math.min(...state.layoutToUse.layers.map(layer => layer.minzoom)) const overpassIsActive = state.currentBounds.map(bbox => { if (bbox === undefined) { diff --git a/Logic/FeatureSource/FeatureSource.ts b/Logic/FeatureSource/FeatureSource.ts index df8b564128..a686377fc0 100644 --- a/Logic/FeatureSource/FeatureSource.ts +++ b/Logic/FeatureSource/FeatureSource.ts @@ -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> + readonly containedIds: Store> } /** * A feature source which has some extra data about it's state */ export interface FeatureSourceState { - readonly sufficientlyZoomed: UIEventSource; - readonly runningQuery: UIEventSource; - readonly timeout: UIEventSource; + readonly sufficientlyZoomed: Store; + readonly runningQuery: Store; + readonly timeout: Store; } diff --git a/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts b/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts index cfa15af1f5..f19e917c67 100644 --- a/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts +++ b/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts @@ -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, + constructor(layers: Store, handleLayerData: (source: FeatureSourceForLayer & Tiled) => void, upstream: FeatureSource, options?: { @@ -19,7 +19,7 @@ export default class PerLayerFeatureSourceSplitter { handleLeftovers?: (featuresWithoutLayer: any[]) => void }) { - const knownLayers = new Map() + const knownLayers = new Map() function update() { const features = upstream.features?.data; diff --git a/Logic/FeatureSource/Sources/RememberingSource.ts b/Logic/FeatureSource/Sources/RememberingSource.ts index c9a5e97e4b..7f32a13246 100644 --- a/Logic/FeatureSource/Sources/RememberingSource.ts +++ b/Logic/FeatureSource/Sources/RememberingSource.ts @@ -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 diff --git a/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts b/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts index 1f5d8cbb41..368b903950 100644 --- a/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts +++ b/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts @@ -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,59 +48,64 @@ 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 { - // This is a a line: add the centroids - let centerpoint: [number, number] = undefined; - let projectedCenterPoint : [number, number] = undefined - if(hasCentroid){ - centerpoint = GeoOperations.centerpointCoordinates(feat) - if(projectedCentroidRenderings.length > 0){ - projectedCenterPoint = <[number,number]> GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates - } + continue + } + + // This is a a line: add the centroids + let centerpoint: [number, number] = undefined; + let projectedCenterPoint: [number, number] = undefined + if (hasCentroid) { + centerpoint = GeoOperations.centerpointCoordinates(feat) + if (projectedCentroidRenderings.length > 0) { + projectedCenterPoint = <[number, number]>GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates } - for (const rendering of centroidRenderings) { + } + for (const rendering of centroidRenderings) { + addAsPoint(feat, rendering, centerpoint) + } + + + if (feat.geometry.type === "LineString") { + + for (const rendering of projectedCentroidRenderings) { + addAsPoint(feat, rendering, projectedCenterPoint) + } + + // Add start- and endpoints + const coordinates = feat.geometry.coordinates + for (const rendering of startRenderings) { + addAsPoint(feat, rendering, coordinates[0]) + } + for (const rendering of endRenderings) { + const coordinate = coordinates[coordinates.length - 1] + addAsPoint(feat, rendering, coordinate) + } + + } else { + for (const rendering of projectedCentroidRenderings) { addAsPoint(feat, rendering, centerpoint) } - - - if (feat.geometry.type === "LineString") { - - for (const rendering of projectedCentroidRenderings) { - addAsPoint(feat, rendering, projectedCenterPoint) - } - - // Add start- and endpoints - const coordinates = feat.geometry.coordinates - for (const rendering of startRenderings) { - addAsPoint(feat, rendering, coordinates[0]) - } - for (const rendering of endRenderings) { - const coordinate = coordinates[coordinates.length - 1] - addAsPoint(feat, rendering, coordinate) - } - - }else{ - for (const rendering of projectedCentroidRenderings) { - addAsPoint(feat, rendering, centerpoint) - } - } - - // AT last, add it 'as is' to what we should render - for (let i = 0; i < lineRenderObjects.length; i++) { - withIndex.push({ - ...feat, - lineRenderingIndex: i - }) - } + } + // AT last, add it 'as is' to what we should render + for (let i = 0; i < lineRenderObjects.length; i++) { + withIndex.push({ + ...feat, + lineRenderingIndex: i + }) } } diff --git a/Logic/FeatureSource/Sources/SimpleFeatureSource.ts b/Logic/FeatureSource/Sources/SimpleFeatureSource.ts index 52007d2f5b..63937763ed 100644 --- a/Logic/FeatureSource/Sources/SimpleFeatureSource.ts +++ b/Logic/FeatureSource/Sources/SimpleFeatureSource.ts @@ -10,7 +10,7 @@ export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled public readonly bbox: BBox = BBox.global; public readonly tileIndex: number; - constructor(layer: FilteredLayer, tileIndex: number, featureSource?: UIEventSource<{ feature: any; freshness: Date }[]>) { + constructor(layer: FilteredLayer, tileIndex: number, featureSource?: UIEventSource<{ feature: any; freshness: Date }[]> ) { this.name = "SimpleFeatureSource(" + layer.layerDef.id + ")" this.layer = layer this.tileIndex = tileIndex ?? 0; diff --git a/Logic/FeatureSource/Sources/StaticFeatureSource.ts b/Logic/FeatureSource/Sources/StaticFeatureSource.ts index 2d8aef37df..fac0243a90 100644 --- a/Logic/FeatureSource/Sources/StaticFeatureSource.ts +++ b/Logic/FeatureSource/Sources/StaticFeatureSource.ts @@ -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>, useFeaturesDirectly) { - const now = new Date(); - if(features === undefined){ + 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, - freshness: now - }))??[]) - } + this.name = name; + this.features = features; + } + + public static fromGeojsonAndDate(features: { feature: any, freshness: Date }[], name = "StaticFeatureSourceFromGeojsonAndDate"): StaticFeatureSource { + return new StaticFeatureSource(new ImmutableStore(features), name); } -} \ No newline at end of file + 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) + } + + +} diff --git a/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts b/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts index 5eaa8f84d3..4752a49199 100644 --- a/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts @@ -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() public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = [] private readonly _backend: string; - private readonly filteredLayers: UIEventSource; + private readonly filteredLayers: Store; private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void; - private isActive: UIEventSource; + private isActive: Store; private options: { handleTile: (tile: FeatureSourceForLayer & Tiled) => void; - isActive: UIEventSource, - neededTiles: UIEventSource, + isActive: Store, + neededTiles: Store, state: { readonly osmConnection: OsmConnection; }, @@ -36,8 +36,8 @@ export default class OsmFeatureSource { constructor(options: { handleTile: (tile: FeatureSourceForLayer & Tiled) => void; - isActive: UIEventSource, - neededTiles: UIEventSource, + isActive: Store, + neededTiles: Store, state: { readonly filteredLayers: UIEventSource; 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 } diff --git a/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts b/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts index 26e3694cff..be0f78e7b3 100644 --- a/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts @@ -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> + public readonly containedIds: Store> public readonly bbox: BBox; public readonly tileIndex: number; diff --git a/Logic/ImageProviders/AllImageProviders.ts b/Logic/ImageProviders/AllImageProviders.ts index 13881a177f..413e513757 100644 --- a/Logic/ImageProviders/AllImageProviders.ts +++ b/Logic/ImageProviders/AllImageProviders.ts @@ -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> = new Map>() - public static LoadImagesFor(tags: UIEventSource, tagKey?: string[]): UIEventSource { + public static LoadImagesFor(tags: Store, tagKey?: string[]): Store { if (tags.data.id === undefined) { return undefined; } diff --git a/Logic/ImageProviders/ImageProvider.ts b/Logic/ImageProviders/ImageProvider.ts index dded72a0b1..ef85b906fe 100644 --- a/Logic/ImageProviders/ImageProvider.ts +++ b/Logic/ImageProviders/ImageProvider.ts @@ -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>() + private _cache = new Map>() - GetAttributionFor(url: string): UIEventSource { + GetAttributionFor(url: string): Store { 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, options?: { + public GetRelevantUrls(allTags: Store, options?: { prefixes?: string[] }): UIEventSource { const prefixes = options?.prefixes ?? this.defaultKeyPrefixes diff --git a/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts b/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts index 95911f432f..273b511da5 100644 --- a/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts +++ b/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts @@ -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 { diff --git a/Logic/Osm/Actions/ReplaceGeometryAction.ts b/Logic/Osm/Actions/ReplaceGeometryAction.ts index f642b2d042..0e0fa84e73 100644 --- a/Logic/Osm/Actions/ReplaceGeometryAction.ts +++ b/Logic/Osm/Actions/ReplaceGeometryAction.ts @@ -159,7 +159,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { }) - return new StaticFeatureSource(Utils.NoNull(preview), false) + return StaticFeatureSource.fromGeojson(Utils.NoNull(preview)) } diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index 17b93b9ae9..777ed5caea 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -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)) { diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index aaa8f92cfb..5eca8976dc 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -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 { 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 { 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(); diff --git a/Logic/Osm/OsmObject.ts b/Logic/Osm/OsmObject.ts index f0ab35407a..8655a8a686 100644 --- a/Logic/Osm/OsmObject.ts +++ b/Logic/Osm/OsmObject.ts @@ -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 { + public static DownloadObject(id: string, forceRefresh: boolean = false): Store { let src: UIEventSource; if (OsmObject.objectCache.has(id)) { src = OsmObject.objectCache.get(id) diff --git a/Logic/Osm/Overpass.ts b/Logic/Osm/Overpass.ts index d0e6f9c36a..889d6bae22 100644 --- a/Logic/Osm/Overpass.ts +++ b/Logic/Osm/Overpass.ts @@ -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; + private readonly _timeout: Store; 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, + timeout?: Store, relationTracker?: RelationsTracker, includeMeta = true) { - this._timeout = timeout ?? new UIEventSource(90); + this._timeout = timeout ?? new ImmutableStore(90); this._interpreterUrl = interpreterUrl; const optimized = filter.optimize() if(optimized === true || optimized === false){ diff --git a/Logic/State/FeatureSwitchState.ts b/Logic/State/FeatureSwitchState.ts index 787d6a4eed..b14c2fd760 100644 --- a/Logic/State/FeatureSwitchState.ts +++ b/Logic/State/FeatureSwitchState.ts @@ -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, diff --git a/Logic/State/MapState.ts b/Logic/State/MapState.ts index 32aba550d6..5a2564c069 100644 --- a/Logic/State/MapState.ts +++ b/Logic/State/MapState.ts @@ -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; + public availableBackgroundLayers: Store; /** * 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 { const pref = this.osmConnection .GetPreference(key) - .map(v => { + .sync(v => { if(v === undefined){ return undefined } diff --git a/Logic/State/UserRelatedState.ts b/Logic/State/UserRelatedState.ts index 959c32f6b8..6ec0b876fb 100644 --- a/Logic/State/UserRelatedState.ts +++ b/Logic/State/UserRelatedState.ts @@ -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; - public readonly isTranslator : UIEventSource; + public readonly isTranslator : Store; 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(";") diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index 31c9585b04..ad316af2ef 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -1,64 +1,10 @@ import {Utils} from "../Utils"; -export class UIEventSource { - - private static allSources: UIEventSource[] = 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[] { - 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(source: UIEventSource>, possibleSources?: UIEventSource[]): UIEventSource { - const sink = new UIEventSource(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 { +/** + * Various static utils + */ +export class Stores { + public static Chronic(millis: number, asLong: () => boolean = undefined): Store { const source = new UIEventSource(undefined); function run() { @@ -72,17 +18,8 @@ export class UIEventSource { 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(promise: Promise): UIEventSource { - const src = new UIEventSource(undefined) - promise?.then(d => src.setData(d)) - promise?.catch(err => console.warn("Promise failed:", err)) - return src + public static FromPromiseWithErr(promise: Promise): Store<{ success: T } | { error: any }>{ + return UIEventSource.FromPromiseWithErr(promise); } /** @@ -91,13 +28,17 @@ export class UIEventSource { * @param promise * @constructor */ - public static FromPromiseWithErr(promise: Promise): 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(promise: Promise): Store { + const src = new UIEventSource(undefined) + promise?.then(d => src.setData(d)) + promise?.catch(err => console.warn("Promise failed:", err)) return src } + public static flatten(source: Store>, possibleSources?: Store[]): Store { + 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 { * @param src * @constructor */ - public static ListStabilized(src: UIEventSource): UIEventSource { + public static ListStabilized(src: Store): Store { const stable = new UIEventSource(src.data) src.addCallback(list => { @@ -141,46 +82,58 @@ export class UIEventSource { }) return stable } +} - public static asFloat(source: UIEventSource): UIEventSource { - 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); +export abstract class Store { + 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" } - ) - } - - public AsPromise(condition?: ((t: T )=> boolean)): Promise { - 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 - }) + if(createStack) { + const callstack = new Error().stack.split("\n") + this.tag = callstack[1] } - }) + } } - public WaitForPromise(promise: Promise, onFail: ((any) => void)): UIEventSource { - const self = this; - promise?.then(d => self.setData(d)) - promise?.catch(err => onFail(err)) - return this - } + abstract map(f: ((t: T) => J)): Store + abstract map(f: ((t: T) => J), extraStoresToWatch: Store[]): Store - public withEqualityStabilized(comparator: (t: T | undefined, t1: T | undefined) => boolean): UIEventSource { + /** + * 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 { let oldValue = undefined; return this.map(v => { if (v == oldValue) { @@ -194,6 +147,205 @@ export class UIEventSource { }) } + /** + * Monadic bind function + */ + public bind(f: ((t: T) => Store)): Store { + const mapped = this.map(f) + const sink = new UIEventSource(undefined) + const seenEventSources = new Set>(); + 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 { + if (Utils.runningFromConsole) { + return this; + } + + const newSource = new UIEventSource(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 { + 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 extends Store { + 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(f: (t: T) => J): ImmutableStore { + return new ImmutableStore(f(this.data)); + } + +} + + +export class UIEventSource extends Store { + + private static allSources: UIEventSource[] = 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[] { + 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(source: Store>, possibleSources?: Store[]): UIEventSource { + const sink = new UIEventSource(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(promise: Promise, onError :( (e: any) => void) = undefined): UIEventSource { + const src = new UIEventSource(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(promise: Promise): 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): UIEventSource { + 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 { // 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 { } /** - * 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(f: ((t: T) => UIEventSource)): UIEventSource { - const mapped = this.map(f) - const sink = new UIEventSource(undefined) - const seenEventSources = new Set>(); - 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(f: ((t: T) => J), + extraSources: Store[] = []): Store { + const self = this; - return sink; + const stack = new Error().stack.split("\n"); + const callee = stack[1] + + const newSource = new UIEventSource( + 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(f: ((t: T) => J), - extraSources: UIEventSource[] = [], - g: ((j: J, t: T) => T) = undefined, + public sync(f: ((t: T) => J), + extraSources: Store[], + g: ((j: J, t: T) => T) , allowUnregister = false): UIEventSource { const self = this; @@ -328,7 +480,7 @@ export class UIEventSource { const self = this; otherSource.addCallback((latest) => self.setData(latest)); if (reverseOverride) { - if(otherSource.data !== undefined){ + if (otherSource.data !== undefined) { this.setData(otherSource.data); } } else if (this.data === undefined) { @@ -339,27 +491,6 @@ export class UIEventSource { return this; } - public stabilized(millisToStabilize): UIEventSource { - if (Utils.runningFromConsole) { - return this; - } - - const newSource = new UIEventSource(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 { } }) } + } \ No newline at end of file diff --git a/Logic/Web/LocalStorageSource.ts b/Logic/Web/LocalStorageSource.ts index 8f43969c23..2de23d4139 100644 --- a/Logic/Web/LocalStorageSource.ts +++ b/Logic/Web/LocalStorageSource.ts @@ -6,7 +6,7 @@ import {UIEventSource} from "../UIEventSource"; export class LocalStorageSource { static GetParsed(key: string, defaultValue: T): UIEventSource { - return LocalStorageSource.Get(key).map( + return LocalStorageSource.Get(key).sync( str => { if (str === undefined) { return defaultValue diff --git a/Logic/Web/QueryParameters.ts b/Logic/Web/QueryParameters.ts index 02a76dce5a..de90d94a42 100644 --- a/Logic/Web/QueryParameters.ts +++ b/Logic/Web/QueryParameters.ts @@ -33,7 +33,7 @@ export class QueryParameters { } public static GetBooleanQueryParameter(key: string, deflt: boolean, documentation?: string): UIEventSource { - return QueryParameters.GetQueryParameter(key, ""+ deflt, documentation).map(str => str === "true", [], b => "" + b) + return QueryParameters.GetQueryParameter(key, ""+ deflt, documentation).sync(str => str === "true", [], b => "" + b) } diff --git a/Logic/Web/Review.ts b/Logic/Web/Review.ts index 777330e449..4fe467b915 100644 --- a/Logic/Web/Review.ts +++ b/Logic/Web/Review.ts @@ -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 + made_by_user: Store } \ No newline at end of file diff --git a/Models/Denomination.ts b/Models/Denomination.ts index 45f2e5be26..e287e08846 100644 --- a/Models/Denomination.ts +++ b/Models/Denomination.ts @@ -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): BaseUIElement { + getToggledHuman(isSingular: Store): BaseUIElement { if (this._humanSingular === undefined) { return this.human } diff --git a/Models/ThemeConfig/FilterConfig.ts b/Models/ThemeConfig/FilterConfig.ts index a76a67a233..d226ef729e 100644 --- a/Models/ThemeConfig/FilterConfig.ts +++ b/Models/ThemeConfig/FilterConfig.ts @@ -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") { diff --git a/UI/AutomatonGui.ts b/UI/AutomatonGui.ts index bc69b44769..ead3303ab0 100644 --- a/UI/AutomatonGui.ts +++ b/UI/AutomatonGui.ts @@ -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, - openChangeset: UIEventSource, 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; diff --git a/UI/Base/AsyncLazy.ts b/UI/Base/AsyncLazy.ts index 5d1eed9a96..04b2e3166d 100644 --- a/UI/Base/AsyncLazy.ts +++ b/UI/Base/AsyncLazy.ts @@ -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() } diff --git a/UI/Base/Link.ts b/UI/Base/Link.ts index d336793126..9b640c1b1b 100644 --- a/UI/Base/Link.ts +++ b/UI/Base/Link.ts @@ -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; + private readonly _href: string | Store; private readonly _embeddedShow: BaseUIElement; private readonly _newTab: boolean; - constructor(embeddedShow: BaseUIElement | string, href: string | UIEventSource, newTab: boolean = false) { + constructor(embeddedShow: BaseUIElement | string, href: string | Store, newTab: boolean = false) { super(); this._embeddedShow = Translations.W(embeddedShow); this._href = href; diff --git a/UI/Base/SubtleButton.ts b/UI/Base/SubtleButton.ts index 64b18958f8..62a05b04d3 100644 --- a/UI/Base/SubtleButton.ts +++ b/UI/Base/SubtleButton.ts @@ -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; newTab?: boolean ; imgSize?: string}; + private readonly options: { url?: string | Store; newTab?: boolean ; imgSize?: string}; constructor(imageUrl: string | BaseUIElement, message: string | BaseUIElement, options: { - url?: string | UIEventSource, + url?: string | Store, newTab?: boolean, imgSize?: "h-11 w-11" | string } = undefined) { diff --git a/UI/Base/VariableUIElement.ts b/UI/Base/VariableUIElement.ts index 13f147a4cf..1dbbe3ded5 100644 --- a/UI/Base/VariableUIElement.ts +++ b/UI/Base/VariableUIElement.ts @@ -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; + private readonly _contents: Store; - constructor(contents: UIEventSource) { + constructor(contents: Store) { super(); this._contents = contents; } diff --git a/UI/BigComponents/FilterView.ts b/UI/BigComponents/FilterView.ts index 01f4ff29ff..4eb75f2714 100644 --- a/UI/BigComponents/FilterView.ts +++ b/UI/BigComponents/FilterView.ts @@ -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, @@ -180,7 +179,8 @@ export default class FilterView extends VariableUiElement { const filter = filterConfig.options[0] const mappings = new Map() - let allValid = new UIEventSource(true) + let allValid: Store = new ImmutableStore(true) + var allFields: InputElement[] = [] const properties = new UIEventSource({}) 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({id: filterConfig.id}), State.state, mappings) - const trigger: UIEventSource = allValid.map(isValid => { + const trigger: Store = allValid.map(isValid => { if (!isValid) { return undefined } @@ -221,8 +222,16 @@ export default class FilterView extends VariableUiElement { state: JSON.stringify(props) } }, [properties]) + + const settableFilter = new UIEventSource(undefined) + trigger.addCallbackAndRun(state => settableFilter.setData(state)) + settableFilter.addCallback(state => { + if(state.currentFilter === undefined){ + allFields.forEach(f => f.GetValue().setData(undefined)); + } + }) - return [tr, trigger]; + return [tr, settableFilter]; } private static createCheckboxFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource] { @@ -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 => { diff --git a/UI/BigComponents/Histogram.ts b/UI/BigComponents/Histogram.ts index ba1387dddf..7b99564963 100644 --- a/UI/BigComponents/Histogram.ts +++ b/UI/BigComponents/Histogram.ts @@ -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 extends VariableUiElement { "#fa61fa" ] - constructor(values: UIEventSource, + constructor(values: Store, title: string | BaseUIElement, countTitle: string | BaseUIElement, options?: { diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index 6d5592783e..61e8869848 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -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 { + ): Store { 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(`${linkPrefix}`) + }) ?? new ImmutableStore(`${linkPrefix}`) } @@ -237,7 +237,7 @@ export default class MoreScreen extends Combine { private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState, themeListClasses: string, search: UIEventSource): BaseUIElement { const prefix = "mapcomplete-unofficial-theme-"; - var currentIds: UIEventSource = state.osmConnection.preferencesHandler.preferences + var currentIds: Store = 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(currentIds) + var stableIds = Stores.ListStabilized(currentIds) return new VariableUiElement( stableIds.map(ids => { const allThemes: { element: BaseUIElement, predicate?: (s: string) => boolean }[] = [] diff --git a/UI/BigComponents/ShareScreen.ts b/UI/BigComponents/ShareScreen.ts index de96839bfd..e9fe42d8a3 100644 --- a/UI/BigComponents/ShareScreen.ts +++ b/UI/BigComponents/ShareScreen.ts @@ -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[] = [] - const optionParts: (UIEventSource)[] = []; + const optionParts: (Store)[] = []; const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true) optionCheckboxes.push(includeLocation); diff --git a/UI/BigComponents/TranslatorsPanel.ts b/UI/BigComponents/TranslatorsPanel.ts index 37e80eadb8..a858ab463a 100644 --- a/UI/BigComponents/TranslatorsPanel.ts +++ b/UI/BigComponents/TranslatorsPanel.ts @@ -124,7 +124,7 @@ export default class TranslatorsPanel extends Toggle { const completeness = new Map() const untranslated = new Map() - Utils.WalkObject(layout, (o, path) => { + Utils.WalkObject(layout, (o) => { const translation = o; if (translation.translations["*"] !== undefined) { return diff --git a/UI/Image/Attribution.ts b/UI/Image/Attribution.ts index 98f72950f8..8d97c3649a 100644 --- a/UI/Image/Attribution.ts +++ b/UI/Image/Attribution.ts @@ -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, icon: BaseUIElement, date?: Date) { + constructor(license: Store, icon: BaseUIElement, date?: Date) { if (license === undefined) { throw "No license source given in the attribution element" } diff --git a/UI/Image/DeleteImage.ts b/UI/Image/DeleteImage.ts index 3f16902446..183762d20b 100644 --- a/UI/Image/DeleteImage.ts +++ b/UI/Image/DeleteImage.ts @@ -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, state: { layoutToUse: LayoutConfig, changes?: Changes, osmConnection?: OsmConnection }) { + constructor(key: string, tags: Store, 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 diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index 92a7136fb8..5c1d6e8299 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -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, + constructor(images: Store<{ key: string, url: string, provider: ImageProvider }[]>, + tags: Store, state: { osmConnection?: OsmConnection, changes?: Changes, layoutToUse: LayoutConfig }) { const uiElements = images.map((imageURLS: { key: string, url: string, provider: ImageProvider }[]) => { const uiElements: BaseUIElement[] = []; diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index 807e5464ce..fe1f094f68 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -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>() - constructor(tagsSource: UIEventSource, + constructor(tagsSource: Store, state: { osmConnection: OsmConnection; layoutToUse: LayoutConfig; changes: Changes, - featureSwitchUserbadge: UIEventSource; + featureSwitchUserbadge: Store; }, imagePrefix: string = "image", text: string = undefined) { const perId = ImageUploadFlow.uploadCountsPerId diff --git a/UI/Image/SlideShow.ts b/UI/Image/SlideShow.ts index 9d37036fe5..aa19056c1a 100644 --- a/UI/Image/SlideShow.ts +++ b/UI/Image/SlideShow.ts @@ -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; + private readonly embeddedElements: Store; - constructor(embeddedElements: UIEventSource) { + constructor(embeddedElements: Store) { super() this.embeddedElements = embeddedElements; this.SetStyle("scroll-snap-type: x mandatory; overflow-x: auto") diff --git a/UI/ImportFlow/AskMetadata.ts b/UI/ImportFlow/AskMetadata.ts index 125b43ed33..6584fc0656 100644 --- a/UI/ImportFlow/AskMetadata.ts +++ b/UI/ImportFlow/AskMetadata.ts @@ -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; + public readonly IsValid: Store; constructor(params: ({ features: any[], theme: string })) { const t = Translations.t.importHelper.askMetadata diff --git a/UI/ImportFlow/CompareToAlreadyExistingNotes.ts b/UI/ImportFlow/CompareToAlreadyExistingNotes.ts index eb0d40f710..3be0f1991f 100644 --- a/UI/ImportFlow/CompareToAlreadyExistingNotes.ts +++ b/UI/ImportFlow/CompareToAlreadyExistingNotes.ts @@ -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 - public Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, features: any[], theme: string }> + public IsValid: Store + 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) diff --git a/UI/ImportFlow/ConfirmProcess.ts b/UI/ImportFlow/ConfirmProcess.ts index c8e3f570e8..580ef7ed05 100644 --- a/UI/ImportFlow/ConfirmProcess.ts +++ b/UI/ImportFlow/ConfirmProcess.ts @@ -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 - public Value: UIEventSource<{ features: any[], theme: string }> + public IsValid: Store + public Value: Store<{ features: any[], theme: string }> constructor(v: { features: any[], theme: string }) { const t = Translations.t.importHelper.confirmProcess; diff --git a/UI/ImportFlow/ConflationChecker.ts b/UI/ImportFlow/ConflationChecker.ts index 3ae2103f8b..250da0ea93 100644 --- a/UI/ImportFlow/ConflationChecker.ts +++ b/UI/ImportFlow/ConflationChecker.ts @@ -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 = fromLocalStorage.map(d => { + const geojson: Store = 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, diff --git a/UI/ImportFlow/FlowStep.ts b/UI/ImportFlow/FlowStep.ts index 694207bcb6..fc9b4b3987 100644 --- a/UI/ImportFlow/FlowStep.ts +++ b/UI/ImportFlow/FlowStep.ts @@ -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 extends BaseUIElement { - readonly IsValid: UIEventSource - readonly Value: UIEventSource + readonly IsValid: Store + readonly Value: Store } export class FlowPanelFactory { diff --git a/UI/ImportFlow/ImportHelperGui.ts b/UI/ImportFlow/ImportHelperGui.ts index 000cae3528..d703de52a1 100644 --- a/UI/ImportFlow/ImportHelperGui.ts +++ b/UI/ImportFlow/ImportHelperGui.ts @@ -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"; diff --git a/UI/ImportFlow/ImportUtils.ts b/UI/ImportFlow/ImportUtils.ts index db881a7097..5ce200fff8 100644 --- a/UI/ImportFlow/ImportUtils.ts +++ b/UI/ImportFlow/ImportUtils.ts @@ -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): UIEventSource<{ hasNearby: any[], noNearby: any[] }> { + public static partitionFeaturesIfNearby(toPartitionFeatureCollection: ({ features: any[] }), compareWith: Store<{ features: any[] }>, cutoffDistanceInMeters: Store): Store<{ hasNearby: any[], noNearby: any[] }> { return compareWith.map(osmData => { if (osmData?.features === undefined) { return undefined diff --git a/UI/ImportFlow/ImportViewerGui.ts b/UI/ImportFlow/ImportViewerGui.ts index 9ac79ebdbf..7c95596899 100644 --- a/UI/ImportFlow/ImportViewerGui.ts +++ b/UI/ImportFlow/ImportViewerGui.ts @@ -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; } diff --git a/UI/ImportFlow/LoginToImport.ts b/UI/ImportFlow/LoginToImport.ts index 8a73042ceb..08db0e303a 100644 --- a/UI/ImportFlow/LoginToImport.ts +++ b/UI/ImportFlow/LoginToImport.ts @@ -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 { - readonly IsValid: UIEventSource; - readonly Value: UIEventSource; + readonly IsValid: Store; + readonly Value: Store; private static readonly whitelist = [15015689]; diff --git a/UI/ImportFlow/MapPreview.ts b/UI/ImportFlow/MapPreview.ts index 64f8cc511d..f4e8d666eb 100644 --- a/UI/ImportFlow/MapPreview.ts +++ b/UI/ImportFlow/MapPreview.ts @@ -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, layer) { + constructor(tags: UIEventSource) { 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; - public readonly Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, features: any[] }> + public readonly IsValid: Store; + 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>(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])))) diff --git a/UI/ImportFlow/PreviewPanel.ts b/UI/ImportFlow/PreviewPanel.ts index 8d54950049..69e18bd052 100644 --- a/UI/ImportFlow/PreviewPanel.ts +++ b/UI/ImportFlow/PreviewPanel.ts @@ -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; - public readonly Value: UIEventSource<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }> + public readonly IsValid: Store; + public readonly Value: Store<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }> constructor( state: UserRelatedState, diff --git a/UI/ImportFlow/RequestFile.ts b/UI/ImportFlow/RequestFile.ts index e435758470..ee1ae621b0 100644 --- a/UI/ImportFlow/RequestFile.ts +++ b/UI/ImportFlow/RequestFile.ts @@ -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 }> { @@ -38,11 +37,11 @@ class FileSelector extends InputElementMap { - public readonly IsValid: UIEventSource + public readonly IsValid: Store /** * 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 = text.map(src => { + const asGeoJson: Store = text.map((src: string) => { if (src === undefined) { return undefined } diff --git a/UI/ImportFlow/SelectTheme.ts b/UI/ImportFlow/SelectTheme.ts index 99339ab627..b443273d68 100644 --- a/UI/ImportFlow/SelectTheme.ts +++ b/UI/ImportFlow/SelectTheme.ts @@ -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; + public readonly IsValid: Store; constructor(params: ({ features: any[], layer: LayerConfig, bbox: BBox, })) { const t = Translations.t.importHelper.selectTheme diff --git a/UI/Input/InputElement.ts b/UI/Input/InputElement.ts index f8a27eddde..68df36c85e 100644 --- a/UI/Input/InputElement.ts +++ b/UI/Input/InputElement.ts @@ -1,11 +1,12 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; +import {Store, UIEventSource} from "../../Logic/UIEventSource"; import BaseUIElement from "../BaseUIElement"; -export abstract class InputElement extends BaseUIElement { - - - abstract GetValue(): UIEventSource; - - abstract IsValid(t: T): boolean; - +export interface ReadonlyInputElement extends BaseUIElement{ + GetValue(): Store; +} + + +export abstract class InputElement extends BaseUIElement implements ReadonlyInputElement{ + abstract GetValue(): UIEventSource; + abstract IsValid(t: T): boolean; } diff --git a/UI/Input/InputElementMap.ts b/UI/Input/InputElementMap.ts index 16b907f469..3cb1a60cc8 100644 --- a/UI/Input/InputElementMap.ts +++ b/UI/Input/InputElementMap.ts @@ -21,7 +21,7 @@ export default class InputElementMap extends InputElement { 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; diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts index 5743cd8513..60a03e677f 100644 --- a/UI/Input/LocationInput.ts +++ b/UI/Input/LocationInput.ts @@ -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 implements MinimapObj { +export default class LocationInput extends BaseUIElement implements ReadonlyInputElement, MinimapObj { private static readonly matchLayer = new LayerConfig(matchpoint, "LocationInput.matchpoint", true) - IsSelected: UIEventSource = new UIEventSource(false); public readonly snappedOnto: UIEventSource = new UIEventSource(undefined) public readonly _matching_layer: LayerConfig; public readonly leafletMap: UIEventSource @@ -33,9 +32,9 @@ export default class LocationInput extends InputElement implements MinimapO * The features to which the input should be snapped * @private */ - private readonly _snapTo: UIEventSource<{ feature: any }[]> - private readonly _value: UIEventSource - private readonly _snappedPoint: UIEventSource + private readonly _snapTo: Store<{ feature: any }[]> + private readonly _value: Store + private readonly _snappedPoint: Store private readonly _maxSnapDistance: number private readonly _snappedPointTags: any; private readonly _bounds: UIEventSource; @@ -151,7 +150,7 @@ export default class LocationInput extends InputElement implements MinimapO this.location = this.map.location; } - GetValue(): UIEventSource { + GetValue(): Store { return this._value; } @@ -188,7 +187,7 @@ export default class LocationInput extends InputElement 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 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, diff --git a/UI/Input/RadioButton.ts b/UI/Input/RadioButton.ts index 0ee275ffc8..c4ac7d38c0 100644 --- a/UI/Input/RadioButton.ts +++ b/UI/Input/RadioButton.ts @@ -152,7 +152,7 @@ export class RadioButton extends InputElement { 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]; diff --git a/UI/Input/Toggle.ts b/UI/Input/Toggle.ts index f4f78e2423..c9bc599675 100644 --- a/UI/Input/Toggle.ts +++ b/UI/Input/Toggle.ts @@ -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; + public readonly isEnabled: Store; - constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, isEnabled: UIEventSource = new UIEventSource(false)) { + constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, isEnabled: Store = new UIEventSource(false)) { super( isEnabled?.map(isEnabled => isEnabled ? showEnabled : showDisabled) ); this.isEnabled = isEnabled } - public static If(condition: UIEventSource, constructor: () => BaseUIElement): BaseUIElement { + public static If(condition: Store, constructor: () => BaseUIElement): BaseUIElement { if (constructor === undefined) { return undefined } @@ -29,8 +29,24 @@ 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; + + constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, isEnabled: UIEventSource = new UIEventSource(false)) { + super( + showEnabled, showDisabled, isEnabled + ); + this.isEnabled = isEnabled + } + + public ToggleOnClick(): ClickableToggle { const self = this; this.onClick(() => { self.isEnabled.setData(!self.isEnabled.data); diff --git a/UI/Input/VariableInputElement.ts b/UI/Input/VariableInputElement.ts index e9a6aa3d97..d4de12af60 100644 --- a/UI/Input/VariableInputElement.ts +++ b/UI/Input/VariableInputElement.ts @@ -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 extends InputElement { +export default class VariableInputElement extends BaseUIElement implements ReadonlyInputElement { - private readonly value: UIEventSource; + private readonly value: Store; private readonly element: BaseUIElement - private readonly upstream: UIEventSource>; - - constructor(upstream: UIEventSource>) { + private readonly upstream: Store>; + constructor(upstream: Store>) { super() this.upstream = upstream; this.value = upstream.bind(v => v.GetValue()) this.element = new VariableUiElement(upstream) } - GetValue(): UIEventSource { + GetValue(): Store { return this.value; } diff --git a/UI/OpeningHours/OpeningHours.ts b/UI/OpeningHours/OpeningHours.ts index 272fa3b247..51a7d697b4 100644 --- a/UI/OpeningHours/OpeningHours.ts +++ b/UI/OpeningHours/OpeningHours.ts @@ -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 [] diff --git a/UI/OpeningHours/OpeningHoursInput.ts b/UI/OpeningHours/OpeningHoursInput.ts index 50aa52cf47..d46b318530 100644 --- a/UI/OpeningHours/OpeningHoursInput.ts +++ b/UI/OpeningHours/OpeningHoursInput.ts @@ -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 { 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 { }) } - const leftoverRules = valueWithoutPrefix.map(str => { + const leftoverRules: Store = valueWithoutPrefix.map(str => { if (str === undefined) { return [] } @@ -72,35 +70,40 @@ export default class OpeningHoursInput extends InputElement { } return leftOvers; }) - // Note: MUST be bound AFTER the leftover rules! - const rulesFromOhPicker = valueWithoutPrefix.map(OH.Parse); - - const ph = valueWithoutPrefix.map(str => { - if (str === undefined) { - return "" + + let ph = ""; + const rules = valueWithoutPrefix.data?.split(";") ?? []; + for (const rule of rules) { + if (OH.ParsePHRule(rule) !== null) { + ph = rule; + break; } - const rules = str.split(";"); - for (const rule of rules) { - if (OH.ParsePHRule(rule) !== null) { - return rule; - } - } - return ""; - }) - const phSelector = new PublicHolidayInput(ph); - - function update() { - const regular = OH.ToString(rulesFromOhPicker.data); - const rules: string[] = [ - regular, - ...leftoverRules.data, - ph.data - ] - valueWithoutPrefix.setData(Utils.NoEmpty(rules).join(";")); } + const phSelector = new PublicHolidayInput(new UIEventSource(ph)); + + + // Note: MUST be bound AFTER the leftover rules! + const rulesFromOhPicker: UIEventSource = 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 + } + + 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; + }); - rulesFromOhPicker.addCallback(update); - ph.addCallback(update); const leftoverWarning = new VariableUiElement(leftoverRules.map((leftovers: string[]) => { diff --git a/UI/Popup/AutoApplyButton.ts b/UI/Popup/AutoApplyButton.ts index 0956a56c53..8c1e00f7a2 100644 --- a/UI/Popup/AutoApplyButton.ts +++ b/UI/Popup/AutoApplyButton.ts @@ -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) }) diff --git a/UI/Popup/DeleteWizard.ts b/UI/Popup/DeleteWizard.ts index b0ce39556c..373a22dc19 100644 --- a/UI/Popup/DeleteWizard.ts +++ b/UI/Popup/DeleteWizard.ts @@ -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 = tagsSource.map(tgs => tgs.id.indexOf("-") < 0) + const isShown: Store = 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; diff --git a/UI/Popup/ImportButton.ts b/UI/Popup/ImportButton.ts index 8db42618de..0c0e189bd1 100644 --- a/UI/Popup/ImportButton.ts +++ b/UI/Popup/ImportButton.ts @@ -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 }) diff --git a/UI/Popup/MultiApply.ts b/UI/Popup/MultiApply.ts index 0eeff2db70..2b31490278 100644 --- a/UI/Popup/MultiApply.ts +++ b/UI/Popup/MultiApply.ts @@ -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, + featureIds: Store, keysToApply: string[], text: string, autoapply: boolean, overwrite: boolean, - tagsSource: UIEventSource, + tagsSource: Store, state: { changes: Changes, allElements: ElementStorage, @@ -145,7 +145,7 @@ export default class MultiApply extends Toggle { } - const isShown: UIEventSource = p.state.osmConnection.isLoggedIn.map(loggedIn => { + const isShown: Store = p.state.osmConnection.isLoggedIn.map(loggedIn => { return loggedIn && p.featureIds.data.length > 0 }, [p.featureIds]) super(new Combine(elems), undefined, isShown); diff --git a/UI/Popup/NearbyImages.ts b/UI/Popup/NearbyImages.ts index 7fbaa34e22..ab9c35fe64 100644 --- a/UI/Popup/NearbyImages.ts +++ b/UI/Popup/NearbyImages.ts @@ -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, towardscenter?: UIEventSource; allowSpherical?: UIEventSource @@ -173,7 +171,7 @@ export default class NearbyImages extends Lazy { const nearbyImages = state !== undefined ? new ImagesInLoadedDataFetcher(state).fetchAround(options) : [] - return UIEventSource.FromPromise( + return Stores.FromPromise( 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(); } diff --git a/UI/Popup/NoteCommentElement.ts b/UI/Popup/NoteCommentElement.ts index d0ba42d193..f0eb7cb327 100644 --- a/UI/Popup/NoteCommentElement.ts +++ b/UI/Popup/NoteCommentElement.ts @@ -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){ diff --git a/UI/Popup/QuestionBox.ts b/UI/Popup/QuestionBox.ts index fcc92ead41..2f94d92042 100644 --- a/UI/Popup/QuestionBox.ts +++ b/UI/Popup/QuestionBox.ts @@ -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; - public readonly restingQuestions: UIEventSource; + public readonly restingQuestions: Store; constructor(state, options: { tagsSource: UIEventSource, @@ -81,7 +81,7 @@ export default class QuestionBox extends VariableUiElement { return undefined; // The questions are depleted }, [skippedQuestions]); - const questionsToAsk: UIEventSource = tagsSource.map(tags => { + const questionsToAsk: Store = tagsSource.map(tags => { if (tags === undefined) { return []; } diff --git a/UI/Popup/SaveButton.ts b/UI/Popup/SaveButton.ts index 91c03e189e..278e4ddbbf 100644 --- a/UI/Popup/SaveButton.ts +++ b/UI/Popup/SaveButton.ts @@ -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, osmConnection: OsmConnection, textEnabled ?: BaseUIElement, textDisabled ?: BaseUIElement) { + constructor(value: Store, osmConnection: OsmConnection, textEnabled ?: BaseUIElement, textDisabled ?: BaseUIElement) { if (value === undefined) { throw "No event source for savebutton, something is wrong" } diff --git a/UI/Popup/SplitRoadWizard.ts b/UI/Popup/SplitRoadWizard.ts index 51bf4ab6a4..53c7a62551 100644 --- a/UI/Popup/SplitRoadWizard.ts +++ b/UI/Popup/SplitRoadWizard.ts @@ -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, diff --git a/UI/Popup/TagApplyButton.ts b/UI/Popup/TagApplyButton.ts index 6bd8e774ba..0a654895b2 100644 --- a/UI/Popup/TagApplyButton.ts +++ b/UI/Popup/TagApplyButton.ts @@ -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): UIEventSource { + public static generateTagsToApply(spec: string, tagSource: Store): Store { const tgsSpec = spec.split(";").map(spec => { const kv = spec.split("=").map(s => s.trim()); diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index ffedccefc0..e5d7311470 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -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"; diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index abec9ec6cf..a3635d6540 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -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) => BaseUIElement, - bottomText?: (src: UIEventSource) => BaseUIElement + saveButtonConstr?: (src: Store) => BaseUIElement, + bottomText?: (src: Store) => BaseUIElement } ) { const applicableMappingsSrc = - UIEventSource.ListStabilized(tags.map(tags => { + Stores.ListStabilized(tags.map(tags => { const applicableMappings: { if: TagsFilter, icon?: string, then: TypedTranslation, 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(undefined) - const inputElement: InputElement = + const inputElement: ReadonlyInputElement = 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, - tags: UIEventSource, + public static CreateTagExplanation(selectedValue: Store, + tags: Store, state?: {osmConnection?: OsmConnection}){ return new VariableUiElement( selectedValue.map( diff --git a/UI/Reviews/ReviewForm.ts b/UI/Reviews/ReviewForm.ts index 7d8d7d46c1..0eff4736af 100644 --- a/UI/Reviews/ReviewForm.ts +++ b/UI/Reviews/ReviewForm.ts @@ -37,7 +37,6 @@ export default class ReviewForm extends InputElement { 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,10 +61,10 @@ export default class ReviewForm extends InputElement { new SaveButton( this._value.map(r => self.IsValid(r)), osmConnection ).onClick(() => { - reviewIsSaving.setData(true), - onSave(this._value.data, () => { - reviewIsSaved.setData(true) - }); + reviewIsSaving.setData(true); + onSave(this._value.data, () => { + reviewIsSaved.setData(true) + }); }), reviewIsSaving ), diff --git a/UI/ShowDataLayer/ShowDataLayerImplementation.ts b/UI/ShowDataLayer/ShowDataLayerImplementation.ts index 16b4293f89..4a2df70d8b 100644 --- a/UI/ShowDataLayer/ShowDataLayerImplementation.ts +++ b/UI/ShowDataLayer/ShowDataLayerImplementation.ts @@ -195,7 +195,7 @@ export default class ShowDataLayerImplementation { const tagsSource = this.allElements?.addOrGetElement(feat) ?? new UIEventSource(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 diff --git a/UI/ShowDataLayer/ShowDataLayerOptions.ts b/UI/ShowDataLayer/ShowDataLayerOptions.ts index a9ace1f8de..cd0dfd7f4b 100644 --- a/UI/ShowDataLayer/ShowDataLayerOptions.ts +++ b/UI/ShowDataLayer/ShowDataLayerOptions.ts @@ -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, popup?: undefined | ((tags: UIEventSource, layer: LayerConfig) => ScrollableFullScreen), zoomToFeatures?: false | boolean, - doShowLayer?: UIEventSource, + doShowLayer?: Store, state?: { allElements?: ElementStorage } } \ No newline at end of file diff --git a/UI/ShowDataLayer/ShowTileInfo.ts b/UI/ShowDataLayer/ShowTileInfo.ts index 7698de25a9..1a04624bca 100644 --- a/UI/ShowDataLayer/ShowTileInfo.ts +++ b/UI/ShowDataLayer/ShowTileInfo.ts @@ -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 = + 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, diff --git a/UI/ShowDataLayer/TileHierarchyAggregator.ts b/UI/ShowDataLayer/TileHierarchyAggregator.ts index 49007a78ab..a5f1f9d6c6 100644 --- a/UI/ShowDataLayer/TileHierarchyAggregator.ts +++ b/UI/ShowDataLayer/TileHierarchyAggregator.ts @@ -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() { diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 5f41c14332..2747927e53 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -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 = tagSource + const listSource: Store = 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 = tagsSource.map(tags => { + const featureIds: Store = tagsSource.map(tags => { const ids = tags[featureIdsKey] try { if (ids === undefined) { diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index dc969deeb7..4a9b37dc33 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -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"; diff --git a/UI/Wikipedia/WikidataPreviewBox.ts b/UI/Wikipedia/WikidataPreviewBox.ts index ebda722a60..ca997d42d6 100644 --- a/UI/Wikipedia/WikidataPreviewBox.ts +++ b/UI/Wikipedia/WikidataPreviewBox.ts @@ -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") diff --git a/UI/Wikipedia/WikidataSearchBox.ts b/UI/Wikipedia/WikidataSearchBox.ts index 87afd99054..ccbfcce7b9 100644 --- a/UI/Wikipedia/WikidataSearchBox.ts +++ b/UI/Wikipedia/WikidataSearchBox.ts @@ -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 { @@ -51,48 +52,55 @@ export default class WikidataSearchBox extends InputElement { }) const selectedWikidataId = this.wikidataId - const lastSearchResults = new UIEventSource([]) - const searchFailMessage = new UIEventSource(undefined) - searchField.GetValue().addCallbackAndRunD(searchText => { - if (searchText.length < 3) { - return; + const tooShort = new ImmutableStore<{success: WikidataResponse[]}>({success: undefined}) + const searchResult: Store<{success?: WikidataResponse[], error?: any}> = searchField.GetValue().bind( + searchText => { + if (searchText.length < 3) { + return tooShort; + } + const lang = Locale.language.data + const key = lang + ":" + searchText + let promise = WikidataSearchBox._searchCache.get(key) + if (promise === undefined) { + promise = Wikidata.searchAndFetch(searchText, { + lang, + maxCount: 5, + notInstanceOf: this.notInstanceOf, + instanceOf: this.instanceOf + } + ) + WikidataSearchBox._searchCache.set(key, promise) + } + return Stores.FromPromiseWithErr(promise) } - searchFailMessage.setData(undefined) + ) + - const lang = Locale.language.data - const key = lang + ":" + searchText - let promise = WikidataSearchBox._searchCache.get(key) - if (promise === undefined) { - promise = Wikidata.searchAndFetch(searchText, { - lang, - maxCount: 5, - notInstanceOf: this.notInstanceOf, - instanceOf: this.instanceOf - } - ) - 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]) - } + 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 { })).SetClass("flex flex-col") - }, [searchFailMessage])) + }, [searchField.GetValue()])) const full = new Combine([ new Title(Translations.t.general.wikipedia.searchWikidata, 3).SetClass("m-2"), diff --git a/UI/Wikipedia/WikipediaBox.ts b/UI/Wikipedia/WikipediaBox.ts index 0b65c18420..a3f3ad66c0 100644 --- a/UI/Wikipedia/WikipediaBox.ts +++ b/UI/Wikipedia/WikipediaBox.ts @@ -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) { diff --git a/assets/layers/maxspeed/maxspeed.json b/assets/layers/maxspeed/maxspeed.json index b41fa24761..de44ecf6a9 100644 --- a/assets/layers/maxspeed/maxspeed.json +++ b/assets/layers/maxspeed/maxspeed.json @@ -27,7 +27,7 @@ ] }, "type!=multipolygon", - "area!=yes" + "area!=yes" ] } }, @@ -70,7 +70,7 @@ "nl": "Dit is een woonerf en heeft dus een maximale snelheid van 20km/h" }, "icon": { - "path":"./assets/layers/maxspeed/living_street_be.svg", + "path": "./assets/layers/maxspeed/living_street_be.svg", "size": "large" }, "hideInAnswer": "_country!=be" diff --git a/assets/themes/mapcomplete-changes/mapcomplete-changes.json b/assets/themes/mapcomplete-changes/mapcomplete-changes.json index 0a04dea61c..c27f6ee95a 100644 --- a/assets/themes/mapcomplete-changes/mapcomplete-changes.json +++ b/assets/themes/mapcomplete-changes/mapcomplete-changes.json @@ -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" diff --git a/scripts/generateCache.ts b/scripts/generateCache.ts index d7d9706ed8..ce791b1899 100644 --- a/scripts/generateCache.ts +++ b/scripts/generateCache.ts @@ -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) } /** diff --git a/scripts/slice.ts b/scripts/slice.ts index 9507befbf7..365e239f9e 100644 --- a/scripts/slice.ts +++ b/scripts/slice.ts @@ -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, diff --git a/test/scripts/GenerateCache.spec.ts b/test/scripts/GenerateCache.spec.ts index 51558bcd8c..ea8acb4e58 100644 --- a/test/scripts/GenerateCache.spec.ts +++ b/test/scripts/GenerateCache.spec.ts @@ -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