Huge refactoring: split readonly and writable stores

This commit is contained in:
Pieter Vander Vennet 2022-06-05 02:24:14 +02:00
parent 0946d8ac9c
commit 4283b76f36
95 changed files with 819 additions and 625 deletions

View file

@ -1,14 +1,14 @@
import BaseLayer from "../../Models/BaseLayer";
import {UIEventSource} from "../UIEventSource";
import {ImmutableStore, Store, UIEventSource} from "../UIEventSource";
import Loc from "../../Models/Loc";
export interface AvailableBaseLayersObj {
readonly osmCarto: BaseLayer;
layerOverview: BaseLayer[];
AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]>
AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]>
SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer>;
SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: Store<string | string[]>): Store<BaseLayer>;
}
@ -24,12 +24,12 @@ export default class AvailableBaseLayers {
private static implementation: AvailableBaseLayersObj
static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
return AvailableBaseLayers.implementation?.AvailableLayersAt(location) ?? new UIEventSource<BaseLayer[]>([]);
static AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> {
return AvailableBaseLayers.implementation?.AvailableLayersAt(location) ?? new ImmutableStore<BaseLayer[]>([]);
}
static SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> {
return AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo(location, preferedCategory) ?? new UIEventSource<BaseLayer>(undefined);
static SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: UIEventSource<string | string[]>): Store<BaseLayer> {
return AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo(location, preferedCategory) ?? new ImmutableStore<BaseLayer>(undefined);
}

View file

@ -1,5 +1,5 @@
import BaseLayer from "../../Models/BaseLayer";
import {UIEventSource} from "../UIEventSource";
import {Store, Stores} from "../UIEventSource";
import Loc from "../../Models/Loc";
import {GeoOperations} from "../GeoOperations";
import * as editorlayerindex from "../../assets/editor-layer-index.json";
@ -29,7 +29,7 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
public readonly layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat(AvailableBaseLayersImplementation.LoadProviderIndex());
public readonly globalLayers = this.layerOverview.filter(layer => layer.feature?.geometry === undefined || layer.feature?.geometry === null)
public readonly localLayers = this.layerOverview.filter(layer => layer.feature?.geometry !== undefined && layer.featuer?.geometry !== null)
public readonly localLayers = this.layerOverview.filter(layer => layer.feature?.geometry !== undefined && layer.feature?.geometry !== null)
private static LoadRasterIndex(): BaseLayer[] {
const layers: BaseLayer[] = []
@ -202,8 +202,8 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
});
}
public AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
return UIEventSource.ListStabilized(location.map(
public AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> {
return Stores.ListStabilized(location.map(
(currentLocation) => {
if (currentLocation === undefined) {
return this.layerOverview;
@ -212,7 +212,7 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
}));
}
public SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> {
public SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: Store<string | string[]>): Store<BaseLayer> {
return this.AvailableLayersAt(location)
.map(available => {
// First float all 'best layers' to the top
@ -264,7 +264,7 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
if (lon === undefined || lat === undefined) {
return availableLayers.concat(this.globalLayers);
}
const lonlat = [lon, lat];
const lonlat : [number, number] = [lon, lat];
for (const layerOverviewItem of this.localLayers) {
const layer = layerOverviewItem;
const bbox = BBox.get(layer.feature)

View file

@ -1,12 +1,12 @@
import {UIEventSource} from "../UIEventSource";
import {Store, UIEventSource} from "../UIEventSource";
import Svg from "../../Svg";
import {LocalStorageSource} from "../Web/LocalStorageSource";
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {QueryParameters} from "../Web/QueryParameters";
import FeatureSource from "../FeatureSource/FeatureSource";
import {BBox} from "../BBox";
import Constants from "../../Models/Constants";
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource";
export interface GeoLocationPointProperties {
id: "gps",
@ -22,7 +22,7 @@ export interface GeoLocationPointProperties {
export default class GeoLocationHandler extends VariableUiElement {
private readonly currentLocation?: FeatureSource
private readonly currentLocation?: SimpleFeatureSource
/**
* Wether or not the geolocation is active, aka the user requested the current location
@ -43,7 +43,7 @@ export default class GeoLocationHandler extends VariableUiElement {
* Literally: _currentGPSLocation.data != undefined
* @private
*/
private readonly _hasLocation: UIEventSource<boolean>;
private readonly _hasLocation: Store<boolean>;
private readonly _currentGPSLocation: UIEventSource<Coordinates>;
/**
* Kept in order to update the marker
@ -70,7 +70,7 @@ export default class GeoLocationHandler extends VariableUiElement {
constructor(
state: {
selectedElement: UIEventSource<any>;
currentUserLocation?: FeatureSource,
currentUserLocation?: SimpleFeatureSource,
leafletMap: UIEventSource<any>,
layoutToUse: LayoutConfig,
featureSwitchGeolocation: UIEventSource<boolean>
@ -236,12 +236,9 @@ export default class GeoLocationHandler extends VariableUiElement {
self.currentLocation?.features?.setData([{feature, freshness: new Date()}])
const timeSinceRequest =
(new Date().getTime() - (self._lastUserRequest.data?.getTime() ?? 0)) / 1000;
if (willFocus.data) {
console.log("Zooming to user location: willFocus is set")
willFocus.setData(false)
lastClick.setData(undefined);
autozoomDone = true;
self.MoveToCurrentLocation(16);
} else if (self._isLocked.data) {

View file

@ -1,4 +1,4 @@
import {UIEventSource} from "../UIEventSource";
import {Store, UIEventSource} from "../UIEventSource";
import {Or} from "../Tags/Or";
import {Overpass} from "../Osm/Overpass";
import FeatureSource from "../FeatureSource/FeatureSource";
@ -34,13 +34,13 @@ export default class OverpassFeatureSource implements FeatureSource {
private readonly retries: UIEventSource<number> = new UIEventSource<number>(0);
private readonly state: {
readonly locationControl: UIEventSource<Loc>,
readonly locationControl: Store<Loc>,
readonly layoutToUse: LayoutConfig,
readonly overpassUrl: UIEventSource<string[]>;
readonly overpassTimeout: UIEventSource<number>;
readonly currentBounds: UIEventSource<BBox>
readonly overpassUrl: Store<string[]>;
readonly overpassTimeout: Store<number>;
readonly currentBounds: Store<BBox>
}
private readonly _isActive: UIEventSource<boolean>
private readonly _isActive: Store<boolean>
/**
* Callback to handle all the data
*/
@ -54,16 +54,16 @@ export default class OverpassFeatureSource implements FeatureSource {
constructor(
state: {
readonly locationControl: UIEventSource<Loc>,
readonly locationControl: Store<Loc>,
readonly layoutToUse: LayoutConfig,
readonly overpassUrl: UIEventSource<string[]>;
readonly overpassTimeout: UIEventSource<number>;
readonly overpassMaxZoom: UIEventSource<number>,
readonly currentBounds: UIEventSource<BBox>
readonly overpassUrl: Store<string[]>;
readonly overpassTimeout: Store<number>;
readonly overpassMaxZoom: Store<number>,
readonly currentBounds: Store<BBox>
},
options: {
padToTiles: UIEventSource<number>,
isActive?: UIEventSource<boolean>,
padToTiles: Store<number>,
isActive?: Store<boolean>,
relationTracker: RelationsTracker,
onBboxLoaded?: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void,
freshnesses?: Map<string, TileFreshnessCalculator>

View file

@ -1,5 +1,4 @@
import {UIEventSource} from "../UIEventSource";
import Translations from "../../UI/i18n/Translations";
import {Store, UIEventSource} from "../UIEventSource";
import Locale from "../../UI/i18n/Locale";
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer";
import Combine from "../../UI/Base/Combine";
@ -9,11 +8,11 @@ import {Utils} from "../../Utils";
export default class TitleHandler {
constructor(state: {
selectedElement: UIEventSource<any>,
selectedElement: Store<any>,
layoutToUse: LayoutConfig,
allElements: ElementStorage
}) {
const currentTitle: UIEventSource<string> = state.selectedElement.map(
const currentTitle: Store<string> = state.selectedElement.map(
selected => {
const layout = state.layoutToUse
const defaultTitle = layout?.title?.txt ?? "MapComplete"

View file

@ -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) {

View file

@ -3,7 +3,7 @@ import FilteringFeatureSource from "./Sources/FilteringFeatureSource";
import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter";
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "./FeatureSource";
import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource";
import {UIEventSource} from "../UIEventSource";
import {Store, UIEventSource} from "../UIEventSource";
import {TileHierarchyTools} from "./TiledFeatureSource/TileHierarchy";
import RememberingSource from "./Sources/RememberingSource";
import OverpassFeatureSource from "../Actors/OverpassFeatureSource";
@ -38,8 +38,8 @@ import {ElementStorage} from "../ElementStorage";
*/
export default class FeaturePipeline {
public readonly sufficientlyZoomed: UIEventSource<boolean>;
public readonly runningQuery: UIEventSource<boolean>;
public readonly sufficientlyZoomed: Store<boolean>;
public readonly runningQuery: Store<boolean>;
public readonly timeout: UIEventSource<number>;
public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false)
public readonly newDataLoadedSignal: UIEventSource<FeatureSource> = new UIEventSource<FeatureSource>(undefined)
@ -314,7 +314,7 @@ export default class FeaturePipeline {
// We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this
perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer)
// AT last, we always apply the metatags whenever possible
perLayer.features.addCallbackAndRunD(feats => {
perLayer.features.addCallbackAndRunD(_ => {
self.onNewDataLoaded(perLayer);
})
@ -417,7 +417,7 @@ export default class FeaturePipeline {
/*
* Gives an UIEventSource containing the tileIndexes of the tiles that should be loaded from OSM
* */
private getNeededTilesFromOsm(isSufficientlyZoomed: UIEventSource<boolean>): UIEventSource<number[]> {
private getNeededTilesFromOsm(isSufficientlyZoomed: Store<boolean>): Store<number[]> {
const self = this
return this.state.currentBounds.map(bbox => {
if (bbox === undefined) {
@ -450,12 +450,12 @@ export default class FeaturePipeline {
private initOverpassUpdater(state: {
allElements: ElementStorage;
layoutToUse: LayoutConfig,
currentBounds: UIEventSource<BBox>,
locationControl: UIEventSource<Loc>,
readonly overpassUrl: UIEventSource<string[]>;
readonly overpassTimeout: UIEventSource<number>;
readonly overpassMaxZoom: UIEventSource<number>,
}, useOsmApi: UIEventSource<boolean>): OverpassFeatureSource {
currentBounds: Store<BBox>,
locationControl: Store<Loc>,
readonly overpassUrl: Store<string[]>;
readonly overpassTimeout: Store<number>;
readonly overpassMaxZoom: Store<number>,
}, useOsmApi: Store<boolean>): OverpassFeatureSource {
const minzoom = Math.min(...state.layoutToUse.layers.map(layer => layer.minzoom))
const overpassIsActive = state.currentBounds.map(bbox => {
if (bbox === undefined) {

View file

@ -1,9 +1,9 @@
import {UIEventSource} from "../UIEventSource";
import {Store, UIEventSource} from "../UIEventSource";
import FilteredLayer from "../../Models/FilteredLayer";
import {BBox} from "../BBox";
export default interface FeatureSource {
features: UIEventSource<{ feature: any, freshness: Date }[]>;
features: Store<{ feature: any, freshness: Date }[]>;
/**
* Mainly used for debuging
*/
@ -26,14 +26,14 @@ export interface FeatureSourceForLayer extends FeatureSource {
* A feature source which is aware of the indexes it contains
*/
export interface IndexedFeatureSource extends FeatureSource {
readonly containedIds: UIEventSource<Set<string>>
readonly containedIds: Store<Set<string>>
}
/**
* A feature source which has some extra data about it's state
*/
export interface FeatureSourceState {
readonly sufficientlyZoomed: UIEventSource<boolean>;
readonly runningQuery: UIEventSource<boolean>;
readonly timeout: UIEventSource<number>;
readonly sufficientlyZoomed: Store<boolean>;
readonly runningQuery: Store<boolean>;
readonly timeout: Store<number>;
}

View file

@ -1,5 +1,5 @@
import FeatureSource, {FeatureSourceForLayer, Tiled} from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
import {Store} from "../UIEventSource";
import FilteredLayer from "../../Models/FilteredLayer";
import SimpleFeatureSource from "./Sources/SimpleFeatureSource";
@ -11,7 +11,7 @@ import SimpleFeatureSource from "./Sources/SimpleFeatureSource";
*/
export default class PerLayerFeatureSourceSplitter {
constructor(layers: UIEventSource<FilteredLayer[]>,
constructor(layers: Store<FilteredLayer[]>,
handleLayerData: (source: FeatureSourceForLayer & Tiled) => void,
upstream: FeatureSource,
options?: {
@ -19,7 +19,7 @@ export default class PerLayerFeatureSourceSplitter {
handleLeftovers?: (featuresWithoutLayer: any[]) => void
}) {
const knownLayers = new Map<string, FeatureSourceForLayer & Tiled>()
const knownLayers = new Map<string, SimpleFeatureSource>()
function update() {
const features = upstream.features?.data;

View file

@ -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

View file

@ -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
})
}
}

View file

@ -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;

View file

@ -1,31 +1,55 @@
import FeatureSource from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource";
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {ImmutableStore, Store, UIEventSource} from "../../UIEventSource";
import {stat} from "fs";
import FilteredLayer from "../../../Models/FilteredLayer";
import {BBox} from "../../BBox";
/**
* A simple dummy implementation for whenever it is needed
* A simple, read only feature store.
*/
export default class StaticFeatureSource implements FeatureSource {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name: string = "StaticFeatureSource"
public readonly features: Store<{ feature: any; freshness: Date }[]>;
public readonly name: string
constructor(features: any[] | UIEventSource<any[] | UIEventSource<{ feature: any, freshness: Date }>>, useFeaturesDirectly) {
const now = new Date();
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);
}
}
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)
}
}

View file

@ -2,7 +2,7 @@ import {Utils} from "../../../Utils";
import * as OsmToGeoJson from "osmtogeojson";
import StaticFeatureSource from "../Sources/StaticFeatureSource";
import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter";
import {UIEventSource} from "../../UIEventSource";
import {Store, UIEventSource} from "../../UIEventSource";
import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {Tiles} from "../../../Models/TileRange";
@ -20,13 +20,13 @@ export default class OsmFeatureSource {
public readonly downloadedTiles = new Set<number>()
public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = []
private readonly _backend: string;
private readonly filteredLayers: UIEventSource<FilteredLayer[]>;
private readonly filteredLayers: Store<FilteredLayer[]>;
private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void;
private isActive: UIEventSource<boolean>;
private isActive: Store<boolean>;
private options: {
handleTile: (tile: FeatureSourceForLayer & Tiled) => void;
isActive: UIEventSource<boolean>,
neededTiles: UIEventSource<number[]>,
isActive: Store<boolean>,
neededTiles: Store<number[]>,
state: {
readonly osmConnection: OsmConnection;
},
@ -36,8 +36,8 @@ export default class OsmFeatureSource {
constructor(options: {
handleTile: (tile: FeatureSourceForLayer & Tiled) => void;
isActive: UIEventSource<boolean>,
neededTiles: UIEventSource<number[]>,
isActive: Store<boolean>,
neededTiles: Store<number[]>,
state: {
readonly filteredLayers: UIEventSource<FilteredLayer[]>;
readonly osmConnection: OsmConnection;
@ -119,7 +119,7 @@ export default class OsmFeatureSource {
const index = Tiles.tile_index(z, x, y);
new PerLayerFeatureSourceSplitter(this.filteredLayers,
this.handleTile,
new StaticFeatureSource(geojson.features, false),
StaticFeatureSource.fromGeojson(geojson.features),
{
tileIndex: index
}

View file

@ -1,5 +1,5 @@
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource";
import {Store, UIEventSource} from "../../UIEventSource";
import FilteredLayer from "../../../Models/FilteredLayer";
import TileHierarchy from "./TileHierarchy";
import {Tiles} from "../../../Models/TileRange";
@ -24,7 +24,7 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
public readonly maxFeatureCount: number;
public readonly name;
public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>
public readonly containedIds: UIEventSource<Set<string>>
public readonly containedIds: Store<Set<string>>
public readonly bbox: BBox;
public readonly tileIndex: number;

View file

@ -2,7 +2,7 @@ import {Mapillary} from "./Mapillary";
import {WikimediaImageProvider} from "./WikimediaImageProvider";
import {Imgur} from "./Imgur";
import GenericImageProvider from "./GenericImageProvider";
import {UIEventSource} from "../UIEventSource";
import {Store, UIEventSource} from "../UIEventSource";
import ImageProvider, {ProvidedImage} from "./ImageProvider";
import {WikidataImageProvider} from "./WikidataImageProvider";
@ -37,7 +37,7 @@ export default class AllImageProviders {
private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map<string, UIEventSource<ProvidedImage[]>>()
public static LoadImagesFor(tags: UIEventSource<any>, tagKey?: string[]): UIEventSource<ProvidedImage[]> {
public static LoadImagesFor(tags: Store<any>, tagKey?: string[]): Store<ProvidedImage[]> {
if (tags.data.id === undefined) {
return undefined;
}

View file

@ -1,4 +1,4 @@
import {UIEventSource} from "../UIEventSource";
import {Store, Stores, UIEventSource} from "../UIEventSource";
import BaseUIElement from "../../UI/BaseUIElement";
import {LicenseInfo} from "./LicenseInfo";
import {Utils} from "../../Utils";
@ -13,14 +13,14 @@ export default abstract class ImageProvider {
public abstract readonly defaultKeyPrefixes: string[]
private _cache = new Map<string, UIEventSource<LicenseInfo>>()
private _cache = new Map<string, Store<LicenseInfo>>()
GetAttributionFor(url: string): UIEventSource<LicenseInfo> {
GetAttributionFor(url: string): Store<LicenseInfo> {
const cached = this._cache.get(url);
if (cached !== undefined) {
return cached;
}
const src = UIEventSource.FromPromise(this.DownloadAttribution(url))
const src = Stores.FromPromise(this.DownloadAttribution(url))
this._cache.set(url, src)
return src;
}
@ -30,7 +30,7 @@ export default abstract class ImageProvider {
/**
* Given a properies object, maps it onto _all_ the available pictures for this imageProvider
*/
public GetRelevantUrls(allTags: UIEventSource<any>, options?: {
public GetRelevantUrls(allTags: Store<any>, options?: {
prefixes?: string[]
}): UIEventSource<ProvidedImage[]> {
const prefixes = options?.prefixes ?? this.defaultKeyPrefixes

View file

@ -182,7 +182,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
features.push(newGeometry)
}
return new StaticFeatureSource(features, false)
return StaticFeatureSource.fromGeojson(features)
}
public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {

View file

@ -159,7 +159,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
})
return new StaticFeatureSource(Utils.NoNull(preview), false)
return StaticFeatureSource.fromGeojson(Utils.NoNull(preview))
}

View file

@ -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)) {

View file

@ -1,5 +1,5 @@
import osmAuth from "osm-auth";
import {UIEventSource} from "../UIEventSource";
import {Stores, UIEventSource} from "../UIEventSource";
import {OsmPreferences} from "./OsmPreferences";
import {ChangesetHandler} from "./ChangesetHandler";
import {ElementStorage} from "../ElementStorage";
@ -228,7 +228,7 @@ export class OsmConnection {
}
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text)
return new Promise((ok, error) => {
return new Promise((ok) => {
ok()
});
}
@ -236,7 +236,7 @@ export class OsmConnection {
this.auth.xhr({
method: 'POST',
path: `/api/0.6/notes/${id}/close${textSuffix}`,
}, function (err, response) {
}, function (err, _) {
if (err !== null) {
error(err)
} else {
@ -251,7 +251,7 @@ export class OsmConnection {
public reopenNote(id: number | string, text?: string): Promise<void> {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text)
return new Promise((ok, error) => {
return new Promise((ok) => {
ok()
});
}
@ -263,7 +263,7 @@ export class OsmConnection {
this.auth.xhr({
method: 'POST',
path: `/api/0.6/notes/${id}/reopen${textSuffix}`
}, function (err, response) {
}, function (err, _) {
if (err !== null) {
error(err)
} else {
@ -278,7 +278,7 @@ export class OsmConnection {
public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually opening note with text ", text)
return new Promise<{ id: number }>((ok, error) => {
return new Promise<{ id: number }>((ok) => {
window.setTimeout(() => ok({id: Math.floor(Math.random() * 1000)}), Math.random() * 5000)
});
}
@ -315,7 +315,7 @@ export class OsmConnection {
public addCommentToNode(id: number | string, text: string): Promise<void> {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id)
return new Promise((ok, error) => {
return new Promise((ok) => {
ok()
});
}
@ -328,7 +328,7 @@ export class OsmConnection {
method: 'POST',
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`
}, function (err, response) {
}, function (err, _) {
if (err !== null) {
error(err)
} else {
@ -374,7 +374,7 @@ export class OsmConnection {
return;
}
this.isChecking = true;
UIEventSource.Chronic(5 * 60 * 1000).addCallback(_ => {
Stores.Chronic(5 * 60 * 1000).addCallback(_ => {
if (self.isLoggedIn.data) {
console.log("Checking for messages")
self.AttemptLogin();

View file

@ -1,6 +1,6 @@
import {Utils} from "../../Utils";
import * as polygon_features from "../../assets/polygon-features.json";
import {UIEventSource} from "../UIEventSource";
import {Store, Stores, UIEventSource} from "../UIEventSource";
import {BBox} from "../BBox";
@ -40,7 +40,7 @@ export abstract class OsmObject {
this.backendURL = url;
}
public static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> {
public static DownloadObject(id: string, forceRefresh: boolean = false): Store<OsmObject> {
let src: UIEventSource<OsmObject>;
if (OsmObject.objectCache.has(id)) {
src = OsmObject.objectCache.get(id)

View file

@ -1,7 +1,7 @@
import {TagsFilter} from "../Tags/TagsFilter";
import RelationsTracker from "./RelationsTracker";
import {Utils} from "../../Utils";
import {UIEventSource} from "../UIEventSource";
import {ImmutableStore, Store} from "../UIEventSource";
import {BBox} from "../BBox";
import * as osmtogeojson from "osmtogeojson";
import {FeatureCollection} from "@turf/turf";
@ -12,7 +12,7 @@ import {FeatureCollection} from "@turf/turf";
export class Overpass {
private _filter: TagsFilter
private readonly _interpreterUrl: string;
private readonly _timeout: UIEventSource<number>;
private readonly _timeout: Store<number>;
private readonly _extraScripts: string[];
private _includeMeta: boolean;
private _relationTracker: RelationsTracker;
@ -20,10 +20,10 @@ export class Overpass {
constructor(filter: TagsFilter,
extraScripts: string[],
interpreterUrl: string,
timeout?: UIEventSource<number>,
timeout?: Store<number>,
relationTracker?: RelationsTracker,
includeMeta = true) {
this._timeout = timeout ?? new UIEventSource<number>(90);
this._timeout = timeout ?? new ImmutableStore<number>(90);
this._interpreterUrl = interpreterUrl;
const optimized = filter.optimize()
if(optimized === true || optimized === false){

View file

@ -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,

View file

@ -1,5 +1,5 @@
import UserRelatedState from "./UserRelatedState";
import {UIEventSource} from "../UIEventSource";
import {Store, Stores, UIEventSource} from "../UIEventSource";
import BaseLayer from "../../Models/BaseLayer";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import AvailableBaseLayers from "../Actors/AvailableBaseLayers";
@ -18,6 +18,7 @@ import {GeoOperations} from "../GeoOperations";
import TitleHandler from "../Actors/TitleHandler";
import {BBox} from "../BBox";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import {TiledStaticFeatureSource} from "../FeatureSource/Sources/StaticFeatureSource";
/**
* Contains all the leaflet-map related state
@ -31,7 +32,7 @@ export default class MapState extends UserRelatedState {
/**
* A list of currently available background layers
*/
public availableBackgroundLayers: UIEventSource<BaseLayer[]>;
public availableBackgroundLayers: Store<BaseLayer[]>;
/**
* The current background layer
@ -52,12 +53,12 @@ export default class MapState extends UserRelatedState {
/**
* The location as delivered by the GPS
*/
public currentUserLocation: FeatureSourceForLayer & Tiled;
public currentUserLocation: SimpleFeatureSource;
/**
* All previously visited points
*/
public historicalUserLocations: FeatureSourceForLayer & Tiled;
public historicalUserLocations: SimpleFeatureSource;
/**
* The number of seconds that the GPS-locations are stored in memory.
* Time in seconds
@ -176,7 +177,7 @@ export default class MapState extends UserRelatedState {
let i = 0
const self = this;
const features: UIEventSource<{ feature: any, freshness: Date }[]> = this.currentBounds.map(bounds => {
const features: Store<{ feature: any, freshness: Date }[]> = this.currentBounds.map(bounds => {
if (bounds === undefined) {
return []
}
@ -205,7 +206,7 @@ export default class MapState extends UserRelatedState {
return [feature]
})
this.currentView = new SimpleFeatureSource(currentViewLayer, 0, features)
this.currentView = new TiledStaticFeatureSource(features, currentViewLayer);
}
private initGpsLocation() {
@ -289,13 +290,13 @@ export default class MapState extends UserRelatedState {
})
let gpsLineLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_track")[0]
if (gpsLineLayerDef !== undefined) {
this.historicalUserLocationsTrack = new SimpleFeatureSource(gpsLineLayerDef, Tiles.tile_index(0, 0, 0), asLine);
this.historicalUserLocationsTrack = new TiledStaticFeatureSource(asLine, gpsLineLayerDef);
}
}
private initHomeLocation() {
const empty = []
const feature = UIEventSource.ListStabilized(this.osmConnection.userDetails.map(userDetails => {
const feature = Stores.ListStabilized(this.osmConnection.userDetails.map(userDetails => {
if (userDetails === undefined) {
return undefined;
@ -328,7 +329,7 @@ export default class MapState extends UserRelatedState {
const flayer = this.filteredLayers.data.filter(l => l.layerDef.id === "home_location")[0]
if (flayer !== undefined) {
this.homeLocation = new SimpleFeatureSource(flayer, Tiles.tile_index(0, 0, 0), feature)
this.homeLocation = new TiledStaticFeatureSource(feature, flayer)
}
}
@ -336,7 +337,7 @@ export default class MapState extends UserRelatedState {
private getPref(key: string, layer: LayerConfig): UIEventSource<boolean> {
const pref = this.osmConnection
.GetPreference(key)
.map(v => {
.sync(v => {
if(v === undefined){
return undefined
}

View file

@ -1,7 +1,7 @@
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {OsmConnection} from "../Osm/OsmConnection";
import {MangroveIdentity} from "../Web/MangroveReviews";
import {UIEventSource} from "../UIEventSource";
import {Store, UIEventSource} from "../UIEventSource";
import {QueryParameters} from "../Web/QueryParameters";
import {LocalStorageSource} from "../Web/LocalStorageSource";
import {Utils} from "../../Utils";
@ -37,7 +37,7 @@ export default class UserRelatedState extends ElementsState {
*/
public favouriteLayers: UIEventSource<string[]>;
public readonly isTranslator : UIEventSource<boolean>;
public readonly isTranslator : Store<boolean>;
constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) {
super(layoutToUse);
@ -53,7 +53,7 @@ export default class UserRelatedState extends ElementsState {
osmConfiguration: <'osm' | 'osm-test'>this.featureSwitchApiURL.data,
attemptLogin: options?.attemptLogin
})
const translationMode = this.osmConnection.GetPreference("translation-mode").map(str => str === undefined ? undefined : str === "true", [], b => b === undefined ? undefined : b+"")
const translationMode = this.osmConnection.GetPreference("translation-mode").sync(str => str === undefined ? undefined : str === "true", [], b => b === undefined ? undefined : b+"")
translationMode.syncWith(Locale.showLinkToWeblate)
@ -108,7 +108,7 @@ export default class UserRelatedState extends ElementsState {
// Important: the favourite layers are initialized _after_ the installed themes, as these might contain an installedTheme
this.favouriteLayers = LocalStorageSource.Get("favouriteLayers")
.syncWith(this.osmConnection.GetLongPreference("favouriteLayers"))
.map(
.sync(
(str) => Utils.Dedup(str?.split(";")) ?? [],
[],
(layers) => Utils.Dedup(layers)?.join(";")

View file

@ -1,64 +1,10 @@
import {Utils} from "../Utils";
export class UIEventSource<T> {
private static allSources: UIEventSource<any>[] = UIEventSource.PrepPerf();
public data: T;
public trace: boolean;
private readonly tag: string;
private _callbacks: ((t: T) => (boolean | void | any)) [] = [];
constructor(data: T, tag: string = "") {
this.tag = tag;
this.data = data;
if (tag === undefined || tag === "") {
const callstack = new Error().stack.split("\n")
this.tag = callstack[1]
}
UIEventSource.allSources.push(this);
}
static PrepPerf(): UIEventSource<any>[] {
if (Utils.runningFromConsole) {
return [];
}
// @ts-ignore
window.mapcomplete_performance = () => {
console.log(UIEventSource.allSources.length, "uieventsources created");
const copy = [...UIEventSource.allSources];
copy.sort((a, b) => b._callbacks.length - a._callbacks.length);
console.log("Topten is:")
for (let i = 0; i < 10; i++) {
console.log(copy[i].tag, copy[i]);
}
return UIEventSource.allSources;
}
return [];
}
public static flatten<X>(source: UIEventSource<UIEventSource<X>>, possibleSources?: UIEventSource<any>[]): UIEventSource<X> {
const sink = new UIEventSource<X>(source.data?.data);
source.addCallback((latestData) => {
sink.setData(latestData?.data);
latestData.addCallback(data => {
if (source.data !== latestData) {
return true;
}
sink.setData(data)
})
});
for (const possibleSource of possibleSources ?? []) {
possibleSource?.addCallback(() => {
sink.setData(source.data?.data);
})
}
return sink;
}
public static Chronic(millis: number, asLong: () => boolean = undefined): UIEventSource<Date> {
/**
* Various static utils
*/
export class Stores {
public static Chronic(millis: number, asLong: () => boolean = undefined): Store<Date> {
const source = new UIEventSource<Date>(undefined);
function run() {
@ -72,17 +18,8 @@ export class UIEventSource<T> {
return source;
}
/**
* Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated.
* If the promise fails, the value will stay undefined
* @param promise
* @constructor
*/
public static FromPromise<T>(promise: Promise<T>): UIEventSource<T> {
const src = new UIEventSource<T>(undefined)
promise?.then(d => src.setData(d))
promise?.catch(err => console.warn("Promise failed:", err))
return src
public static FromPromiseWithErr<T>(promise: Promise<T>): Store<{ success: T } | { error: any }>{
return UIEventSource.FromPromiseWithErr(promise);
}
/**
@ -91,13 +28,17 @@ export class UIEventSource<T> {
* @param promise
* @constructor
*/
public static FromPromiseWithErr<T>(promise: Promise<T>): UIEventSource<{ success: T } | { error: any }> {
const src = new UIEventSource<{ success: T } | { error: any }>(undefined)
promise?.then(d => src.setData({success: d}))
promise?.catch(err => src.setData({error: err}))
public static FromPromise<T>(promise: Promise<T>): Store<T> {
const src = new UIEventSource<T>(undefined)
promise?.then(d => src.setData(d))
promise?.catch(err => console.warn("Promise failed:", err))
return src
}
public static flatten<X>(source: Store<Store<X>>, possibleSources?: Store<any>[]): Store<X> {
return UIEventSource.flatten(source, possibleSources);
}
/**
* Given a UIEVentSource with a list, returns a new UIEventSource which is only updated if the _contents_ of the list are different.
* E.g.
@ -112,7 +53,7 @@ export class UIEventSource<T> {
* @param src
* @constructor
*/
public static ListStabilized<T>(src: UIEventSource<T[]>): UIEventSource<T[]> {
public static ListStabilized<T>(src: Store<T[]>): Store<T[]> {
const stable = new UIEventSource<T[]>(src.data)
src.addCallback(list => {
@ -141,46 +82,58 @@ export class UIEventSource<T> {
})
return stable
}
}
public static asFloat(source: UIEventSource<string>): UIEventSource<number> {
return source.map(
(str) => {
let parsed = parseFloat(str);
return isNaN(parsed) ? undefined : parsed;
},
[],
(fl) => {
if (fl === undefined || isNaN(fl)) {
return undefined;
}
return ("" + fl).substr(0, 8);
export abstract class Store<T> {
abstract readonly data: T;
/**
* OPtional value giving a title to the UIEventSource, mainly used for debugging
*/
public readonly tag: string | undefined;
constructor(tag: string = undefined) {
this.tag = tag;
if ((tag === undefined || tag === "")) {
let createStack = Utils.runningFromConsole;
if(!Utils.runningFromConsole) {
createStack = window.location.hostname === "127.0.0.1"
}
)
}
public AsPromise(condition?: ((t: T )=> boolean)): Promise<T> {
const self = this;
condition = condition ?? (t => t !== undefined)
return new Promise((resolve, reject) => {
if (condition(self.data)) {
resolve(self.data)
} else {
self.addCallbackD(data => {
resolve(data)
return true; // return true to unregister as we only need to be called once
})
if(createStack) {
const callstack = new Error().stack.split("\n")
this.tag = callstack[1]
}
})
}
}
public WaitForPromise(promise: Promise<T>, onFail: ((any) => void)): UIEventSource<T> {
const self = this;
promise?.then(d => self.setData(d))
promise?.catch(err => onFail(err))
return this
}
abstract map<J>(f: ((t: T) => J)): Store<J>
abstract map<J>(f: ((t: T) => J), extraStoresToWatch: Store<any>[]): Store<J>
public withEqualityStabilized(comparator: (t: T | undefined, t1: T | undefined) => boolean): UIEventSource<T> {
/**
* Add a callback function which will run on future data changes
*/
abstract addCallback(callback: (data: T) => void);
/**
* Adds a callback function, which will be run immediately.
* Only triggers if the current data is defined
*/
abstract addCallbackAndRunD(callback: (data: T) => void);
/**
* Add a callback function which will run on future data changes
* Only triggers if the data is defined
*/
abstract addCallbackD(callback: (data: T) => void);
/**
* Adds a callback function, which will be run immediately.
* Only triggers if the current data is defined
*/
abstract addCallbackAndRun(callback: (data: T) => void);
public withEqualityStabilized(comparator: (t: T | undefined, t1: T | undefined) => boolean): Store<T> {
let oldValue = undefined;
return this.map(v => {
if (v == oldValue) {
@ -194,6 +147,205 @@ export class UIEventSource<T> {
})
}
/**
* Monadic bind function
*/
public bind<X>(f: ((t: T) => Store<X>)): Store<X> {
const mapped = this.map(f)
const sink = new UIEventSource<X>(undefined)
const seenEventSources = new Set<Store<X>>();
mapped.addCallbackAndRun(newEventSource => {
if (newEventSource === null) {
sink.setData(null)
} else if (newEventSource === undefined) {
sink.setData(undefined)
} else if (!seenEventSources.has(newEventSource)) {
seenEventSources.add(newEventSource)
newEventSource.addCallbackAndRun(resultData => {
if (mapped.data === newEventSource) {
sink.setData(resultData);
}
})
} else {
// Already seen, so we don't have to add a callback, just update the value
sink.setData(newEventSource.data)
}
})
return sink;
}
public stabilized(millisToStabilize): Store<T> {
if (Utils.runningFromConsole) {
return this;
}
const newSource = new UIEventSource<T>(this.data);
let currentCallback = 0;
this.addCallback(latestData => {
currentCallback++;
const thisCallback = currentCallback;
window.setTimeout(() => {
if (thisCallback === currentCallback) {
newSource.setData(latestData);
}
}, millisToStabilize)
});
return newSource;
}
public AsPromise(condition?: ((t: T) => boolean)): Promise<T> {
const self = this;
condition = condition ?? (t => t !== undefined)
return new Promise((resolve) => {
if (condition(self.data)) {
resolve(self.data)
} else {
self.addCallbackD(data => {
resolve(data)
return true; // return true to unregister as we only need to be called once
})
}
})
}
}
export class ImmutableStore<T> extends Store<T> {
public readonly data: T;
constructor(data: T) {
super();
this.data = data;
}
addCallback(callback: (data: T) => void) {
// pass: data will never change
}
addCallbackAndRun(callback: (data: T) => void) {
callback(this.data)
// no callback registry: data will never change
}
addCallbackAndRunD(callback: (data: T) => void) {
if(this.data !== undefined){
callback(this.data)
}
// no callback registry: data will never change
}
addCallbackD(callback: (data: T) => void) {
// pass: data will never change
}
map<J>(f: (t: T) => J): ImmutableStore<J> {
return new ImmutableStore<J>(f(this.data));
}
}
export class UIEventSource<T> extends Store<T> {
private static allSources: UIEventSource<any>[] = UIEventSource.PrepPerf();
public data: T;
private _callbacks: ((t: T) => (boolean | void | any)) [] = [];
constructor(data: T, tag: string = "") {
super(tag);
this.data = data;
UIEventSource.allSources.push(this);
}
static PrepPerf(): UIEventSource<any>[] {
if (Utils.runningFromConsole) {
return [];
}
// @ts-ignore
window.mapcomplete_performance = () => {
console.log(UIEventSource.allSources.length, "uieventsources created");
const copy = [...UIEventSource.allSources];
copy.sort((a, b) => b._callbacks.length - a._callbacks.length);
console.log("Topten is:")
for (let i = 0; i < 10; i++) {
console.log(copy[i].tag, copy[i]);
}
return UIEventSource.allSources;
}
return [];
}
public static flatten<X>(source: Store<Store<X>>, possibleSources?: Store<any>[]): UIEventSource<X> {
const sink = new UIEventSource<X>(source.data?.data);
source.addCallback((latestData) => {
sink.setData(latestData?.data);
latestData.addCallback(data => {
if (source.data !== latestData) {
return true;
}
sink.setData(data)
})
});
for (const possibleSource of possibleSources ?? []) {
possibleSource?.addCallback(() => {
sink.setData(source.data?.data);
})
}
return sink;
}
/**
* Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated.
* If the promise fails, the value will stay undefined, but 'onError' will be called
*/
public static FromPromise<T>(promise: Promise<T>, onError :( (e: any) => void) = undefined): UIEventSource<T> {
const src = new UIEventSource<T>(undefined)
promise?.then(d => src.setData(d))
promise?.catch(err => {
if(onError !== undefined){
onError(err)
}else{
console.warn("Promise failed:", err);
}
})
return src
}
/**
* Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated.
* If the promise fails, the value will stay undefined
* @param promise
* @constructor
*/
public static FromPromiseWithErr<T>(promise: Promise<T>): UIEventSource<{ success: T } | { error: any }> {
const src = new UIEventSource<{ success: T } | { error: any }>(undefined)
promise?.then(d => src.setData({success: d}))
promise?.catch(err => src.setData({error: err}))
return src
}
public static asFloat(source: UIEventSource<string>): UIEventSource<number> {
return source.sync(
(str) => {
let parsed = parseFloat(str);
return isNaN(parsed) ? undefined : parsed;
},
[],
(fl) => {
if (fl === undefined || isNaN(fl)) {
return undefined;
}
return ("" + fl).substr(0, 8);
}
)
}
/**
* Adds a callback
*
@ -205,9 +357,6 @@ export class UIEventSource<T> {
// This ^^^ actually works!
throw "Don't add console.log directly as a callback - you'll won't be able to find it afterwards. Wrap it in a lambda instead."
}
if (this.trace) {
console.trace("Added a callback")
}
this._callbacks.push(callback);
return this;
}
@ -255,44 +404,47 @@ export class UIEventSource<T> {
}
/**
* Monadic bind function
* Monoidal map which results in a read-only store
* Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)'
* @param f: The transforming function
* @param extraSources: also trigger the update if one of these sources change
*/
public bind<X>(f: ((t: T) => UIEventSource<X>)): UIEventSource<X> {
const mapped = this.map(f)
const sink = new UIEventSource<X>(undefined)
const seenEventSources = new Set<UIEventSource<X>>();
mapped.addCallbackAndRun(newEventSource => {
if (newEventSource === null) {
sink.setData(null)
} else if (newEventSource === undefined) {
sink.setData(undefined)
} else if (!seenEventSources.has(newEventSource)) {
seenEventSources.add(newEventSource)
newEventSource.addCallbackAndRun(resultData => {
if (mapped.data === newEventSource) {
sink.setData(resultData);
}
})
} else {
// Already seen, so we don't have to add a callback, just update the value
sink.setData(newEventSource.data)
}
})
public map<J>(f: ((t: T) => J),
extraSources: Store<any>[] = []): Store<J> {
const self = this;
return sink;
const stack = new Error().stack.split("\n");
const callee = stack[1]
const newSource = new UIEventSource<J>(
f(this.data),
"map(" + this.tag + ")@" + callee
);
const update = function () {
newSource.setData(f(self.data));
return false;
}
this.addCallback(update);
for (const extraSource of extraSources) {
extraSource?.addCallback(update);
}
return newSource;
}
/**
* Monoidal map:
* Two way sync with functions in both directions
* Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)'
* @param f: The transforming function
* @param extraSources: also trigger the update if one of these sources change
* @param g: a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData
* @param allowUnregister: if set, the update will be halted if no listeners are registered
*/
public map<J>(f: ((t: T) => J),
extraSources: UIEventSource<any>[] = [],
g: ((j: J, t: T) => T) = undefined,
public sync<J>(f: ((t: T) => J),
extraSources: Store<any>[],
g: ((j: J, t: T) => T) ,
allowUnregister = false): UIEventSource<J> {
const self = this;
@ -328,7 +480,7 @@ export class UIEventSource<T> {
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<T> {
return this;
}
public stabilized(millisToStabilize): UIEventSource<T> {
if (Utils.runningFromConsole) {
return this;
}
const newSource = new UIEventSource<T>(this.data);
let currentCallback = 0;
this.addCallback(latestData => {
currentCallback++;
const thisCallback = currentCallback;
window.setTimeout(() => {
if (thisCallback === currentCallback) {
newSource.setData(latestData);
}
}, millisToStabilize)
});
return newSource;
}
addCallbackAndRunD(callback: (data: T) => void) {
this.addCallbackAndRun(data => {
if (data !== undefined && data !== null) {
@ -375,4 +506,5 @@ export class UIEventSource<T> {
}
})
}
}

View file

@ -6,7 +6,7 @@ import {UIEventSource} from "../UIEventSource";
export class LocalStorageSource {
static GetParsed<T>(key: string, defaultValue: T): UIEventSource<T> {
return LocalStorageSource.Get(key).map(
return LocalStorageSource.Get(key).sync(
str => {
if (str === undefined) {
return defaultValue

View file

@ -33,7 +33,7 @@ export class QueryParameters {
}
public static GetBooleanQueryParameter(key: string, deflt: boolean, documentation?: string): UIEventSource<boolean> {
return QueryParameters.GetQueryParameter(key, ""+ deflt, documentation).map(str => str === "true", [], b => "" + b)
return QueryParameters.GetQueryParameter(key, ""+ deflt, documentation).sync(str => str === "true", [], b => "" + b)
}

View file

@ -1,4 +1,4 @@
import {UIEventSource} from "../UIEventSource";
import {Store} from "../UIEventSource";
export interface Review {
comment?: string,
@ -9,5 +9,5 @@ export interface Review {
/**
* True if the current logged in user is the creator of this comment
*/
made_by_user: UIEventSource<boolean>
made_by_user: Store<boolean>
}