Performance: sprinkle 'onDestroy' everywhere to cleanup old stores; cleanup 'Stores' utility class

This commit is contained in:
Pieter Vander Vennet 2025-08-01 03:07:37 +02:00
parent 66f093afd8
commit 81be4db044
79 changed files with 332 additions and 325 deletions

View file

@ -47,7 +47,7 @@ export default class TileLocalStorage<T> {
return cached return cached
} }
const src = <UIEventSource<T> & { flush: () => void }>( const src = <UIEventSource<T> & { flush: () => void }>(
UIEventSource.FromPromise(this.GetIdb(tileIndex)) UIEventSource.fromPromise(this.GetIdb(tileIndex))
) )
src.flush = () => this.SetIdb(tileIndex, src.data) src.flush = () => this.SetIdb(tileIndex, src.data)
src.addCallbackD((data) => this.SetIdb(tileIndex, data)) src.addCallbackD((data) => this.SetIdb(tileIndex, data))

View file

@ -21,7 +21,7 @@ export default class ChangeGeometryApplicator implements FeatureSource {
source.features.addCallbackAndRunD(() => this.update()) source.features.addCallbackAndRunD(() => this.update())
Stores.ListStabilized(changes.allChanges).addCallbackAndRunD(() => this.update()) Stores.listStabilized(changes.allChanges).addCallbackAndRunD(() => this.update())
} }
private update() { private update() {

View file

@ -26,7 +26,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
public readonly allFavourites: Store<Feature[]> public readonly allFavourites: Store<Feature[]>
constructor(state: WithChangesState) { constructor(state: WithChangesState) {
const features: Store<Feature[]> = Stores.ListStabilized( const features: Store<Feature[]> = Stores.listStabilized(
state.osmConnection.preferencesHandler.allPreferences.map((prefs) => { state.osmConnection.preferencesHandler.allPreferences.map((prefs) => {
const feats: Feature[] = [] const feats: Feature[] = []
const allIds = new Set<string>() const allIds = new Set<string>()
@ -60,7 +60,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
this.allFavourites = features this.allFavourites = features
this._osmConnection = state.osmConnection this._osmConnection = state.osmConnection
this._detectedIds = Stores.ListStabilized( this._detectedIds = Stores.listStabilized(
features.map((feats) => feats.map((f) => f.properties.id)) features.map((feats) => feats.map((f) => f.properties.id))
) )
const allFeatures = state.indexedFeatures const allFeatures = state.indexedFeatures

View file

@ -31,7 +31,7 @@ export default class NearbyFeatureSource implements FeatureSource {
this._currentZoom = options?.currentZoom.stabilized(500) this._currentZoom = options?.currentZoom.stabilized(500)
this._bounds = options?.bounds this._bounds = options?.bounds
this.features = Stores.ListStabilized(this._result) this.features = Stores.listStabilized(this._result)
sources.forEach((source, layer) => { sources.forEach((source, layer) => {
this.registerSource(source, layer) this.registerSource(source, layer)

View file

@ -44,7 +44,7 @@ export default class DynamicTileSource<
this.zDiff = options?.zDiff ?? 0 this.zDiff = options?.zDiff ?? 0
this.bounds = mapProperties.bounds this.bounds = mapProperties.bounds
const neededTiles: Store<number[]> = Stores.ListStabilized( const neededTiles: Store<number[]> = Stores.listStabilized(
mapProperties.bounds mapProperties.bounds
.mapD(() => { .mapD(() => {
if (options?.isActive && !options?.isActive.data) { if (options?.isActive && !options?.isActive.data) {

View file

@ -123,7 +123,7 @@ export class SummaryTileSource extends DynamicTileSource {
const [z, x, y] = Tiles.tile_from_index(tileIndex) const [z, x, y] = Tiles.tile_from_index(tileIndex)
let coordinates = Tiles.centerPointOf(z, x, y) let coordinates = Tiles.centerPointOf(z, x, y)
const url = `${cacheserver}/summary/${layersSummed}/${z}/${x}/${y}.json` const url = `${cacheserver}/summary/${layersSummed}/${z}/${x}/${y}.json`
const count = UIEventSource.FromPromiseWithErr(Utils.downloadJson(url)) const count = UIEventSource.fromPromiseWithErr(Utils.downloadJson(url))
return count.mapD((count) => { return count.mapD((count) => {
if (count["error"] !== undefined) { if (count["error"] !== undefined) {
console.error( console.error(

View file

@ -148,7 +148,7 @@ export default class AllImageProviders {
const singleSource = tags.bindD((tags) => imageProvider.getRelevantUrls(tags, prefixes)) const singleSource = tags.bindD((tags) => imageProvider.getRelevantUrls(tags, prefixes))
allSources.push(singleSource) allSources.push(singleSource)
} }
const source = Stores.fromStoresArray(allSources).map((result) => { const source = Stores.concat(allSources).map((result) => {
const all = Utils.concat(result) const all = Utils.concat(result)
return Utils.DedupOnId(all, (i) => [i?.id, i?.url, i?.alt_id]) return Utils.DedupOnId(all, (i) => [i?.id, i?.url, i?.alt_id])
}) })

View file

@ -1,4 +1,4 @@
import { Store, Stores } from "../UIEventSource" import { Store, UIEventSource } from "../UIEventSource"
import { LicenseInfo } from "./LicenseInfo" import { LicenseInfo } from "./LicenseInfo"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { Feature, Point } from "geojson" import { Feature, Point } from "geojson"
@ -114,7 +114,7 @@ export default abstract class ImageProvider {
tags: Record<string, string>, tags: Record<string, string>,
prefixes: string[] prefixes: string[]
): Store<ProvidedImage[]> { ): Store<ProvidedImage[]> {
return Stores.FromPromise(this.getRelevantUrlsFor(tags, prefixes)) return UIEventSource.fromPromise(this.getRelevantUrlsFor(tags, prefixes))
} }
public abstract ExtractUrls( public abstract ExtractUrls(

View file

@ -81,7 +81,7 @@ export class ImageUploadManager {
this._changes = changes this._changes = changes
this._gps = gpsLocation this._gps = gpsLocation
this._reportError = reportError this._reportError = reportError
Stores.Chronic(5 * 60000).addCallback(() => { Stores.chronic(5 * 60000).addCallback(() => {
// If images failed to upload: attempt to reupload // If images failed to upload: attempt to reupload
this.uploadQueue() this.uploadQueue()
}) })

View file

@ -290,7 +290,7 @@ export class Mapillary extends ImageProvider {
*/ */
public static isInStrictMode(): Store<boolean> { public static isInStrictMode(): Store<boolean> {
if (this._isInStrictMode === undefined) { if (this._isInStrictMode === undefined) {
this._isInStrictMode = UIEventSource.FromPromise(this.checkStrictMode()) this._isInStrictMode = UIEventSource.fromPromise(this.checkStrictMode())
} }
return this._isInStrictMode return this._isInStrictMode
} }

View file

@ -168,7 +168,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
} }
getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> { getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {
const source = UIEventSource.FromPromise(super.getRelevantUrlsFor(tags, prefixes)) const source = UIEventSource.fromPromise(super.getRelevantUrlsFor(tags, prefixes))
function hasLoading(data: ProvidedImage[]) { function hasLoading(data: ProvidedImage[]) {
if (data === undefined) { if (data === undefined) {
return true return true
@ -182,14 +182,14 @@ export default class PanoramaxImageProvider extends ImageProvider {
) )
} }
Stores.Chronic(5000, () => hasLoading(source.data)).addCallback(() => { Stores.chronic(5000, () => hasLoading(source.data)).addCallback(() => {
super.getRelevantUrlsFor(tags, prefixes).then((data) => { super.getRelevantUrlsFor(tags, prefixes).then((data) => {
source.set(data) source.set(data)
return !hasLoading(data) return !hasLoading(data)
}) })
}) })
return Stores.ListStabilized(source) return Stores.listStabilized(source)
} }
public async DownloadAttribution(providedImage: { id: string }): Promise<LicenseInfo> { public async DownloadAttribution(providedImage: { id: string }): Promise<LicenseInfo> {

View file

@ -682,7 +682,7 @@ export class OsmConnection {
if (this.isChecking) { if (this.isChecking) {
return return
} }
Stores.Chronic(3 * 1000).addCallback(() => { Stores.chronic(3 * 1000).addCallback(() => {
if (!(this.apiIsOnline.data === "unreachable" || this.apiIsOnline.data === "offline")) { if (!(this.apiIsOnline.data === "unreachable" || this.apiIsOnline.data === "offline")) {
return return
} }
@ -699,7 +699,7 @@ export class OsmConnection {
if (!this._doCheckRegularly) { if (!this._doCheckRegularly) {
return return
} }
Stores.Chronic(60 * 5 * 1000).addCallback(() => { Stores.chronic(60 * 5 * 1000).addCallback(() => {
// Check for new messages every 5 minutes // Check for new messages every 5 minutes
if (this.isLoggedIn.data) { if (this.isLoggedIn.data) {
try { try {

View file

@ -40,6 +40,6 @@ export class NominatimGeocoding implements GeocodingProvider {
} }
suggest(query: string, options?: GeocodingOptions): Store<{ success: GeocodeResult[] } | { error: any }> { suggest(query: string, options?: GeocodingOptions): Store<{ success: GeocodeResult[] } | { error: any }> {
return UIEventSource.FromPromiseWithErr(this.search(query, options)) return UIEventSource.fromPromiseWithErr(this.search(query, options))
} }
} }

View file

@ -93,6 +93,6 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
} }
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error: any}> { suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error: any}> {
return UIEventSource.FromPromiseWithErr(this.search(query, options)) return UIEventSource.fromPromiseWithErr(this.search(query, options))
} }
} }

View file

@ -11,7 +11,7 @@ import { Utils } from "../../Utils"
import { Feature, FeatureCollection } from "geojson" import { Feature, FeatureCollection } from "geojson"
import Locale from "../../UI/i18n/Locale" import Locale from "../../UI/i18n/Locale"
import { GeoOperations } from "../GeoOperations" import { GeoOperations } from "../GeoOperations"
import { Store, Stores } from "../UIEventSource" import { Store, UIEventSource } from "../UIEventSource"
export default class PhotonSearch implements GeocodingProvider, ReverseGeocodingProvider { export default class PhotonSearch implements GeocodingProvider, ReverseGeocodingProvider {
private readonly _endpoint: string private readonly _endpoint: string
@ -74,7 +74,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
} }
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error: any}> { suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error: any}> {
return Stores.FromPromiseWithErr(this.search(query, options)) return UIEventSource.fromPromiseWithErr(this.search(query, options))
} }
private buildDescription(entry: Feature) { private buildDescription(entry: Feature) {

View file

@ -97,7 +97,7 @@ export class ThemeSearchIndex {
theme: ThemeConfig theme: ThemeConfig
}): Store<ThemeSearchIndex> { }): Store<ThemeSearchIndex> {
const layersToIgnore = state.theme.layers.filter((l) => l.isNormal()).map((l) => l.id) const layersToIgnore = state.theme.layers.filter((l) => l.isNormal()).map((l) => l.id)
const knownHidden: Store<string[]> = Stores.ListStabilized( const knownHidden: Store<string[]> = Stores.listStabilized(
UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection) UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection)
.stabilized(1000) .stabilized(1000)
.map((list) => Utils.Dedup(list)) .map((list) => Utils.Dedup(list))

View file

@ -53,8 +53,13 @@ class RoundRobinStore<T> {
* @param t * @param t
*/ */
public add(t: T) { public add(t: T) {
const i = this._index.data const i = this._index.data ?? 0
this._index.set((i + 1) % this._maxCount) if(isNaN(Number(i))){
this._index.set(0)
this.add(t)
return
}
this._index.set((Math.max(i,0) + 1) % this._maxCount)
this._store.data[i] = t this._store.data[i] = t
this._store.ping() this._store.ping()
} }
@ -84,19 +89,17 @@ export class OptionallySyncedHistory<T extends object | string> {
defaultValue: "sync", defaultValue: "sync",
}) })
this.syncedBackingStore = Stores.fromArray( this.syncedBackingStore = UIEventSource.concat(Utils.TimesT(maxHistory, (i) => {
Utils.TimesT(maxHistory, (i) => { const pref = osmconnection.getPreference(key + "-hist-" + i + "-")
const pref = osmconnection.getPreference(key + "-hist-" + i + "-") return UIEventSource.asObject<T>(pref, undefined)
return UIEventSource.asObject<T>(pref, undefined) }))
})
)
const ringIndex = UIEventSource.asInt( const ringIndex = UIEventSource.asInt(
osmconnection.getPreference(key + "-hist-round-robin", { osmconnection.getPreference(key + "-hist-round-robin", {
defaultValue: "0", defaultValue: "0",
}) })
) )
this.syncedOrdered = new RoundRobinStore<T>(this.syncedBackingStore, ringIndex, 10) this.syncedOrdered = new RoundRobinStore<T>(this.syncedBackingStore, ringIndex, maxHistory)
const local = (this.local = LocalStorageSource.getParsed<T[]>(key + "-history", [])) const local = (this.local = LocalStorageSource.getParsed<T[]>(key + "-history", []))
const thisSession = (this.thisSession = new UIEventSource<T[]>( const thisSession = (this.thisSession = new UIEventSource<T[]>(
[], [],
@ -170,10 +173,10 @@ export class OptionallySyncedHistory<T extends object | string> {
} }
/** /**
* Adds the value when the user is actually logged in * Sets the value when the user is actually logged in
* @param t * @param t
*/ */
public addDefferred(t: T) { public addDeferred(t: T) {
if (t === undefined) { if (t === undefined) {
return return
} }
@ -217,7 +220,7 @@ export default class UserRelatedState {
public readonly fixateNorth: UIEventSource<undefined | "yes"> public readonly fixateNorth: UIEventSource<undefined | "yes">
public readonly a11y: UIEventSource<undefined | "always" | "never" | "default"> public readonly a11y: UIEventSource<undefined | "always" | "never" | "default">
public readonly homeLocation: FeatureSource public readonly homeLocation: FeatureSource<Feature>
public readonly morePrivacy: UIEventSource<undefined | "yes" | "no"> public readonly morePrivacy: UIEventSource<undefined | "yes" | "no">
/** /**
* The language as saved into the preferences of the user, if logged in. * The language as saved into the preferences of the user, if logged in.
@ -335,7 +338,7 @@ export default class UserRelatedState {
(a, b) => a.osm_id === b.osm_id && a.osm_type === b.osm_type (a, b) => a.osm_id === b.osm_id && a.osm_type === b.osm_type
) )
this.syncLanguage() this.syncLanguage()
this.recentlyVisitedThemes.addDefferred(layout?.id) this.recentlyVisitedThemes.addDeferred(layout?.id)
} }
private syncLanguage() { private syncLanguage() {
@ -461,9 +464,9 @@ export default class UserRelatedState {
) )
} }
private initHomeLocation(): FeatureSource { private initHomeLocation(): FeatureSource<Feature> {
const empty = [] const empty = []
const feature: Store<Feature[]> = Stores.ListStabilized( const feature: Store<Feature[]> = Stores.listStabilized(
this.osmConnection.userDetails.map((userDetails) => { this.osmConnection.userDetails.map((userDetails) => {
if (userDetails === undefined) { if (userDetails === undefined) {
return undefined return undefined

View file

@ -5,7 +5,7 @@ import { Readable, Subscriber, Unsubscriber, Updater, Writable } from "svelte/st
* Various static utils * Various static utils
*/ */
export class Stores { export class Stores {
public static Chronic(millis: number, asLong: () => boolean = undefined): Store<Date> { public static chronic(millis: number, asLong: () => boolean = undefined): Store<Date> {
const source = new UIEventSource<Date>(undefined) const source = new UIEventSource<Date>(undefined)
function run() { function run() {
@ -23,32 +23,12 @@ export class Stores {
return source return source
} }
public static FromPromiseWithErr<T>( public static concat<T>(stores: ReadonlyArray<Store<T | undefined>>): Store<(T | undefined)[]> ;
promise: Promise<T> public static concat<T>(stores: ReadonlyArray<Store<T>>): Store<T[]> ;
): Store<{ success: T } | { error: any }> { public static concat<T>(stores: ReadonlyArray<Store<T | undefined>>): Store<(T | undefined)[]> {
return UIEventSource.FromPromiseWithErr(promise) const newStore = new UIEventSource<(T | undefined)[]>(
} stores.map(store => store?.data),
)
/**
* 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>): Store<T | undefined> {
const src = new UIEventSource<T>(undefined)
promise
?.catch((err): undefined => {
console.warn("Promise failed:", err)
return undefined
})
?.then((d) => src.setData(d))
return src
}
public static concat<T>(stores: Store<T | undefined>[]): Store<(T | undefined)[]> ;
public static concat<T>(stores: Store<T>[]): Store<T[]> ;
public static concat<T>(stores: Store<T | undefined>[]): Store<(T | undefined)[]> {
const newStore = new UIEventSource<(T | undefined)[]>([])
function update() { function update() {
if (newStore._callbacks.isDestroyed) { if (newStore._callbacks.isDestroyed) {
@ -64,7 +44,6 @@ export class Stores {
for (const store of stores) { for (const store of stores) {
store.addCallback(() => update()) store.addCallback(() => update())
} }
update()
return newStore return newStore
} }
@ -82,7 +61,7 @@ export class Stores {
* @param src * @param src
* @constructor * @constructor
*/ */
public static ListStabilized<T>(src: Store<T[]>): Store<T[]> { public static listStabilized<T>(src: Store<T[]>): Store<T[]> {
const stable = new UIEventSource<T[]>(undefined) const stable = new UIEventSource<T[]>(undefined)
src.addCallbackAndRun((list) => { src.addCallbackAndRun((list) => {
if (list === undefined) { if (list === undefined) {
@ -110,33 +89,6 @@ export class Stores {
}) })
return newStore return newStore
} }
public static fromArray<T>(sources: ReadonlyArray<UIEventSource<T>>): UIEventSource<T[]> {
const src = new UIEventSource<T[]>(sources.map((s) => s.data))
for (let i = 0; i < sources.length; i++) {
sources[i].addCallback((content) => {
src.data[i] = content
src.ping()
})
}
src.addCallbackD((contents) => {
for (let i = 0; i < contents.length; i++) {
sources[i]?.setData(contents[i])
}
})
return src
}
public static fromStoresArray<T>(sources: ReadonlyArray<Store<T>>): Store<T[]> {
const src = new UIEventSource<T[]>(sources.map((s) => s.data))
for (let i = 0; i < sources.length; i++) {
sources[i].addCallback((content) => {
src.data[i] = content
src.ping()
})
}
return src
}
} }
export abstract class Store<T> implements Readable<T> { export abstract class Store<T> implements Readable<T> {
@ -162,17 +114,26 @@ export abstract class Store<T> implements Readable<T> {
} }
abstract map<J>(f: (t: T) => J): Store<J> abstract map<J>(f: (t: T) => J): Store<J>
abstract map<J>(f: (t: T) => J, extraStoresToWatch: Store<unknown>[]): Store<J> abstract map<J>(f: (t: T) => J, callbackDestroyFunction: (f: () => void) => void): Store<J>
abstract map<J>( abstract map<J>(
f: (t: T) => J, f: (t: T) => J,
extraStoresToWatch: Store<unknown>[], extraStoresToWatch: Store<unknown>[],
callbackDestroyFunction: (f: () => void) => void callbackDestroyFunction?: (f: () => void) => void,
): Store<J> ): Store<J>
public mapD<J>( public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J, f: (t: Exclude<T, undefined | null>) => J,
extraStoresToWatch?: Store<unknown>[], extraStoresToWatch?: Store<unknown>[],
callbackDestroyFunction?: (f: () => void) => void callbackDestroyFunction?: (f: () => void) => void,
): Store<J>
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
callbackDestroyFunction?: (f: () => void) => void,
): Store<J>
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraStoresToWatch?: Store<unknown>[] | ((f: () => void) => void),
callbackDestroyFunction?: (f: () => void) => void,
): Store<J> { ): Store<J> {
return this.map( return this.map(
(t) => { (t) => {
@ -184,8 +145,8 @@ export abstract class Store<T> implements Readable<T> {
} }
return f(<Exclude<T, undefined | null>>t) return f(<Exclude<T, undefined | null>>t)
}, },
extraStoresToWatch, typeof extraStoresToWatch === "function" ? [] : extraStoresToWatch,
callbackDestroyFunction callbackDestroyFunction ?? (typeof extraStoresToWatch === "function" ? extraStoresToWatch : undefined),
) )
} }
@ -212,22 +173,6 @@ export abstract class Store<T> implements Readable<T> {
*/ */
abstract addCallbackAndRun(callback: (data: T) => void): () => void abstract addCallbackAndRun(callback: (data: T) => void): () => void
public withEqualityStabilized(
comparator: (t: T | undefined, t1: T | undefined) => boolean
): Store<T> {
let oldValue = undefined
return this.map((v) => {
if (v == oldValue) {
return oldValue
}
if (comparator(oldValue, v)) {
return oldValue
}
oldValue = v
return v
})
}
/** /**
* Monadic bind function * Monadic bind function
* *
@ -274,8 +219,9 @@ export abstract class Store<T> implements Readable<T> {
* src.setData(0) * src.setData(0)
* lastValue // => "def" * lastValue // => "def"
*/ */
public bind<X>(f: (t: T) => Store<X>, extraSources: Store<unknown>[] = []): Store<X> { public bind<X>(f: (t: T) => Store<X>, extraSources?: Store<unknown>[] | ((f: () => void) => void), onDestroy?: (f : () => void) => void): Store<X> {
const mapped = this.map(f, extraSources) const mapped = this.map(f, typeof extraSources === "function" ? undefined : extraSources,
onDestroy ?? (typeof extraSources === "function" ? extraSources : undefined))
const sink = new UIEventSource<X>(undefined) const sink = new UIEventSource<X>(undefined)
const seenEventSources = new Set<Store<X>>() const seenEventSources = new Set<Store<X>>()
mapped.addCallbackAndRun((newEventSource) => { mapped.addCallbackAndRun((newEventSource) => {
@ -308,7 +254,8 @@ export abstract class Store<T> implements Readable<T> {
public bindD<X>( public bindD<X>(
f: (t: Exclude<T, undefined | null>) => Store<X>, f: (t: Exclude<T, undefined | null>) => Store<X>,
extraSources: Store<unknown>[] = [] extraSources?: Store<unknown>[],
onDestroy?: ((f: () => void) => void)
): Store<X> { ): Store<X> {
return this.bind((t) => { return this.bind((t) => {
if (t === null) { if (t === null) {
@ -318,7 +265,7 @@ export abstract class Store<T> implements Readable<T> {
return undefined return undefined
} }
return f(<Exclude<T, undefined | null>>t) return f(<Exclude<T, undefined | null>>t)
}, extraSources) }, extraSources, onDestroy)
} }
public stabilized(millisToStabilize): Store<T> { public stabilized(millisToStabilize): Store<T> {
@ -402,7 +349,8 @@ export class ImmutableStore<T> extends Store<T> {
this.data = data this.data = data
} }
private static readonly pass: () => void = () => {} private static readonly pass: () => void = () => {
}
addCallback(_: (data: T) => void): () => void { addCallback(_: (data: T) => void): () => void {
// pass: data will never change // pass: data will never change
@ -430,8 +378,8 @@ export class ImmutableStore<T> extends Store<T> {
map<J>( map<J>(
f: (t: T) => J, f: (t: T) => J,
extraStores: Store<any>[] = undefined, extraStores: Store<any>[] | ((f: () => void) => void)= undefined,
ondestroyCallback?: (f: () => void) => void ondestroyCallback?: (f: () => void) => void,
): ImmutableStore<J> { ): ImmutableStore<J> {
if (extraStores?.length > 0) { if (extraStores?.length > 0) {
return new MappedStore(this, f, extraStores, undefined, f(this.data), ondestroyCallback) return new MappedStore(this, f, extraStores, undefined, f(this.data), ondestroyCallback)
@ -482,7 +430,6 @@ class ListenerTracker<T> {
public ping(data: T): number { public ping(data: T): number {
this.pingCount++ this.pingCount++
let toDelete = undefined let toDelete = undefined
const startTime = new Date().getTime() / 1000
for (const callback of this._callbacks) { for (const callback of this._callbacks) {
try { try {
if (callback(data) === true) { if (callback(data) === true) {
@ -498,12 +445,6 @@ class ListenerTracker<T> {
console.error("Got an error while running a callback:", e) console.error("Got an error while running a callback:", e)
} }
} }
const endTime = new Date().getTime() / 1000
if (endTime - startTime > 500) {
console.trace(
"Warning: a ping took more then 500ms; this is probably a performance issue"
)
}
if (toDelete !== undefined) { if (toDelete !== undefined) {
for (const toDeleteElement of toDelete) { for (const toDeleteElement of toDelete) {
this._callbacks.splice(this._callbacks.indexOf(toDeleteElement), 1) this._callbacks.splice(this._callbacks.indexOf(toDeleteElement), 1)
@ -529,6 +470,11 @@ class MappedStore<TIn, T> extends Store<T> {
private static readonly pass: () => void private static readonly pass: () => void
private readonly _upstream: Store<TIn> private readonly _upstream: Store<TIn>
private readonly _upstreamCallbackHandler: ListenerTracker<TIn> | undefined private readonly _upstreamCallbackHandler: ListenerTracker<TIn> | undefined
/**
* THe last 'pingcount' (aka 'tick') that the upstream did.
* If we get a request for the data, we check if it is still up to date with this
* @private
*/
private _upstreamPingCount: number = -1 private _upstreamPingCount: number = -1
private _unregisterFromUpstream: () => void private _unregisterFromUpstream: () => void
private readonly _f: (t: TIn) => T private readonly _f: (t: TIn) => T
@ -540,10 +486,10 @@ class MappedStore<TIn, T> extends Store<T> {
constructor( constructor(
upstream: Store<TIn>, upstream: Store<TIn>,
f: (t: TIn) => T, f: (t: TIn) => T,
extraStores: Store<unknown>[], extraStores: Store<unknown>[] | ((t: () => void) => void),
upstreamListenerHandler: ListenerTracker<TIn> | undefined, upstreamListenerHandler: ListenerTracker<TIn> | undefined,
initialState: T, initialState: T,
onDestroy?: (f: () => void) => void onDestroy?: (f: () => void) => void,
) { ) {
super() super()
this._upstream = upstream this._upstream = upstream
@ -551,7 +497,11 @@ class MappedStore<TIn, T> extends Store<T> {
this._f = f this._f = f
this._data = initialState this._data = initialState
this._upstreamPingCount = upstreamListenerHandler?.pingCount this._upstreamPingCount = upstreamListenerHandler?.pingCount
this._extraStores = extraStores if (typeof extraStores === "function") {
onDestroy ??= extraStores
} else {
this._extraStores = extraStores
}
this.registerCallbacksToUpstream() this.registerCallbacksToUpstream()
if (onDestroy !== undefined) { if (onDestroy !== undefined) {
onDestroy(() => this.unregisterFromUpstream()) onDestroy(() => this.unregisterFromUpstream())
@ -572,7 +522,7 @@ class MappedStore<TIn, T> extends Store<T> {
if (!this._callbacksAreRegistered) { if (!this._callbacksAreRegistered) {
// Callbacks are not registered, so we haven't been listening for updates from the upstream which might have changed // Callbacks are not registered, so we haven't been listening for updates from the upstream which might have changed
if (this._upstreamCallbackHandler?.pingCount != this._upstreamPingCount) { if (this._upstreamCallbackHandler?.pingCount != this._upstreamPingCount) {
// Upstream has pinged - let's update our data first // Upstream has pinged at least once - let's update our data first
this._data = this._f(this._upstream.data) this._data = this._f(this._upstream.data)
} }
return this._data return this._data
@ -582,30 +532,34 @@ class MappedStore<TIn, T> extends Store<T> {
map<J>( map<J>(
f: (t: T) => J, f: (t: T) => J,
extraStores: Store<unknown>[] = undefined, extraStores: Store<unknown>[] | ((f: () => void) => void) = undefined,
ondestroyCallback?: (f: () => void) => void ondestroyCallback?: (f: () => void) => void,
): Store<J> { ): Store<J> {
let stores: Store<unknown>[] = undefined let stores: Store<unknown>[] = undefined
if (extraStores?.length > 0 || this._extraStores?.length > 0) { if (typeof extraStores !== "function") {
stores = []
} if (extraStores?.length > 0 || this._extraStores?.length > 0) {
if (extraStores?.length > 0) { stores = []
stores?.push(...extraStores) }
} if (extraStores?.length > 0) {
if (this._extraStores?.length > 0) { stores?.push(...extraStores)
this._extraStores?.forEach((store) => { }
if (stores.indexOf(store) < 0) { if (this._extraStores?.length > 0) {
stores.push(store) this._extraStores?.forEach((store) => {
} if (stores.indexOf(store) < 0) {
}) stores.push(store)
}
})
}
} }
return new MappedStore( return new MappedStore(
this, this,
f, // we could fuse the functions here (e.g. data => f(this._f(data), but this might result in _f being calculated multiple times, breaking things f, // we could fuse the functions here (e.g. data => f(this._f(data), but this might result in _f being calculated multiple times, breaking things
stores, stores,
this._callbacks, this._callbacks,
f(this.data), f(this.data),
ondestroyCallback ondestroyCallback,
) )
} }
@ -651,7 +605,12 @@ class MappedStore<TIn, T> extends Store<T> {
} }
private unregisterFromUpstream() { private unregisterFromUpstream() {
if (!this._callbacksAreRegistered) {
return
}
this._upstreamPingCount = this._upstreamCallbackHandler.pingCount
this._callbacksAreRegistered = false this._callbacksAreRegistered = false
console.log("Unregistering from upstream", this._upstream)
this._unregisterFromUpstream() this._unregisterFromUpstream()
this._unregisterFromExtraStores?.forEach((unr) => unr()) this._unregisterFromExtraStores?.forEach((unr) => unr())
} }
@ -659,7 +618,7 @@ class MappedStore<TIn, T> extends Store<T> {
private registerCallbacksToUpstream() { private registerCallbacksToUpstream() {
this._unregisterFromUpstream = this._upstream.addCallback(() => this.update()) this._unregisterFromUpstream = this._upstream.addCallback(() => this.update())
this._unregisterFromExtraStores = this._extraStores?.map((store) => this._unregisterFromExtraStores = this._extraStores?.map((store) =>
store?.addCallback(() => this.update()) store?.addCallback(() => this.update()),
) )
this._callbacksAreRegistered = true this._callbacksAreRegistered = true
} }
@ -680,7 +639,8 @@ class MappedStore<TIn, T> extends Store<T> {
} }
export class UIEventSource<T> extends Store<T> implements Writable<T> { export class UIEventSource<T> extends Store<T> implements Writable<T> {
private static readonly pass: () => void = () => {} private static readonly pass: () => void = () => {
}
public data: T public data: T
_callbacks: ListenerTracker<T> = new ListenerTracker<T>() _callbacks: ListenerTracker<T> = new ListenerTracker<T>()
@ -695,7 +655,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
public static flatten<X>( public static flatten<X>(
source: Store<Store<X>>, source: Store<Store<X>>,
possibleSources?: Store<object>[] possibleSources?: Store<object>[],
): UIEventSource<X> { ): UIEventSource<X> {
const sink = new UIEventSource<X>(source.data?.data) const sink = new UIEventSource<X>(source.data?.data)
@ -722,9 +682,9 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
* Converts a promise into a UIventsource, sets the UIeventSource when the result is calculated. * 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 * If the promise fails, the value will stay undefined, but 'onError' will be called
*/ */
public static FromPromise<T>( public static fromPromise<T>(
promise: Promise<T>, promise: Promise<T>,
onError: (e) => void = undefined onError: (e) => void = undefined,
): UIEventSource<T> { ): UIEventSource<T> {
const src = new UIEventSource<T>(undefined) const src = new UIEventSource<T>(undefined)
promise?.then((d) => src.setData(d)) promise?.then((d) => src.setData(d))
@ -744,8 +704,8 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
* @param promise * @param promise
* @constructor * @constructor
*/ */
public static FromPromiseWithErr<T>( public static fromPromiseWithErr<T>(
promise: Promise<T> promise: Promise<T>,
): UIEventSource<{ success: T } | { error: any } | undefined> { ): UIEventSource<{ success: T } | { error: any } | undefined> {
const src = new UIEventSource<{ success: T } | { error: any }>(undefined) const src = new UIEventSource<{ success: T } | { error: any }>(undefined)
promise promise
@ -778,7 +738,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return undefined return undefined
} }
return "" + fl return "" + fl
} },
) )
} }
@ -809,7 +769,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return undefined return undefined
} }
return "" + fl return "" + fl
} },
) )
} }
@ -817,13 +777,13 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return stringUIEventSource.sync( return stringUIEventSource.sync(
(str) => str === "true", (str) => str === "true",
[], [],
(b) => "" + b (b) => "" + b,
) )
} }
static asObject<T extends object | string>( static asObject<T extends object | string>(
stringUIEventSource: UIEventSource<string>, stringUIEventSource: UIEventSource<string>,
defaultV: T defaultV: T,
): UIEventSource<T> { ): UIEventSource<T> {
return stringUIEventSource.sync( return stringUIEventSource.sync(
(str) => { (str) => {
@ -839,13 +799,13 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
"due to", "due to",
e, e,
"; the underlying data store has tag", "; the underlying data store has tag",
stringUIEventSource.tag stringUIEventSource.tag,
) )
return defaultV return defaultV
} }
}, },
[], [],
(b) => JSON.stringify(b) ?? "" (b) => JSON.stringify(b) ?? "",
) )
} }
@ -934,8 +894,17 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
*/ */
public map<J>( public map<J>(
f: (t: T) => J, f: (t: T) => J,
extraSources: Store<unknown>[] = [], extraSources?: Store<unknown>[],
onDestroy?: (f: () => void) => void onDestroy?: (f: () => void) => void,
)
public map<J>(
f: (t: T) => J,
onDestroy: (f: () => void) => void,
)
public map<J>(
f: (t: T) => J,
extraSources: Store<unknown>[] | ((f: () => void) => void),
onDestroy?: (f: () => void) => void,
): Store<J> { ): Store<J> {
return new MappedStore(this, f, extraSources, this._callbacks, f(this.data), onDestroy) return new MappedStore(this, f, extraSources, this._callbacks, f(this.data), onDestroy)
} }
@ -946,8 +915,8 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
*/ */
public mapD<J>( public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J, f: (t: Exclude<T, undefined | null>) => J,
extraSources: Store<unknown>[] = [], extraSources?: Store<unknown>[] | ((f: () => void) => void),
callbackDestroyFunction?: (f: () => void) => void callbackDestroyFunction?: (f: () => void) => void,
): Store<J | undefined> { ): Store<J | undefined> {
return new MappedStore( return new MappedStore(
this, this,
@ -965,12 +934,12 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
this.data === undefined || this.data === null this.data === undefined || this.data === null
? <undefined | null>this.data ? <undefined | null>this.data
: f(<Exclude<T, undefined | null>>this.data), : f(<Exclude<T, undefined | null>>this.data),
callbackDestroyFunction callbackDestroyFunction,
) )
} }
public mapAsyncD<J>(f: (t: T) => Promise<J>): Store<J> { public mapAsyncD<J>(f: (t: T) => Promise<J>): Store<J> {
return this.bindD((t) => UIEventSource.FromPromise(f(t))) return this.bindD((t) => UIEventSource.fromPromise(f(t)))
} }
/** /**
@ -985,7 +954,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
f: (t: T) => J, f: (t: T) => J,
extraSources: Store<unknown>[], extraSources: Store<unknown>[],
g: (j: J, t: T) => T, g: (j: J, t: T) => T,
allowUnregister = false allowUnregister = false,
): UIEventSource<J> { ): UIEventSource<J> {
const stack = new Error().stack.split("\n") const stack = new Error().stack.split("\n")
const callee = stack[1] const callee = stack[1]
@ -1033,4 +1002,16 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
update(f: Updater<T> & ((value: T) => T)): void { update(f: Updater<T> & ((value: T) => T)): void {
this.setData(f(this.data)) this.setData(f(this.data))
} }
public static concat<T>(stores: ReadonlyArray<UIEventSource<T | undefined>>): UIEventSource<(T | undefined)[]> ;
public static concat<T>(stores: ReadonlyArray<UIEventSource<T>>): UIEventSource<T[]> ;
public static concat<T>(stores: ReadonlyArray<UIEventSource<T | undefined>>): UIEventSource<(T | undefined)[]> {
const newStore = <UIEventSource<T[]>>Stores.concat(stores)
newStore.addCallbackD(list => {
for (let i = 0; i < list.length; i++) {
stores[i]?.setData(list[i])
}
})
return newStore
}
} }

View file

@ -39,7 +39,7 @@ export class AndroidPolyfill {
* @private * @private
*/ */
private static backfillGeolocation(databridgePlugin: DatabridgePlugin) { private static backfillGeolocation(databridgePlugin: DatabridgePlugin) {
const src = UIEventSource.FromPromise( const src = UIEventSource.fromPromise(
databridgePlugin.request({ key: "location:has-permission" }) databridgePlugin.request({ key: "location:has-permission" })
) )
src.addCallbackAndRunD((permission) => { src.addCallbackAndRunD((permission) => {

View file

@ -173,7 +173,7 @@ export default class Wikidata {
if (cached) { if (cached) {
return cached return cached
} }
const src = UIEventSource.FromPromiseWithErr(Wikidata.LoadWikidataEntryAsync(key)) const src = UIEventSource.fromPromiseWithErr(Wikidata.LoadWikidataEntryAsync(key))
Wikidata._storeCache.set(key, src) Wikidata._storeCache.set(key, src)
return src return src
} }

View file

@ -90,14 +90,14 @@ export class AvailableRasterLayers {
location: Store<{ lon: number; lat: number }>, location: Store<{ lon: number; lat: number }>,
enableBing?: Store<boolean> enableBing?: Store<boolean>
): Store<RasterLayerPolygon[]> { ): Store<RasterLayerPolygon[]> {
const availableLayersBboxes = Stores.ListStabilized( const availableLayersBboxes = Stores.listStabilized(
location.mapD((loc) => { location.mapD((loc) => {
const eli = AvailableRasterLayers._editorLayerIndex const eli = AvailableRasterLayers._editorLayerIndex
const lonlat: [number, number] = [loc.lon, loc.lat] const lonlat: [number, number] = [loc.lon, loc.lat]
return eli.filter((eliPolygon) => BBox.get(eliPolygon).contains(lonlat)) return eli.filter((eliPolygon) => BBox.get(eliPolygon).contains(lonlat))
}) })
) )
return Stores.ListStabilized( return Stores.listStabilized(
availableLayersBboxes.mapD( availableLayersBboxes.mapD(
(eliPolygons) => { (eliPolygons) => {
const loc = location.data const loc = location.data

View file

@ -1167,7 +1167,7 @@ export class TagRenderingConfigUtils {
return undefined return undefined
} }
const center = GeoOperations.centerpointCoordinates(feature) const center = GeoOperations.centerpointCoordinates(feature)
return UIEventSource.FromPromise( return UIEventSource.fromPromise(
NameSuggestionIndex.generateMappings( NameSuggestionIndex.generateMappings(
config.freeform.key, config.freeform.key,
tags, tags,

View file

@ -48,7 +48,7 @@ export class Orientation {
if (rotateAlpha) { if (rotateAlpha) {
this._animateFakeMeasurements = true this._animateFakeMeasurements = true
Stores.Chronic(25).addCallback((date) => { Stores.chronic(25).addCallback((date) => {
this.alpha.setData((date.getTime() / 50) % 360) this.alpha.setData((date.getTime() / 50) % 360)
if (!this._animateFakeMeasurements) { if (!this._animateFakeMeasurements) {
return true return true

View file

@ -78,7 +78,7 @@
) )
) )
const customThemes: Store<MinimalThemeInformation[]> = Stores.ListStabilized<string>( const customThemes: Store<MinimalThemeInformation[]> = Stores.listStabilized<string>(
state.installedUserThemes.stabilized(1000) state.installedUserThemes.stabilized(1000)
).mapD((stableIds) => Utils.NoNullInplace(stableIds.map((id) => state.getUnofficialTheme(id)))) ).mapD((stableIds) => Utils.NoNullInplace(stableIds.map((id) => state.getUnofficialTheme(id))))

View file

@ -16,6 +16,7 @@
import { ariaLabelStore } from "../../Utils/ariaLabel" import { ariaLabelStore } from "../../Utils/ariaLabel"
import type { SpecialVisualizationState } from "../SpecialVisualization" import type { SpecialVisualizationState } from "../SpecialVisualization"
import Center from "../../assets/svg/Center.svelte" import Center from "../../assets/svg/Center.svelte"
import { onDestroy } from "svelte"
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
export let feature: Feature export let feature: Feature
@ -31,9 +32,7 @@
return { bearing, dist } return { bearing, dist }
} }
) )
let bearingFromGps = state.geolocation.geolocationState.currentGPSLocation.mapD((coordinate) => { let bearingFromGps = state.geolocation.geolocationState.currentGPSLocation.mapD((coordinate) => GeoOperations.bearing([coordinate.longitude, coordinate.latitude], fcenter),onDestroy)
return GeoOperations.bearing([coordinate.longitude, coordinate.latitude], fcenter)
})
let compass = Orientation.singleton.alpha let compass = Orientation.singleton.alpha
let relativeDirections = Translations.t.general.visualFeedback.directionsRelative let relativeDirections = Translations.t.general.visualFeedback.directionsRelative
@ -80,11 +79,11 @@
} }
| undefined | undefined
> = state.geolocation.geolocationState.currentGPSLocation.mapD(({ longitude, latitude }) => { > = state.geolocation.geolocationState.currentGPSLocation.mapD(({ longitude, latitude }) => {
let gps = [longitude, latitude] const gps = [longitude, latitude]
let bearing = Math.round(GeoOperations.bearing(gps, fcenter)) const bearing = Math.round(GeoOperations.bearing(gps, fcenter))
let dist = round10(Math.round(GeoOperations.distanceBetween(fcenter, gps))) const dist = round10(Math.round(GeoOperations.distanceBetween(fcenter, gps)))
return { bearing, dist } return { bearing, dist }
}) }, onDestroy)
let labelFromGps: Store<string | undefined> = bearingAndDistGps.mapD( let labelFromGps: Store<string | undefined> = bearingAndDistGps.mapD(
({ bearing, dist }) => { ({ bearing, dist }) => {
const distHuman = GeoOperations.distanceToHuman(dist) const distHuman = GeoOperations.distanceToHuman(dist)
@ -103,7 +102,7 @@
}) })
return mainTr.textFor(lang) return mainTr.textFor(lang)
}, },
[compass, Locale.language] [compass, Locale.language], onDestroy
) )
let label = labelFromCenter.map( let label = labelFromCenter.map(
@ -116,7 +115,7 @@
} }
return labelFromCenter return labelFromCenter
}, },
[labelFromGps] [labelFromGps], onDestroy
) )
function focusMap() { function focusMap() {

View file

@ -16,7 +16,7 @@
let layer = state.getMatchingLayer(selected.properties) let layer = state.getMatchingLayer(selected.properties)
let stillMatches = tags.map( let stillMatches = tags.map(
(tags) => !layer?.source?.osmTags || layer?.source?.osmTags?.matchesProperties(tags) (tags) => !layer?.source?.osmTags || layer?.source?.osmTags?.matchesProperties(tags), onDestroy
) )
onDestroy( onDestroy(
stillMatches.addCallbackAndRunD((matches) => { stillMatches.addCallbackAndRunD((matches) => {

View file

@ -45,7 +45,7 @@
) )
const globalResources: UIEventSource<Record<string, CommunityResource>> = const globalResources: UIEventSource<Record<string, CommunityResource>> =
UIEventSource.FromPromise( UIEventSource.fromPromise(
Utils.downloadJsonCached<Record<string, CommunityResource>>( Utils.downloadJsonCached<Record<string, CommunityResource>>(
Constants.communityIndexHost + "/global.json", Constants.communityIndexHost + "/global.json",
24 * 60 * 60 * 1000 24 * 60 * 60 * 1000

View file

@ -61,6 +61,7 @@
import QueuedImagesView from "../Image/QueuedImagesView.svelte" import QueuedImagesView from "../Image/QueuedImagesView.svelte"
import InsetSpacer from "../Base/InsetSpacer.svelte" import InsetSpacer from "../Base/InsetSpacer.svelte"
import UserCircle from "@rgossiaux/svelte-heroicons/solid/UserCircle" import UserCircle from "@rgossiaux/svelte-heroicons/solid/UserCircle"
import { onDestroy } from "svelte"
export let state: { export let state: {
favourites: FavouritesFeatureSource favourites: FavouritesFeatureSource
@ -212,7 +213,7 @@
</LoginToggle> </LoginToggle>
<LanguagePicker <LanguagePicker
preferredLanguages={state.userRelatedState.osmConnection.userDetails.mapD( preferredLanguages={state.userRelatedState.osmConnection.userDetails.mapD(
(ud) => ud.languages (ud) => ud.languages, onDestroy
)} )}
/> />
</SidebarUnit> </SidebarUnit>

View file

@ -23,7 +23,7 @@
) )
let isAddNew = tags.mapD( let isAddNew = tags.mapD(
(t) => t?.id?.startsWith(LastClickFeatureSource.newPointElementId) ?? false (t) => t?.id?.startsWith(LastClickFeatureSource.newPointElementId) ?? false, onDestroy
) )
export let layer: LayerConfig export let layer: LayerConfig
@ -59,7 +59,7 @@
(config.condition?.matchesProperties(tgs) ?? true) && (config.condition?.matchesProperties(tgs) ?? true) &&
(config.metacondition?.matchesProperties({ ...tgs, ..._metatags }) ?? true) (config.metacondition?.matchesProperties({ ...tgs, ..._metatags }) ?? true)
) )
}) }, onDestroy)
) )
</script> </script>

View file

@ -2,11 +2,12 @@
import { Popover } from "flowbite-svelte" import { Popover } from "flowbite-svelte"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { onDestroy } from "svelte"
let open = false let open = false
export let state: { osmConnection: OsmConnection } export let state: { osmConnection: OsmConnection }
let userdetails = state.osmConnection.userDetails let userdetails = state.osmConnection.userDetails
let username = userdetails.mapD((ud) => ud.name) let username = userdetails.mapD((ud) => ud.name, onDestroy)
username.addCallbackAndRunD((ud) => { username.addCallbackAndRunD((ud) => {
if (ud) { if (ud) {
open = true open = true

View file

@ -11,7 +11,7 @@
export let features: Feature[] export let features: Feature[]
const downloader = new OsmObjectDownloader() const downloader = new OsmObjectDownloader()
let allHistories: UIEventSource<OsmObject[][]> = UIEventSource.FromPromise( let allHistories: UIEventSource<OsmObject[][]> = UIEventSource.fromPromise(
Promise.all(features.map((f) => downloader.downloadHistory(f.properties.id))) Promise.all(features.map((f) => downloader.downloadHistory(f.properties.id)))
) )
let imageKeys = new Set( let imageKeys = new Set(

View file

@ -22,7 +22,7 @@
let usernames = new Set(onlyShowUsername) let usernames = new Set(onlyShowUsername)
const downloader = new OsmObjectDownloader() const downloader = new OsmObjectDownloader()
let allHistories: UIEventSource<OsmObject[][]> = UIEventSource.FromPromise( let allHistories: UIEventSource<OsmObject[][]> = UIEventSource.fromPromise(
Promise.all(features.map((f) => downloader.downloadHistory(f.properties.id))) Promise.all(features.map((f) => downloader.downloadHistory(f.properties.id)))
) )
let allDiffs: Store< let allDiffs: Store<

View file

@ -5,7 +5,7 @@
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider" import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
export let hash: string export let hash: string
let image: UIEventSource<ProvidedImage> = UIEventSource.FromPromise( let image: UIEventSource<ProvidedImage> = UIEventSource.fromPromise(
PanoramaxImageProvider.singleton.getInfo(hash) PanoramaxImageProvider.singleton.getInfo(hash)
) )
</script> </script>

View file

@ -15,7 +15,7 @@
export let id: OsmId export let id: OsmId
let usernames = new Set(onlyShowChangesBy) let usernames = new Set(onlyShowChangesBy)
let fullHistory = UIEventSource.FromPromise(new OsmObjectDownloader().downloadHistory(id)) let fullHistory = UIEventSource.fromPromise(new OsmObjectDownloader().downloadHistory(id))
let partOfLayer = fullHistory.mapD((history) => let partOfLayer = fullHistory.mapD((history) =>
history.map((step) => ({ history.map((step) => ({

View file

@ -15,7 +15,7 @@
console.log("Initial license:", image.license, image.provider) console.log("Initial license:", image.license, image.provider)
let license: Store<LicenseInfo> = image.license let license: Store<LicenseInfo> = image.license
? new ImmutableStore(image.license) ? new ImmutableStore(image.license)
: UIEventSource.FromPromise(image.provider?.DownloadAttribution(image)) : UIEventSource.fromPromise(image.provider?.DownloadAttribution(image))
let icon = image.provider?.sourceIcon() let icon = image.provider?.sourceIcon()
let openOriginal = image.provider?.visitUrl(image) let openOriginal = image.provider?.visitUrl(image)
</script> </script>

View file

@ -75,8 +75,8 @@
) )
let selected = new UIEventSource<P4CPicture>(undefined) let selected = new UIEventSource<P4CPicture>(undefined)
let selectedAsFeature = selected.mapD((s) => { let selectedAsFeature = selected.mapD((s) =>
return [ [
<Feature<Point>>{ <Feature<Point>>{
type: "Feature", type: "Feature",
geometry: { geometry: {
@ -89,14 +89,13 @@
rotation: s.direction, rotation: s.direction,
}, },
}, },
] ], onDestroy)
})
let someLoading = imageState.state.mapD((stateRecord) => let someLoading = imageState.state.mapD((stateRecord) =>
Object.values(stateRecord).some((v) => v === "loading") Object.values(stateRecord).some((v) => v === "loading"), onDestroy
) )
let errors = imageState.state.mapD((stateRecord) => let errors = imageState.state.mapD((stateRecord) =>
Object.keys(stateRecord).filter((k) => stateRecord[k] === "error") Object.keys(stateRecord).filter((k) => stateRecord[k] === "error"), onDestroy
) )
let highlighted = new UIEventSource<string>(undefined) let highlighted = new UIEventSource<string>(undefined)

View file

@ -20,6 +20,7 @@
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import Translations from "../../i18n/Translations" import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte" import Tr from "../../Base/Tr.svelte"
import { onDestroy } from "svelte"
export let value: UIEventSource<number> export let value: UIEventSource<number>
export let feature: Feature export let feature: Feature
@ -84,7 +85,7 @@
}, },
] ]
}, },
[mapLocation] [mapLocation], onDestroy
) )
new ShowDataLayer(map, { new ShowDataLayer(map, {

View file

@ -79,7 +79,7 @@
forceIndex = floors.data.indexOf(level) forceIndex = floors.data.indexOf(level)
}) })
Stores.Chronic(50).addCallback(() => stabilize()) Stores.chronic(50).addCallback(() => stabilize())
floors.addCallback((floors) => { floors.addCallback((floors) => {
forceIndex = floors.findIndex((s) => s === value.data) forceIndex = floors.findIndex((s) => s === value.data)
}) })

View file

@ -7,6 +7,7 @@
import { GeoOperations } from "../../../Logic/GeoOperations" import { GeoOperations } from "../../../Logic/GeoOperations"
import If from "../../Base/If.svelte" import If from "../../Base/If.svelte"
import type { SpecialVisualizationState } from "../../SpecialVisualization" import type { SpecialVisualizationState } from "../../SpecialVisualization"
import { onDestroy } from "svelte"
/** /**
* The value exported to outside, saved only when the button is pressed * The value exported to outside, saved only when the button is pressed
@ -61,7 +62,7 @@
} else { } else {
return -1 return -1
} }
}) }, onDestroy)
beta.map( beta.map(
(beta) => { (beta) => {
@ -77,7 +78,7 @@
previewDegrees.setData(beta + "°") previewDegrees.setData(beta + "°")
previewPercentage.setData(degreesToPercentage(beta)) previewPercentage.setData(degreesToPercentage(beta))
}, },
[valuesign, beta] [valuesign, beta], onDestroy
) )
function onSave() { function onSave() {

View file

@ -5,7 +5,7 @@
import Translations from "../../i18n/Translations" import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte" import Tr from "../../Base/Tr.svelte"
import { ImmutableStore, Store, Stores, UIEventSource } from "../../../Logic/UIEventSource" import { ImmutableStore, Store, UIEventSource } from "../../../Logic/UIEventSource"
import Wikidata, { WikidataResponse } from "../../../Logic/Web/Wikidata" import Wikidata, { WikidataResponse } from "../../../Logic/Web/Wikidata"
import Locale from "../../i18n/Locale" import Locale from "../../i18n/Locale"
import Loading from "../../Base/Loading.svelte" import Loading from "../../Base/Loading.svelte"
@ -64,7 +64,7 @@
}) })
WikidataValidator._searchCache.set(key, promise) WikidataValidator._searchCache.set(key, promise)
} }
return Stores.FromPromiseWithErr(promise) return UIEventSource.fromPromiseWithErr(promise)
} }
) )

View file

@ -173,7 +173,7 @@
) )
} }
const isValid = unvalidatedText.map((v) => validator?.isValid(v, getCountry) ?? true) const isValid = unvalidatedText.map((v) => validator?.isValid(v, getCountry) ?? true, onDestroy)
let htmlElem: HTMLInputElement | HTMLTextAreaElement let htmlElem: HTMLInputElement | HTMLTextAreaElement

View file

@ -65,7 +65,7 @@
let searchSuggestions = searchvalue.bindD((search) => { let searchSuggestions = searchvalue.bindD((search) => {
searchIsRunning.set(true) searchIsRunning.set(true)
try { try {
return UIEventSource.FromPromise(geocoder.search(search)) return UIEventSource.fromPromise(geocoder.search(search))
} finally { } finally {
searchIsRunning.set(false) searchIsRunning.set(false)
} }

View file

@ -4,6 +4,7 @@
import DynamicIcon from "./DynamicIcon.svelte" import DynamicIcon from "./DynamicIcon.svelte"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import { Orientation } from "../../Sensors/Orientation" import { Orientation } from "../../Sensors/Orientation"
import { onDestroy } from "svelte"
/** /**
* Renders a 'marker', which consists of multiple 'icons' * Renders a 'marker', which consists of multiple 'icons'
@ -20,7 +21,7 @@
? tags.map((tags) => rotation.GetRenderValue(tags).Subs(tags).txt) ? tags.map((tags) => rotation.GetRenderValue(tags).Subs(tags).txt)
: new ImmutableStore("0deg") : new ImmutableStore("0deg")
if (rotation?.render?.txt === "{alpha}deg") { if (rotation?.render?.txt === "{alpha}deg") {
_rotation = Orientation.singleton.alpha.map((alpha) => (alpha ? alpha + "deg" : "0deg ")) _rotation = Orientation.singleton.alpha.map((alpha) => (alpha ? alpha + "deg" : "0deg "), onDestroy)
} }
</script> </script>

View file

@ -179,7 +179,7 @@ class SingleBackgroundHandler {
} }
private fadeOut() { private fadeOut() {
Stores.Chronic( Stores.chronic(
8, 8,
() => this.opacity.data > 0 && this._deactivationTime !== undefined () => this.opacity.data > 0 && this._deactivationTime !== undefined
).addCallback(() => { ).addCallback(() => {
@ -193,7 +193,7 @@ class SingleBackgroundHandler {
} }
private fadeIn() { private fadeIn() {
Stores.Chronic( Stores.chronic(
8, 8,
() => this.opacity.data < 1.0 && this._deactivationTime === undefined () => this.opacity.data < 1.0 && this._deactivationTime === undefined
).addCallback(() => { ).addCallback(() => {

View file

@ -4,10 +4,11 @@
import MapControlButton from "../Base/MapControlButton.svelte" import MapControlButton from "../Base/MapControlButton.svelte"
import Plus from "../../assets/svg/Plus.svelte" import Plus from "../../assets/svg/Plus.svelte"
import type { MapProperties } from "../../Models/MapProperties" import type { MapProperties } from "../../Models/MapProperties"
import { onDestroy } from "svelte"
export let adaptor: MapProperties export let adaptor: MapProperties
let canZoomIn = adaptor.maxzoom.map((mz) => adaptor.zoom.data < mz, [adaptor.zoom]) let canZoomIn = adaptor.maxzoom.map((mz) => adaptor.zoom.data < mz, [adaptor.zoom], onDestroy)
let canZoomOut = adaptor.minzoom.map((mz) => adaptor.zoom.data > mz, [adaptor.zoom]) let canZoomOut = adaptor.minzoom.map((mz) => adaptor.zoom.data > mz, [adaptor.zoom], onDestroy)
</script> </script>
<div class="pointer-events-none absolute bottom-0 right-0 flex flex-col"> <div class="pointer-events-none absolute bottom-0 right-0 flex flex-col">

View file

@ -19,7 +19,7 @@
) )
onDestroy( onDestroy(
Stores.Chronic(250).addCallback(() => { Stores.chronic(250).addCallback(() => {
const mapIsLoading = !map.data?.isStyleLoaded() const mapIsLoading = !map.data?.isStyleLoaded()
isLoading = mapIsLoading && (didChange || rasterLayer === undefined) isLoading = mapIsLoading && (didChange || rasterLayer === undefined)
if (didChange && !mapIsLoading) { if (didChange && !mapIsLoading) {

View file

@ -43,7 +43,7 @@
return undefined return undefined
} }
}, },
[Stores.Chronic(5 * 60 * 1000)] [Stores.chronic(5 * 60 * 1000)]
) )
.mapD((date) => Utils.TwoDigits(date.getHours()) + ":" + Utils.TwoDigits(date.getMinutes())) .mapD((date) => Utils.TwoDigits(date.getHours()) + ":" + Utils.TwoDigits(date.getMinutes()))

View file

@ -5,6 +5,7 @@
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import { OH } from "./OpeningHours" import { OH } from "./OpeningHours"
import TimeInput from "../InputElement/Helpers/TimeInput.svelte" import TimeInput from "../InputElement/Helpers/TimeInput.svelte"
import { onDestroy } from "svelte"
export let value: UIEventSource<string> export let value: UIEventSource<string>
let startValue: UIEventSource<string> = new UIEventSource<string>(undefined) let startValue: UIEventSource<string> = new UIEventSource<string>(undefined)
@ -13,8 +14,9 @@
const t = Translations.t.general.opening_hours const t = Translations.t.general.opening_hours
let mode = new UIEventSource("") let mode = new UIEventSource("")
onDestroy(
value value
.map((ph) => OH.ParsePHRule(ph)) .map((ph) => OH.ParsePHRule(ph), onDestroy)
.addCallbackAndRunD((parsed) => { .addCallbackAndRunD((parsed) => {
if (parsed === null) { if (parsed === null) {
return return
@ -23,7 +25,7 @@
startValue.setData(parsed.start) startValue.setData(parsed.start)
endValue.setData(parsed.end) endValue.setData(parsed.end)
}) })
)
function updateValue() { function updateValue() {
if (mode.data === undefined || mode.data === "") { if (mode.data === undefined || mode.data === "") {
// not known // not known

View file

@ -13,7 +13,7 @@
import WikidatapreviewWithLoading from "../Wikipedia/WikidatapreviewWithLoading.svelte" import WikidatapreviewWithLoading from "../Wikipedia/WikidatapreviewWithLoading.svelte"
export let species: PlantNetSpeciesMatch export let species: PlantNetSpeciesMatch
let wikidata = UIEventSource.FromPromise( let wikidata = UIEventSource.fromPromise(
Wikidata.Sparql<{ species }>( Wikidata.Sparql<{ species }>(
["?species", "?speciesLabel"], ["?species", "?speciesLabel"],
['?species wdt:P846 "' + species.gbif.id + '"'] ['?species wdt:P846 "' + species.gbif.id + '"']
@ -27,7 +27,7 @@
* PlantNet give us a GBIF-id, but we want the Wikidata-id instead. * PlantNet give us a GBIF-id, but we want the Wikidata-id instead.
* We look this up in wikidata * We look this up in wikidata
*/ */
const wikidataId: Store<string> = UIEventSource.FromPromise( const wikidataId: Store<string> = UIEventSource.fromPromise(
Wikidata.Sparql<{ species }>( Wikidata.Sparql<{ species }>(
["?species", "?speciesLabel"], ["?species", "?speciesLabel"],
['?species wdt:P846 "' + species.gbif.id + '"'] ['?species wdt:P846 "' + species.gbif.id + '"']

View file

@ -25,6 +25,7 @@
import type { GlobalFilter } from "../../../Models/GlobalFilter" import type { GlobalFilter } from "../../../Models/GlobalFilter"
import { EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid" import { EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
import Layers from "../../../assets/svg/Layers.svelte" import Layers from "../../../assets/svg/Layers.svelte"
import { onDestroy } from "svelte"
export let state: ThemeViewState export let state: ThemeViewState
export let layer: LayerConfig export let layer: LayerConfig
@ -44,7 +45,7 @@
let asTags = tags.map((tgs) => let asTags = tags.map((tgs) =>
Object.keys(tgs) Object.keys(tgs)
.filter((k) => !k.startsWith("_") && !forbiddenKeys.has(k)) .filter((k) => !k.startsWith("_") && !forbiddenKeys.has(k))
.map((k) => new Tag(k, tgs[k])) .map((k) => new Tag(k, tgs[k])), onDestroy
) )
let showPopup: UIEventSource<boolean> = new UIEventSource(false) let showPopup: UIEventSource<boolean> = new UIEventSource(false)

View file

@ -7,9 +7,10 @@
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import TagLink from "./TagLink.svelte" import TagLink from "./TagLink.svelte"
import Tag from "@rgossiaux/svelte-heroicons/solid/Tag" import Tag from "@rgossiaux/svelte-heroicons/solid/Tag"
import { onDestroy } from "svelte"
export let tags: UIEventSource<Record<string, any>> export let tags: UIEventSource<Record<string, any>>
export let tagKeys = tags.map((tgs) => (tgs === undefined ? [] : Object.keys(tgs))) export let tagKeys = tags.map((tgs) => (tgs === undefined ? [] : Object.keys(tgs)), onDestroy)
export let layer: LayerConfig | undefined = undefined export let layer: LayerConfig | undefined = undefined

View file

@ -103,7 +103,7 @@ export default class AutoApplyButtonVis extends SpecialVisualizationSvelte {
} }
const to_parse: UIEventSource<string[]> = new UIEventSource<string[]>(undefined) const to_parse: UIEventSource<string[]> = new UIEventSource<string[]>(undefined)
Stores.Chronic(500, () => to_parse.data === undefined) Stores.chronic(500, () => to_parse.data === undefined)
.map(() => { .map(() => {
const applicable = <string | string[]>tagSource.data[argument[1]] const applicable = <string | string[]>tagSource.data[argument[1]]
if (typeof applicable === "string") { if (typeof applicable === "string") {
@ -116,7 +116,7 @@ export default class AutoApplyButtonVis extends SpecialVisualizationSvelte {
to_parse.set(data) to_parse.set(data)
}) })
const stableIds: Store<string[]> = Stores.ListStabilized(to_parse).map((ids) => { const stableIds: Store<string[]> = Stores.listStabilized(to_parse).map((ids) => {
if (typeof ids === "string") { if (typeof ids === "string") {
ids = JSON.parse(ids) ids = JSON.parse(ids)
} }

View file

@ -95,7 +95,7 @@ export class DeleteFlowState {
if (allByMyself.data === null && useTheInternet) { 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 // We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above
UIEventSource.FromPromise(this.objectDownloader.downloadHistory(id)) UIEventSource.fromPromise(this.objectDownloader.downloadHistory(id))
.mapD((versions) => .mapD((versions) =>
versions.map((version) => versions.map((version) =>
Number(version.tags["_last_edit:contributor:uid"]) Number(version.tags["_last_edit:contributor:uid"])

View file

@ -4,19 +4,18 @@
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization" import type { SpecialVisualizationState } from "../SpecialVisualization"
import { onDestroy } from "svelte"
export let key: string export let key: string
export let tags: Store<Record<string, string>> export let tags: Store<Record<string, string>>
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
const validator = new FediverseValidator() const validator = new FediverseValidator()
const userinfo = tags let userinfo = tags
.mapD((t) => t[key]) .mapD((t) => t[key], onDestroy)
.mapD((fediAccount) => { .mapD((fediAccount) => FediverseValidator.extractServer(validator.reformat(fediAccount)), onDestroy)
return FediverseValidator.extractServer(validator.reformat(fediAccount)) let homeLocation: Store<string> = state.userRelatedState?.preferencesAsTags
}) .mapD((prefs) => prefs["_mastodon_link"], onDestroy)
const homeLocation: Store<string> = state.userRelatedState?.preferencesAsTags .mapD((userhandle) => FediverseValidator.extractServer(validator.reformat(userhandle))?.server, onDestroy)
.mapD((prefs) => prefs["_mastodon_link"])
.mapD((userhandle) => FediverseValidator.extractServer(validator.reformat(userhandle))?.server)
</script> </script>
<div class="flex w-full flex-col"> <div class="flex w-full flex-col">

View file

@ -8,6 +8,7 @@
import type { Feature } from "geojson" import type { Feature } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import EditButton from "../TagRendering/EditButton.svelte" import EditButton from "../TagRendering/EditButton.svelte"
import { onDestroy } from "svelte"
export let key: string export let key: string
export let tags: UIEventSource<Record<string, string>> export let tags: UIEventSource<Record<string, string>>
@ -34,7 +35,7 @@
} }
} }
return foundLanguages return foundLanguages
}) }, onDestroy)
const forceInputMode = new UIEventSource(false) const forceInputMode = new UIEventSource(false)
</script> </script>

View file

@ -8,6 +8,7 @@
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { ariaLabel } from "../../Utils/ariaLabel" import { ariaLabel } from "../../Utils/ariaLabel"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import { onDestroy } from "svelte"
/** /**
* A small 'mark as favourite'-button to serve as title-icon * A small 'mark as favourite'-button to serve as title-icon
@ -16,7 +17,7 @@
export let feature: Feature export let feature: Feature
export let tags: UIEventSource<Record<string, string>> export let tags: UIEventSource<Record<string, string>>
export let layer: LayerConfig export let layer: LayerConfig
let isFavourite = tags?.map((tags) => tags._favourite === "yes") let isFavourite = tags?.map((tags) => tags._favourite === "yes", onDestroy)
const t = Translations.t.favouritePoi const t = Translations.t.favouritePoi
function markFavourite(isFavourite: boolean) { function markFavourite(isFavourite: boolean) {

View file

@ -8,6 +8,7 @@
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import MaplibreMap from "../Map/MaplibreMap.svelte" import MaplibreMap from "../Map/MaplibreMap.svelte"
import DelayedComponent from "../Base/DelayedComponent.svelte" import DelayedComponent from "../Base/DelayedComponent.svelte"
import { onDestroy } from "svelte"
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
export let tagSource: UIEventSource<Record<string, string>> export let tagSource: UIEventSource<Record<string, string>>
@ -48,7 +49,7 @@
} }
return features return features
}, },
[tagSource] [tagSource], onDestroy
) )
let mlmap = new UIEventSource(undefined) let mlmap = new UIEventSource(undefined)

View file

@ -26,6 +26,7 @@
import Loading from "../Base/Loading.svelte" import Loading from "../Base/Loading.svelte"
import { Map as MlMap } from "maplibre-gl" import { Map as MlMap } from "maplibre-gl"
import type { GeocodeResult } from "../../Logic/Search/GeocodingProvider" import type { GeocodeResult } from "../../Logic/Search/GeocodingProvider"
import { onDestroy } from "svelte"
export let state: ThemeViewState export let state: ThemeViewState
@ -62,12 +63,15 @@
} }
let notAllowed = moveWizardState.moveDisallowedReason let notAllowed = moveWizardState.moveDisallowedReason
let currentMapProperties: Store<Partial<MapProperties> & { location }> = reason.mapD((r) => let currentMapProperties: Store<Partial<MapProperties> & { location }> = reason.mapD((r) =>
initMapProperties(r) initMapProperties(r), onDestroy
) )
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined) let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let searchValue = new UIEventSource<string>("") let searchValue = new UIEventSource<string>("")
let isSearching = new UIEventSource<boolean>(false) let isSearching = new UIEventSource<boolean>(false)
let zoomedInEnough = currentMapProperties.bindD(properties => properties.zoom, onDestroy).mapD(
(zoom) => zoom >= Constants.minZoomLevelToAddNewPoint, onDestroy
)
const searcher = new NominatimGeocoding(1) const searcher = new NominatimGeocoding(1)
async function searchPressed() { async function searchPressed() {
@ -160,9 +164,7 @@
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<If <If
condition={$currentMapProperties.zoom.mapD( condition={zoomedInEnough}
(zoom) => zoom >= Constants.minZoomLevelToAddNewPoint
)}
> >
<button <button
class="primary w-full" class="primary w-full"

View file

@ -11,6 +11,7 @@
import LoginToggle from "../../Base/LoginToggle.svelte" import LoginToggle from "../../Base/LoginToggle.svelte"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import Loading from "../../Base/Loading.svelte" import Loading from "../../Base/Loading.svelte"
import { onDestroy } from "svelte"
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>> export let tags: UIEventSource<Record<string, string>>
@ -29,7 +30,7 @@
} }
const t = Translations.t.notes const t = Translations.t.notes
let isClosed: Store<boolean> = tags.map((tags) => (tags?.["closed_at"] ?? "") !== "") let isClosed: Store<boolean> = tags.map((tags) => (tags?.["closed_at"] ?? "") !== "", onDestroy)
let isProcessing = writable(false) let isProcessing = writable(false)
async function addComment() { async function addComment() {

View file

@ -7,6 +7,7 @@
import Icon from "../../Map/Icon.svelte" import Icon from "../../Map/Icon.svelte"
import NoteCommentElement from "./NoteCommentElement" import NoteCommentElement from "./NoteCommentElement"
import { Translation } from "../../i18n/Translation" import { Translation } from "../../i18n/Translation"
import { onDestroy } from "svelte"
const t = Translations.t.notes const t = Translations.t.notes
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
@ -19,7 +20,7 @@
export let zoomMoreMessage: string export let zoomMoreMessage: string
let curZoom = state.mapProperties.zoom let curZoom = state.mapProperties.zoom
const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "") const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "", onDestroy)
async function closeNote() { async function closeNote() {
const id = tags.data[idkey] const id = tags.data[idkey]

View file

@ -4,7 +4,7 @@
import Note from "../../../assets/svg/Note.svelte" import Note from "../../../assets/svg/Note.svelte"
import Resolved from "../../../assets/svg/Resolved.svelte" import Resolved from "../../../assets/svg/Resolved.svelte"
import Speech_bubble from "../../../assets/svg/Speech_bubble.svelte" import Speech_bubble from "../../../assets/svg/Speech_bubble.svelte"
import { Stores } from "../../../Logic/UIEventSource" import { UIEventSource } from "../../../Logic/UIEventSource"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import Tr from "../../Base/Tr.svelte" import Tr from "../../Base/Tr.svelte"
import AllImageProviders from "../../../Logic/ImageProviders/AllImageProviders" import AllImageProviders from "../../../Logic/ImageProviders/AllImageProviders"
@ -25,12 +25,10 @@
const t = Translations.t.notes const t = Translations.t.notes
// Info about the user who made the comment // Info about the user who made the comment
let userinfo = Stores.FromPromise( let userinfo = UIEventSource.fromPromise(Utils.downloadJsonCached<{ user: { img: { href: string } } }>(
Utils.downloadJsonCached<{ user: { img: { href: string } } }>( "https://api.openstreetmap.org/api/0.6/user/" + comment.uid,
"https://api.openstreetmap.org/api/0.6/user/" + comment.uid, 24 * 60 * 60 * 1000
24 * 60 * 60 * 1000 ))
)
)
const htmlElement = document.createElement("div") const htmlElement = document.createElement("div")
htmlElement.innerHTML = Utils.purify(comment.html) htmlElement.innerHTML = Utils.purify(comment.html)

View file

@ -79,7 +79,8 @@
} }
return questionsToAsk return questionsToAsk
}, },
[skippedQuestions] [skippedQuestions],
onDestroy
) )
let firstQuestion: UIEventSource<TagRenderingConfig> = new UIEventSource<TagRenderingConfig>( let firstQuestion: UIEventSource<TagRenderingConfig> = new UIEventSource<TagRenderingConfig>(
undefined undefined
@ -87,6 +88,8 @@
let allQuestionsToAsk: UIEventSource<TagRenderingConfig[]> = new UIEventSource< let allQuestionsToAsk: UIEventSource<TagRenderingConfig[]> = new UIEventSource<
TagRenderingConfig[] TagRenderingConfig[]
>([]) >([])
onDestroy(() => firstQuestion.destroy())
onDestroy(() => allQuestionsToAsk.destroy())
async function calculateQuestions() { async function calculateQuestions() {
const qta = questionsToAsk.data const qta = questionsToAsk.data

View file

@ -8,6 +8,7 @@
import { Store, UIEventSource } from "../../../Logic/UIEventSource" import { Store, UIEventSource } from "../../../Logic/UIEventSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
import { onDestroy } from "svelte"
export let tags: UIEventSource<Record<string, string> | undefined> export let tags: UIEventSource<Record<string, string> | undefined>
@ -26,7 +27,7 @@
throw "Config is undefined in tagRenderingAnswer" throw "Config is undefined in tagRenderingAnswer"
} }
let trs: Store<{ then: Translation; icon?: string; iconClass?: string }[]> = tags.mapD((tags) => let trs: Store<{ then: Translation; icon?: string; iconClass?: string }[]> = tags.mapD((tags) =>
Utils.NoNull(config?.GetRenderValues(tags)) Utils.NoNull(config?.GetRenderValues(tags)),onDestroy
) )
</script> </script>

View file

@ -51,7 +51,7 @@
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined) let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key)) let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
let isKnown = tags.mapD((tags) => config.GetRenderValue(tags) !== undefined) let isKnown = tags.mapD((tags) => config.GetRenderValue(tags) !== undefined, onDestroy)
let matchesEmpty = config.GetRenderValue({}) !== undefined let matchesEmpty = config.GetRenderValue({}) !== undefined
// Will be bound if a freeform is available // Will be bound if a freeform is available
@ -68,7 +68,7 @@
* The tags to apply to mark this answer as "unknown" * The tags to apply to mark this answer as "unknown"
*/ */
let onMarkUnknown: Store<UploadableTag[] | undefined> = tags.mapD((tags) => let onMarkUnknown: Store<UploadableTag[] | undefined> = tags.mapD((tags) =>
config.markUnknown(layer, tags) config.markUnknown(layer, tags), onDestroy
) )
let unknownModal = new UIEventSource(false) let unknownModal = new UIEventSource(false)

View file

@ -16,6 +16,7 @@
import { ExclamationTriangle } from "@babeard/svelte-heroicons/solid/ExclamationTriangle" import { ExclamationTriangle } from "@babeard/svelte-heroicons/solid/ExclamationTriangle"
import ReviewPrivacyShield from "./ReviewPrivacyShield.svelte" import ReviewPrivacyShield from "./ReviewPrivacyShield.svelte"
import ThemeViewState from "../../Models/ThemeViewState" import ThemeViewState from "../../Models/ThemeViewState"
import { onDestroy } from "svelte"
export let state: ThemeViewState export let state: ThemeViewState
export let tags: UIEventSource<Record<string, string>> export let tags: UIEventSource<Record<string, string>>
@ -46,7 +47,7 @@
return "too_long" return "too_long"
} }
return undefined return undefined
}) }, onDestroy)
let uploadFailed: string = undefined let uploadFailed: string = undefined
let isTesting = state?.featureSwitchIsTesting let isTesting = state?.featureSwitchIsTesting

View file

@ -4,10 +4,11 @@
import Loading from "../Base/Loading.svelte" import Loading from "../Base/Loading.svelte"
import FilterToggle from "./FilterToggle.svelte" import FilterToggle from "./FilterToggle.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization" import type { SpecialVisualizationState } from "../SpecialVisualization"
import { onDestroy } from "svelte"
export let activeFilter: ActiveFilter[] export let activeFilter: ActiveFilter[]
let { control, filter } = activeFilter[0] let { control, filter } = activeFilter[0]
let option = control.map((c) => filter.options[c] ?? filter.options[0]) let option = control.map((c) => filter.options[c] ?? filter.options[0], onDestroy)
let loading = false let loading = false
function clear() { function clear() {

View file

@ -12,6 +12,7 @@
import Locale from "../i18n/Locale" import Locale from "../i18n/Locale"
import { Store } from "../../Logic/UIEventSource" import { Store } from "../../Logic/UIEventSource"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte" import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import { onDestroy } from "svelte"
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
let searchTerm = state.searchState.searchTerm let searchTerm = state.searchState.searchTerm
@ -20,7 +21,8 @@
let filtersMerged = filterResults.map( let filtersMerged = filterResults.map(
(filters) => FilterSearch.mergeSemiIdenticalLayers(filters, Locale.language.data), (filters) => FilterSearch.mergeSemiIdenticalLayers(filters, Locale.language.data),
[Locale.language] [Locale.language],
onDestroy
) )
let layerResults = state.searchState.layerSuggestions.map( let layerResults = state.searchState.layerSuggestions.map(
@ -32,7 +34,7 @@
} }
return layers return layers
}, },
[activeLayers] [activeLayers], onDestroy
) )
let filterResultsClipped: Store<{ let filterResultsClipped: Store<{
clipped: (FilterSearchResult[] | LayerConfig)[] clipped: (FilterSearchResult[] | LayerConfig)[]
@ -46,7 +48,7 @@
} }
return { clipped: ls.slice(0, 4), rest: ls.slice(4) } return { clipped: ls.slice(0, 4), rest: ls.slice(4) }
}, },
[layerResults, activeLayers, Locale.language] [layerResults, activeLayers, Locale.language], onDestroy
) )
</script> </script>

View file

@ -13,7 +13,8 @@
import FeaturePropertiesStore from "../../Logic/FeatureSource/Actors/FeaturePropertiesStore" import FeaturePropertiesStore from "../../Logic/FeatureSource/Actors/FeaturePropertiesStore"
import SearchState from "../../Logic/State/SearchState" import SearchState from "../../Logic/State/SearchState"
import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp" import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp"
import { createEventDispatcher } from "svelte" import { createEventDispatcher, onDestroy } from "svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
export let entry: GeocodeResult export let entry: GeocodeResult
export let state: { export let state: {
@ -21,7 +22,7 @@
theme?: ThemeConfig theme?: ThemeConfig
featureProperties?: FeaturePropertiesStore featureProperties?: FeaturePropertiesStore
searchState: Partial<SearchState> searchState: Partial<SearchState>
} } & SpecialVisualizationState
let layer: LayerConfig let layer: LayerConfig
let tags: UIEventSource<Record<string, string>> let tags: UIEventSource<Record<string, string>>
@ -33,13 +34,13 @@
} }
let distance = state.mapProperties.location.mapD((l) => let distance = state.mapProperties.location.mapD((l) =>
GeoOperations.distanceBetween([l.lon, l.lat], [entry.lon, entry.lat]) GeoOperations.distanceBetween([l.lon, l.lat], [entry.lon, entry.lat]), onDestroy
) )
let bearing = state.mapProperties.location.mapD((l) => let bearing = state.mapProperties.location.mapD((l) =>
GeoOperations.bearing([l.lon, l.lat], [entry.lon, entry.lat]) GeoOperations.bearing([l.lon, l.lat], [entry.lon, entry.lat]), onDestroy)
)
let mapRotation = state.mapProperties.rotation let mapRotation = state.mapProperties.rotation
let inView = state.mapProperties.bounds.mapD((bounds) => bounds.contains([entry.lon, entry.lat])) let inView = state.mapProperties.bounds.mapD((bounds) => bounds.contains([entry.lon, entry.lat]), onDestroy)
let dispatch = createEventDispatcher<{ select: GeocodeResult }>() let dispatch = createEventDispatcher<{ select: GeocodeResult }>()
function select() { function select() {

View file

@ -12,25 +12,22 @@
import { CogIcon } from "@rgossiaux/svelte-heroicons/solid" import { CogIcon } from "@rgossiaux/svelte-heroicons/solid"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import { MinimalThemeInformation } from "../../Models/ThemeConfig/ThemeConfig" import { MinimalThemeInformation } from "../../Models/ThemeConfig/ThemeConfig"
import ThemeViewState from "../../Models/ThemeViewState"
import { WithSearchState } from "../../Models/ThemeViewState/WithSearchState"
import { Store } from "../../Logic/UIEventSource"
import { Utils } from "../../Utils"
export let state: SpecialVisualizationState export let state: WithSearchState
let searchTerm = state.searchState.searchTerm let searchTerm = state.searchState.searchTerm
let recentThemes = state.userRelatedState.recentlyVisitedThemes.value.map((themes) => { let visitedThemes = state.userRelatedState.recentlyVisitedThemes.value
const recent = themes.filter((th) => th !== state.theme.id).slice(0, 6) let recentThemes: Store<string[]> = visitedThemes.map((themes) =>
const deduped: MinimalThemeInformation[] = [] Utils.Dedup(themes.filter((th) => th !== state.theme.id)).slice(0, 6))
for (const theme of recent) {
if (deduped.some((th) => th.id === theme.id)) {
continue
}
deduped.push(theme)
}
return deduped
})
let themeResults = state.searchState.themeSuggestions let themeResults = state.searchState.themeSuggestions
const t = Translations.t.general.search const t = Translations.t.general.search
</script> </script>
{JSON.stringify($visitedThemes)}
{#if $themeResults.length > 0} {#if $themeResults.length > 0}
<SidebarUnit> <SidebarUnit>
<h3> <h3>

View file

@ -49,7 +49,7 @@
AndroidPolyfill.init() AndroidPolyfill.init()
let webgl_supported = webgl_support() let webgl_supported = webgl_support()
let availableLayers = UIEventSource.FromPromise(getAvailableLayers()) let availableLayers = UIEventSource.fromPromise(getAvailableLayers())
const state = new WithSearchState(theme, availableLayers) const state = new WithSearchState(theme, availableLayers)
</script> </script>

View file

@ -6,7 +6,7 @@ import WayImportButtonViz from "../Popup/ImportButtons/WayImportButtonViz"
import ConflateImportButtonViz from "../Popup/ImportButtons/ConflateImportButtonViz" import ConflateImportButtonViz from "../Popup/ImportButtons/ConflateImportButtonViz"
import { PlantNetDetectionViz } from "../Popup/PlantNetDetectionViz" import { PlantNetDetectionViz } from "../Popup/PlantNetDetectionViz"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource" import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Feature, GeoJsonProperties } from "geojson" import { Feature, GeoJsonProperties } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
@ -170,50 +170,46 @@ class LinkedDataFromWebsite extends SpecialVisualization {
} }
const country = countryStore.data const country = countryStore.data
if (url.startsWith("https://data.velopark.be/")) { if (url.startsWith("https://data.velopark.be/")) {
return Stores.FromPromiseWithErr( return UIEventSource.fromPromiseWithErr((async () => {
(async () => { try {
try { const loadAll = layer.id.toLowerCase().indexOf("maproulette") >= 0 // Dirty hack
const loadAll = layer.id.toLowerCase().indexOf("maproulette") >= 0 // Dirty hack const features = await LinkedDataLoader.fetchVeloparkEntry(
const features = await LinkedDataLoader.fetchVeloparkEntry( url,
url, loadAll
loadAll )
) const feature =
const feature = features.find((f) => f.properties["ref:velopark"] === url) ??
features.find((f) => f.properties["ref:velopark"] === url) ?? features[0]
features[0] const properties = feature.properties
const properties = feature.properties properties["ref:velopark"] = url
properties["ref:velopark"] = url console.log("Got properties from velopark:", properties)
console.log("Got properties from velopark:", properties) return properties
return properties } catch (e) {
} catch (e) { console.error(e)
console.error(e) throw e
throw e }
} })())
})()
)
} }
if (country === undefined) { if (country === undefined) {
return undefined return undefined
} }
return Stores.FromPromiseWithErr( return UIEventSource.fromPromiseWithErr((async () => {
(async () => { try {
try { return await LinkedDataLoader.fetchJsonLd(
return await LinkedDataLoader.fetchJsonLd( url,
url, { country },
{ country }, useProxy ? "proxy" : "fetch-lod"
useProxy ? "proxy" : "fetch-lod" )
) } catch (e) {
} catch (e) { console.log(
console.log( "Could not get with proxy/download LOD, attempting to download directly. Error for ",
"Could not get with proxy/download LOD, attempting to download directly. Error for ", url,
url, "is",
"is", e
e )
) return await LinkedDataLoader.fetchJsonLd(url, { country }, "fetch-raw")
return await LinkedDataLoader.fetchJsonLd(url, { country }, "fetch-raw") }
} })())
})()
)
}, },
[countryStore, downloadInformation] [countryStore, downloadInformation]
) )
@ -271,7 +267,7 @@ class CompareData extends SpecialVisualization {
): BaseUIElement { ): BaseUIElement {
const url = args[0] const url = args[0]
const readonly = args[3] === "yes" const readonly = args[3] === "yes"
const externalData = Stores.FromPromiseWithErr(Utils.downloadJson(url)) const externalData = UIEventSource.fromPromiseWithErr(Utils.downloadJson(url))
return new SvelteUIElement(ComparisonTool, { return new SvelteUIElement(ComparisonTool, {
url, url,
state, state,

View file

@ -41,7 +41,7 @@
} }
let allData = <UIEventSource<(ChangeSetData & OsmFeature)[]>>( let allData = <UIEventSource<(ChangeSetData & OsmFeature)[]>>(
UIEventSource.FromPromise(downloadData()) UIEventSource.fromPromise(downloadData())
) )
let overview: Store<ChangesetsOverview | undefined> = allData.mapD( let overview: Store<ChangesetsOverview | undefined> = allData.mapD(

View file

@ -8,7 +8,7 @@
let homeUrl = "https://data.mapcomplete.org/changeset-metadata/" let homeUrl = "https://data.mapcomplete.org/changeset-metadata/"
let stats_files = "file-overview.json" let stats_files = "file-overview.json"
let indexFile = UIEventSource.FromPromise(Utils.downloadJson<string[]>(homeUrl + stats_files)) let indexFile = UIEventSource.fromPromise(Utils.downloadJson<string[]>(homeUrl + stats_files))
let prefix = /^stats.202[45]/ let prefix = /^stats.202[45]/
let filteredIndex = indexFile.mapD((index) => index.filter((path) => path.match(prefix))) let filteredIndex = indexFile.mapD((index) => index.filter((path) => path.match(prefix)))
filteredIndex.addCallbackAndRunD((filtered) => filteredIndex.addCallbackAndRunD((filtered) =>

View file

@ -13,7 +13,7 @@
let services: MCService[] = [] let services: MCService[] = []
let recheckSignal: UIEventSource<any> = new UIEventSource<any>(undefined) let recheckSignal: UIEventSource<any> = new UIEventSource<any>(undefined)
let checkSignal = Stores.Chronic(10000) let checkSignal = Stores.chronic(10000)
let autoCheckAgain = new UIEventSource<boolean>(false) let autoCheckAgain = new UIEventSource<boolean>(false)
function testDownload(url: string, raw: boolean = false): Store<{ success } | { error }> { function testDownload(url: string, raw: boolean = false): Store<{ success } | { error }> {

View file

@ -11,7 +11,7 @@
export let osmConnection: OsmConnection export let osmConnection: OsmConnection
const dispatch = createEventDispatcher<{ layerSelected: string }>() const dispatch = createEventDispatcher<{ layerSelected: string }>()
let displayName = UIEventSource.FromPromise( let displayName = UIEventSource.fromPromise(
osmConnection.getInformationAboutUser(info.owner) osmConnection.getInformationAboutUser(info.owner)
).mapD((response) => response.display_name) ).mapD((response) => response.display_name)
let selfId = osmConnection.userDetails.mapD((ud) => ud.uid) let selfId = osmConnection.userDetails.mapD((ud) => ud.uid)

View file

@ -23,7 +23,7 @@ export default class StudioServer {
constructor(url: string, userId: Store<number | undefined>) { constructor(url: string, userId: Store<number | undefined>) {
this.url = url this.url = url
this._userId = userId this._userId = userId
this.overview = UIEventSource.FromPromiseWithErr(this.fetchOverviewRaw()) this.overview = UIEventSource.fromPromiseWithErr(this.fetchOverviewRaw())
} }
public fetchOverview(): Store< public fetchOverview(): Store<

View file

@ -27,7 +27,7 @@
if (!k) { if (!k) {
return undefined return undefined
} }
return UIEventSource.FromPromise(TagInfo.global.getStats(k, v)) return UIEventSource.fromPromise(TagInfo.global.getStats(k, v))
} catch (e) { } catch (e) {
return undefined return undefined
} }

View file

@ -8,13 +8,14 @@
import Wikipedia from "../../assets/svg/Wikipedia.svelte" import Wikipedia from "../../assets/svg/Wikipedia.svelte"
import Wikidatapreview from "./Wikidatapreview.svelte" import Wikidatapreview from "./Wikidatapreview.svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte" import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import { onDestroy } from "svelte"
/** /**
* Shows a wikipedia-article + wikidata preview for the given item * Shows a wikipedia-article + wikidata preview for the given item
*/ */
export let wikipediaDetails: Store<FullWikipediaDetails> export let wikipediaDetails: Store<FullWikipediaDetails>
let titleOnly = wikipediaDetails.mapD( let titleOnly = wikipediaDetails.mapD(
(details) => Object.keys(details).length === 1 && details.title !== undefined (details) => Object.keys(details).length === 1 && details.title !== undefined, [], onDestroy
) )
</script> </script>

View file

@ -55,6 +55,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
>() >()
public static readonly empty: ReadonlyArray<any>
public static readonly isIframe = !Utils.runningFromConsole && window !== window.top public static readonly isIframe = !Utils.runningFromConsole && window !== window.top
public static initDomPurify() { public static initDomPurify() {
@ -1902,4 +1904,10 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
return <T>copy return <T>copy
} }
public static pass(){
// Does nothing
}
} }