diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json index 11ff08031..9b104a3d9 100644 --- a/assets/layers/usersettings/usersettings.json +++ b/assets/layers/usersettings/usersettings.json @@ -838,20 +838,20 @@ }, "mappings": [ { - "if": "mapcomplete-theme-history=sync", - "alsoShowIf": "mapcomplete-theme-history=", + "if": "mapcomplete-preference-theme-history=sync", + "alsoShowIf": "mapcomplete-preference-theme-history=", "then": { "en": "Save the visited thematic maps and sync them via openstreetmap.org. OpenStreetMap and all apps you use can see this history" } }, { - "if": "mapcomplete-theme-history=local", + "if": "mapcomplete-preference-theme-history=local", "then": { "en": "Save the visited thematic maps on my device" } }, { - "if": "mapcomplete-theme-history=no", + "if": "mapcomplete-preference-theme-history=no", "then": { "en": "Don't save visited thematic maps" } @@ -868,20 +868,20 @@ }, "mappings": [ { - "if": "mapcomplete-search-history=sync", - "alsoShowIf": "mapcomplete-search-history=", + "if": "mapcomplete-preference-search-history=sync", + "alsoShowIf": "mapcomplete-preference-search-history=", "then": { "en": "Save the locations you search for and inspect and sync them via openstreetmap.org. OpenStreetMap and all apps you use can see this history" } }, { - "if": "mapcomplete-search-history=local", + "if": "mapcomplete-preference-search-history=local", "then": { "en": "Save the locations you search for and inspect on my device" } }, { - "if": "mapcomplete-search-history=no", + "if": "mapcomplete-preference-search-history=no", "then": { "en": "Don't save the locations you search for and inspect " } diff --git a/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts b/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts index 56542e55d..595c1def3 100644 --- a/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts @@ -24,7 +24,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource { constructor(state: SpecialVisualizationState) { const features: Store = Stores.ListStabilized( - state.osmConnection.preferencesHandler.preferences.map((prefs) => { + state.osmConnection.preferencesHandler.allPreferences.map((prefs) => { const feats: Feature[] = [] const allIds = new Set() for (const key in prefs) { diff --git a/src/Logic/Geocoding/RecentSearch.ts b/src/Logic/Geocoding/RecentSearch.ts deleted file mode 100644 index 3a2a13041..000000000 --- a/src/Logic/Geocoding/RecentSearch.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Store, UIEventSource } from "../UIEventSource" -import { Feature } from "geojson" -import { OsmConnection } from "../Osm/OsmConnection" -import { GeocodeResult } from "./GeocodingProvider" -import { GeoOperations } from "../GeoOperations" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" - -export class RecentSearch { - - - public readonly seenThisSession: UIEventSource - - constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store }) { - const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches") - this.seenThisSession = new UIEventSource([])//UIEventSource.asObject(prefs, []) - - prefs.addCallbackAndRunD(pref => { - if (pref === "") { - return - } - try { - - const simpleArr = JSON.parse(pref) - if (simpleArr.length > 0) { - this.seenThisSession.set(simpleArr) - return true - } - } catch (e) { - console.error(e, pref) - prefs.setData("") - } - }) - - this.seenThisSession.stabilized(2500).addCallbackAndRunD(seen => { - const results = [] - for (let i = 0; i < Math.min(3, seen.length); i++) { - const gc = seen[i] - const simple = { - category: gc.category, - description: gc.description, - display_name: gc.display_name, - lat: gc.lat, lon: gc.lon, - osm_id: gc.osm_id, - osm_type: gc.osm_type, - } - results.push(simple) - } - prefs.setData(JSON.stringify(results)) - - }) - - state.selectedElement.addCallbackAndRunD(selected => { - - const [osm_type, osm_id] = selected.properties.id.split("/") - if (!osm_id) { - return - } - if (["node", "way", "relation"].indexOf(osm_type) < 0) { - return - } - const [lon, lat] = GeoOperations.centerpointCoordinates(selected) - const entry = { - feature: selected, - display_name: selected.properties.name ?? selected.properties.alt_name ?? selected.properties.local_name, - osm_id, osm_type, - lon, lat, - } - this.addSelected(entry) - - }) - } - - addSelected(entry: GeocodeResult) { - const id = entry.osm_type + entry.osm_id - const arr = [...(this.seenThisSession.data.reverse() ?? []).slice(0, 5)] - .filter(e => e.osm_type + e.osm_id !== id) - - this.seenThisSession.set([entry, ...arr]) - } -} diff --git a/src/Logic/Geocoding/ThemeSearch.ts b/src/Logic/Geocoding/ThemeSearch.ts index 6428a3e61..96b0577cd 100644 --- a/src/Logic/Geocoding/ThemeSearch.ts +++ b/src/Logic/Geocoding/ThemeSearch.ts @@ -1,13 +1,12 @@ import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider" -import * as themeOverview from "../../assets/generated/theme_overview.json" import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" import { SpecialVisualizationState } from "../../UI/SpecialVisualization" import MoreScreen from "../../UI/BigComponents/MoreScreen" import { ImmutableStore, Store } from "../UIEventSource" +import UserRelatedState from "../State/UserRelatedState" export default class ThemeSearch implements GeocodingProvider { - private static allThemes: MinimalLayoutInformation[] = (themeOverview["default"] ?? themeOverview) private readonly _state: SpecialVisualizationState private readonly _knownHiddenThemes: Store> private readonly _suggestionLimit: number @@ -18,7 +17,7 @@ export default class ThemeSearch implements GeocodingProvider { this._state = state this._layersToIgnore = state.layout.layers.map(l => l.id) this._suggestionLimit = suggestionLimit - this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection) + this._knownHiddenThemes = UserRelatedState.initDiscoveredHiddenThemes(this._state.osmConnection).map(list => new Set(list)) this._otherThemes = MoreScreen.officialThemes.themes .filter(th => th.id !== state.layout.id) } @@ -45,7 +44,6 @@ export default class ThemeSearch implements GeocodingProvider { return [] } const sorted = MoreScreen.sortedByLowest(query, this._otherThemes, this._layersToIgnore) - console.log(">>>", sorted) return sorted .filter(sorted => sorted.lowest < 2) .map(th => th.theme) diff --git a/src/Logic/Osm/OsmConnection.ts b/src/Logic/Osm/OsmConnection.ts index b705e3839..1f98c6015 100644 --- a/src/Logic/Osm/OsmConnection.ts +++ b/src/Logic/Osm/OsmConnection.ts @@ -137,7 +137,6 @@ export class OsmConnection { this.preferencesHandler = new OsmPreferences(this.auth, this, this.fakeUser) if (options.oauth_token?.data !== undefined) { - console.log(options.oauth_token.data) this.auth.bootstrapToken(options.oauth_token.data, (err, result) => { console.log("Bootstrap token called back", err, result) this.AttemptLogin() @@ -160,10 +159,10 @@ export class OsmConnection { key: string, defaultValue: string = undefined, options?: { - documentation?: string prefix?: string } ): UIEventSource { + options ??= {prefix: "mapcomplete-"} return >this.preferencesHandler.GetPreference(key, defaultValue, options) } @@ -183,7 +182,6 @@ export class OsmConnection { this.userDetails.ping() console.log("Logged out") this.loadingStatus.setData("not-attempted") - this.preferencesHandler.preferences.setData(undefined) } /** diff --git a/src/Logic/Osm/OsmPreferences.ts b/src/Logic/Osm/OsmPreferences.ts index e634e499a..9e2fce8c3 100644 --- a/src/Logic/Osm/OsmPreferences.ts +++ b/src/Logic/Osm/OsmPreferences.ts @@ -1,151 +1,109 @@ -import { UIEventSource } from "../UIEventSource" -import UserDetails, { OsmConnection } from "./OsmConnection" -import { Utils } from "../../Utils" +import { Store, UIEventSource } from "../UIEventSource" +import { OsmConnection } from "./OsmConnection" import { LocalStorageSource } from "../Web/LocalStorageSource" import OSMAuthInstance = OSMAuth.osmAuth export class OsmPreferences { - /** - * A dictionary containing all the preferences. The 'preferenceSources' will be initialized from this - * We keep a local copy of them, to init mapcomplete with the previous choices and to be able to get the open changesets right away - */ - public preferences = LocalStorageSource.GetParsed>( - "all-osm-preferences", - {} - ) - /** - * A map containing the individual preference sources - * @private - */ - private readonly preferenceSources = new Map>() - private readonly auth: OSMAuthInstance - private userDetails: UIEventSource - private longPreferences = {} + + private normalPreferences: Record> = {} + private longPreferences: Record> = {} + private localStorageInited: Set = new Set() + + private readonly _allPreferences: UIEventSource> = new UIEventSource({}) + public readonly allPreferences: Store>> = this._allPreferences private readonly _fakeUser: boolean + private readonly auth: OSMAuthInstance + private readonly osmConnection: OsmConnection constructor(auth: OSMAuthInstance, osmConnection: OsmConnection, fakeUser: boolean = false) { this.auth = auth this._fakeUser = fakeUser - this.userDetails = osmConnection.userDetails + this.osmConnection = osmConnection osmConnection.OnLoggedIn(() => { - this.UpdatePreferences(true) + this.loadBulkPreferences() return true }) } - /** - * OSM preferences can be at most 255 chars - * @param key - * @param prefix - * @constructor - */ - public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { - if (this.longPreferences[prefix + key] !== undefined) { - return this.longPreferences[prefix + key] + private getLongValue(allPrefs: Record, key: string): string { + const count = Number(allPrefs[key + "-length"]) + let str = "" + for (let i = 0; i < count; i++) { + str += allPrefs[key + i] } + return str - const source = new UIEventSource(undefined, "long-osm-preference:" + prefix + key) - this.longPreferences[prefix + key] = source + } - const allStartWith = prefix + key + "-combined" - const subOptions = { prefix: "" } - // Gives the number of combined preferences - const length = this.GetPreference(allStartWith + "-length", "", subOptions) - const preferences = this.preferences - if ((allStartWith + "-length").length > 255) { - throw ( - "This preference key is too long, it has " + - key.length + - " characters, but at most " + - (255 - "-length".length - "-combined".length - prefix.length) + - " characters are allowed" - ) + private setPreferencesAll(key: string, value: string) { + if (this._allPreferences.data[key] !== value) { + this._allPreferences.data[key] = value + this._allPreferences.ping() } + } - source.addCallback((str) => { - if (str === undefined || str === "") { - return - } - if (str === null) { - console.error("Deleting " + allStartWith) - const count = parseInt(length.data) - for (let i = 0; i < count; i++) { - // Delete all the preferences - this.GetPreference(allStartWith + "-" + i, "", subOptions).setData("") - } - this.GetPreference(allStartWith + "-length", "", subOptions).setData("") - return + private initPreference(key: string, value: string = "", excludeFromAll: boolean = false): UIEventSource { + if (this.normalPreferences[key] !== undefined) { + return this.normalPreferences[key] + } + const pref = this.normalPreferences[key] = new UIEventSource(value, "preference: " + key) + if(value && !excludeFromAll){ + this.setPreferencesAll(key, value) + } + pref.addCallback(v => { + this.UploadPreference(key, v) + if(!excludeFromAll){ + this.setPreferencesAll(key, v) } + }) + return pref + } + private initLongPreference(key: string, initialValue: string): UIEventSource { + if (this.longPreferences[key] !== undefined) { + return this.longPreferences[key] + } + const pref = this.longPreferences[key] = new UIEventSource(initialValue, "long-preference-"+key) + const maxLength = 255 + const length = UIEventSource.asInt(this.initPreference(key + "-length", "0", true)) + if(initialValue){ + this.setPreferencesAll(key, initialValue) + } + pref.addCallback(v => { + length.set(Math.ceil(v.length / maxLength)) let i = 0 - while (str !== "") { - if (str === undefined || str === "undefined") { - source.setData(undefined) - throw ( - "Got 'undefined' or a literal string containing 'undefined' for a long preference with name " + - key - ) - } - if (str === "undefined") { - source.setData(undefined) - throw ( - "Got a literal string containing 'undefined' for a long preference with name " + - key - ) - } - if (i > 100) { - throw "This long preference is getting very long... " - } - this.GetPreference(allStartWith + "-" + i, "", subOptions).setData( - str.substr(0, 255) - ) - str = str.substr(255) + while (v.length > 0) { + this.UploadPreference(key + "-" + i, v.substring(0, maxLength)) i++ + v = v.substring(maxLength) } - length.setData("" + i) // We use I, the number of preference fields used + this.setPreferencesAll(key, v) }) + return pref + } - - function updateData(l: number) { - if (Object.keys(preferences.data).length === 0) { - // The preferences are still empty - they are not yet updated, so we delay updating for now - return + private async loadBulkPreferences() { + const prefs = await this.getPreferencesDict() + const isCombined = /-combined-/ + for (const key in prefs) { + if (key.endsWith("-combined-length")) { + const v = this.getLongValue(prefs, key.substring(0, key.length - "-length".length)) + this.initLongPreference(key, v) } - const prefsCount = Number(l) - if (prefsCount > 100) { - throw "Length to long" - } - let str = "" - for (let i = 0; i < prefsCount; i++) { - const key = allStartWith + "-" + i - if (preferences.data[key] === undefined) { - console.warn( - "Detected a broken combined preference:", - key, - "is undefined", - preferences - ) - continue - } - const v = preferences.data[key] - if(v === "undefined"){ - delete preferences.data[key] - continue - } - str += preferences.data[key] ?? "" + if (key.match(isCombined)) { + continue } - - source.setData(str) + this.initPreference(key, prefs[key]) } + } - length.addCallback((l) => { - updateData(Number(l)) - }) - this.preferences.addCallbackAndRun(() => { - updateData(Number(length.data)) - }) - - return source + /** + * OSM preferences can be at most 255 chars. + * This method chains multiple together. + * Values written into this key will be erased when the user logs in + */ + public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { + return this.getPreferenceSeedFromlocal(key, true, undefined, { prefix }) } public GetPreference( @@ -154,145 +112,108 @@ export class OsmPreferences { options?: { documentation?: string prefix?: string - } + }, + ) { + return this.getPreferenceSeedFromlocal(key, false, defaultValue, options) + } + + /** + * Gets a OSM-preference. + * The OSM-preference is cached in local storage and updated from the OSM.org as soon as those values come in. + * THis means that values written before being logged in might be erased by the cloud settings + * @param key + * @param defaultValue + * @param options + * @constructor + */ + private getPreferenceSeedFromlocal( + key: string, + long: boolean, + defaultValue: string = undefined, + options?: { + prefix?: string, + saveToLocalStorage?: true | boolean + }, ): UIEventSource { - const prefix: string = options?.prefix ?? "mapcomplete-" - if (key.startsWith(prefix) && prefix !== "") { - console.trace( - "A preference was requested which has a duplicate prefix in its key. This is probably a bug" - ) + if (options?.prefix) { + key = options.prefix + key } - key = prefix + key key = key.replace(/[:/"' {}.%\\]/g, "") - if (key.length >= 255) { - throw "Preferences: key length to big" - } - const cached = this.preferenceSources.get(key) - if (cached !== undefined) { - return cached + + + let pref : UIEventSource + const localStorage = LocalStorageSource.Get(key) + if(localStorage.data === "null" || localStorage.data === "undefined"){ + localStorage.set(undefined) } - if (this.userDetails.data.loggedIn && this.preferences.data[key] === undefined) { - this.UpdatePreferences() + if(long){ + pref = this.initLongPreference(key, localStorage.data ?? defaultValue) + }else{ + pref = this.initPreference(key, localStorage.data ?? defaultValue) } - const pref = new UIEventSource( - this.preferences.data[key] ?? defaultValue, - "osm-preference:" + key - ) - pref.addCallback((v) => { - this.UploadPreference(key, v) - }) - - this.preferences.addCallbackD((allPrefs) => { - const v = allPrefs[key] - if (v === undefined) { - return - } - pref.setData(v) - }) + if (this.localStorageInited.has(key)) { + return pref + } - this.preferenceSources.set(key, pref) + if (options?.saveToLocalStorage ?? true) { + pref.addCallback(v => localStorage.set(v)) // Keep a local copy + } + this.localStorageInited.add(key) return pref } public ClearPreferences() { - let isRunning = false - this.preferences.addCallback((prefs) => { - console.log("Cleaning preferences...") - if (Object.keys(prefs).length == 0) { - return - } - if (isRunning) { - return - } - isRunning = true - const prefixes = ["mapcomplete-"] - for (const key in prefs) { - const matches = prefixes.some((prefix) => key.startsWith(prefix)) - if (matches) { - console.log("Clearing ", key) - this.GetPreference(key, "", { prefix: "" }).setData("") - } - } - isRunning = false - return - }) + console.log("Starting to remove all preferences") + this.removeAllWithPrefix("") } - removeAllWithPrefix(prefix: string) { - for (const key in this.preferences.data) { - if (key.startsWith(prefix)) { - this.GetPreference(key, "", { prefix: "" }).setData(undefined) - console.log("Clearing preference", key) - } - } - this.preferences.ping() - } - private UpdatePreferences(forceUpdate?: boolean) { - if (this._fakeUser) { - return - } - this.auth.xhr( - { - method: "GET", - path: "/api/0.6/user/preferences", - }, - (error, value: XMLDocument) => { - if (error) { - console.log("Could not load preferences", error) - return - } - const prefs = value.getElementsByTagName("preference") - const seenKeys = new Set() - for (let i = 0; i < prefs.length; i++) { - const pref = prefs[i] - const k = pref.getAttribute("k") - this.preferences.data[k] = pref.getAttribute("v") - seenKeys.add(k) - } - if (forceUpdate) { - for (const key in this.preferences.data) { - if (seenKeys.has(key)) { - continue - } - console.log("Deleting key", key, "as we didn't find it upstream") - delete this.preferences.data[key] + /** + * Bulk-downloads all preferences + * @private + */ + private getPreferencesDict(): Promise> { + return new Promise>((resolve, reject) => { + this.auth.xhr( + { + method: "GET", + path: "/api/0.6/user/preferences", + }, + (error, value: XMLDocument) => { + if (error) { + console.log("Could not load preferences", error) + reject(error) + return } - } - - // We merge all the preferences: new keys are uploaded - // For differing values, the server overrides local changes - this.preferenceSources.forEach((preference, key) => { - const osmValue = this.preferences.data[key] - if (osmValue === undefined && preference.data !== undefined) { - // OSM doesn't know this value yet - this.UploadPreference(key, preference.data) - } else { - // OSM does have a value - set it - preference.setData(osmValue) + const prefs = value.getElementsByTagName("preference") + const dict: Record = {} + for (let i = 0; i < prefs.length; i++) { + const pref = prefs[i] + const k = pref.getAttribute("k") + dict[k] = pref.getAttribute("v") } - }) + resolve(dict) + }, + ) + }) - this.preferences.ping() - } - ) } + /** + * UPloads the given k=v to the OSM-server + * Deletes it if 'v' is undefined, null or empty + */ private UploadPreference(k: string, v: string) { - if (!this.userDetails.data.loggedIn) { + if (!this.osmConnection.userDetails.data.loggedIn) { console.debug(`Not saving preference ${k}: user not logged in`) return } - if (this.preferences.data[k] === v) { - return - } - console.debug("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15)) if (this._fakeUser) { return } - if (v === undefined || v === "") { + if (v === undefined || v === "" || v === null) { this.auth.xhr( { method: "DELETE", @@ -304,10 +225,8 @@ export class OsmPreferences { console.warn("Could not remove preference", error) return } - delete this.preferences.data[k] - this.preferences.ping() console.debug("Preference ", k, "removed!") - } + }, ) return } @@ -319,15 +238,38 @@ export class OsmPreferences { headers: { "Content-Type": "text/plain" }, content: v, }, - (error)=> { + (error) => { if (error) { console.warn(`Could not set preference "${k}"'`, error) return } - this.preferences.data[k] = v - this.preferences.ping() - console.debug(`Preference ${k} written!`) - } + }, ) } + + removeAllWithPrefix(prefix: string) { + for (const key in this.normalPreferences) { + if(key.startsWith(prefix)){ + this.normalPreferences[key].set(null) + } + } + for (const key in this.longPreferences) { + if(key.startsWith(prefix)){ + this.longPreferences[key].set(null) + } + } + } + + getExistingPreference(key: string, defaultValue: undefined, prefix: string ): UIEventSource { + if (prefix) { + key = prefix + key + } + key = key.replace(/[:/"' {}.%\\]/g, "") + + if(this.normalPreferences[key]){ + return this.normalPreferences[key] + } + return this.longPreferences[key] + + } } diff --git a/src/Logic/State/SearchState.ts b/src/Logic/State/SearchState.ts index 7d9b6a1ec..e717bb0ff 100644 --- a/src/Logic/State/SearchState.ts +++ b/src/Logic/State/SearchState.ts @@ -4,7 +4,6 @@ import GeocodingProvider, { GeocodingUtils, type SearchResult } from "../Geocoding/GeocodingProvider" -import { RecentSearch } from "../Geocoding/RecentSearch" import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" import CombinedSearcher from "../Geocoding/CombinedSearcher" import FilterSearch from "../Geocoding/FilterSearch" @@ -25,7 +24,6 @@ import ShowDataLayer from "../../UI/Map/ShowDataLayer" export default class SearchState { public readonly isSearching = new UIEventSource(false) - public readonly recentlySearched: RecentSearch public readonly feedback: UIEventSource = new UIEventSource(undefined) public readonly searchTerm: UIEventSource = new UIEventSource("") public readonly searchIsFocused = new UIEventSource(false) @@ -48,7 +46,6 @@ export default class SearchState { new PhotonSearch() // new NominatimGeocoding(), ] - this.recentlySearched = new RecentSearch(state) const bounds = state.mapProperties.bounds const suggestionsList = this.searchTerm.stabilized(250).mapD(search => { if (search.length === 0) { diff --git a/src/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts index 82c17484c..21ecec2b8 100644 --- a/src/Logic/State/UserRelatedState.ts +++ b/src/Logic/State/UserRelatedState.ts @@ -19,6 +19,104 @@ import { QueryParameters } from "../Web/QueryParameters" import { ThemeMetaTagging } from "./UserSettingsMetaTagging" import { MapProperties } from "../../Models/MapProperties" import Showdown from "showdown" +import { LocalStorageSource } from "../Web/LocalStorageSource" +import { GeocodeResult } from "../Geocoding/GeocodingProvider" + + +export class OptionallySyncedHistory { + + public readonly syncPreference: UIEventSource<"sync" | "local" | "no"> + public readonly value: Store + private readonly synced: UIEventSource + private readonly local: UIEventSource + private readonly thisSession: UIEventSource + private readonly _maxHistory: number + private readonly _isSame: (a: T, b: T) => boolean + private osmconnection: OsmConnection + + constructor(key: string, osmconnection: OsmConnection, maxHistory: number = 20, isSame?: (a: T, b: T) => boolean) { + this.osmconnection = osmconnection + this._maxHistory = maxHistory + this._isSame = isSame + this.syncPreference = osmconnection.GetPreference( + "preference-" + key + "-history", + "sync", + ) + console.log(">>>",key, this.syncPreference) + + const synced = this.synced = UIEventSource.asObject(osmconnection.GetLongPreference(key + "-history"), []) + const local = this.local = LocalStorageSource.GetParsed(key + "-history", []) + const thisSession = this.thisSession = new UIEventSource([], "optionally-synced:"+key+"(session only)") + this.syncPreference.addCallback(syncmode => { + if (syncmode === "sync") { + let list = [...thisSession.data, ...synced.data].slice(0, maxHistory) + if (this._isSame) { + for (let i = 0; i < list.length; i++) { + for (let j = i + 1; j < list.length; j++) { + if (this._isSame(list[i], list[j])) { + list.splice(j, 1) + } + } + } + } + synced.set(list) + } else if (syncmode === "local") { + local.set(synced.data?.slice(0, maxHistory)) + synced.set([]) + } else { + synced.set([]) + local.set([]) + } + }) + + this.value = this.syncPreference.bind(syncPref => this.getAppropriateStore(syncPref)) + + + } + + private getAppropriateStore(syncPref?: string) { + syncPref ??= this.syncPreference.data + if (syncPref === "sync") { + return this.synced + } + if (syncPref === "local") { + return this.local + } + return this.thisSession + } + + public add(t: T) { + const store = this.getAppropriateStore() + let oldList = store.data ?? [] + if (this._isSame) { + oldList = oldList.filter(x => !this._isSame(t, x)) + } + console.log("Setting new history:", store, [t, ...oldList]) + store.set([t, ...oldList].slice(0, this._maxHistory)) + } + + /** + * Adds the value when the user is actually logged in + * @param t + */ + public addDefferred(t: T) { + if (t === undefined) { + return + } + this.osmconnection.isLoggedIn.addCallbackAndRun(loggedIn => { + if (!loggedIn) { + return + } + this.add(t) + return true + }) + + } + + clear() { + this.getAppropriateStore().set([]) + } +} /** * The part of the state which keeps track of user-related stuff, e.g. the OSM-connection, @@ -62,7 +160,7 @@ export default class UserRelatedState { */ public readonly gpsLocationHistoryRetentionTime = new UIEventSource( 7 * 24 * 60 * 60, - "gps_location_retention" + "gps_location_retention", ) public readonly addNewFeatureMode = new UIEventSource< @@ -80,87 +178,62 @@ export default class UserRelatedState { public readonly preferencesAsTags: UIEventSource> private readonly _mapProperties: MapProperties - public readonly recentlyVisitedThemes: UIEventSource + public readonly recentlyVisitedThemes: OptionallySyncedHistory + public readonly recentlyVisitedSearch: OptionallySyncedHistory constructor( osmConnection: OsmConnection, layout?: LayoutConfig, featureSwitches?: FeatureSwitchState, - mapProperties?: MapProperties + mapProperties?: MapProperties, ) { this.osmConnection = osmConnection this._mapProperties = mapProperties this.showAllQuestionsAtOnce = UIEventSource.asBoolean( - this.osmConnection.GetPreference("show-all-questions", "false", { - documentation: - "Either 'true' or 'false'. If set, all questions will be shown all at once", - }) + this.osmConnection.GetPreference("show-all-questions", "false"), ) this.language = this.osmConnection.GetPreference("language") this.showTags = this.osmConnection.GetPreference("show_tags") this.showCrosshair = this.osmConnection.GetPreference("show_crosshair") this.fixateNorth = this.osmConnection.GetPreference("fixate-north") + console.log("Fixate north is:", this.fixateNorth) this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no") this.a11y = this.osmConnection.GetPreference("a11y") this.mangroveIdentity = new MangroveIdentity( this.osmConnection.GetLongPreference("identity", "mangrove"), - this.osmConnection.GetPreference("identity-creation-date", "mangrove") - ) - this.preferredBackgroundLayer = this.osmConnection.GetPreference( - "preferred-background-layer", - undefined, - { - documentation: - "The ID of a layer or layer category that MapComplete uses by default", - } + this.osmConnection.GetPreference("identity-creation-date", "mangrove"), ) + this.preferredBackgroundLayer = this.osmConnection.GetPreference("preferred-background-layer") this.addNewFeatureMode = this.osmConnection.GetPreference( "preferences-add-new-mode", "button_click_right", - { - documentation: "How adding a new feature is done", - } ) - this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", { - documentation: "The license under which new images are uploaded", - }) - this.installedUserThemes = this.InitInstalledUserThemes() + this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0") + this.installedUserThemes = UserRelatedState.initInstalledUserThemes(osmConnection) this.translationMode = this.initTranslationMode() this.homeLocation = this.initHomeLocation() this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches) - const prefs = this.osmConnection - this.recentlyVisitedThemes = UIEventSource.asObject(prefs.GetLongPreference("recently-visited-themes"), []) - if (layout) { - const osmConn = this.osmConnection - const recentlyVisited = this.recentlyVisitedThemes - - function update() { - if (!osmConn.isLoggedIn.data) { - return - } - const previously = recentlyVisited.data - if (previously[0] === layout.id) { - return true - } - const newThemes = Utils.Dedup([layout.id, ...previously]).slice(0, 30) - recentlyVisited.set(newThemes) - return true - } - - - this.recentlyVisitedThemes.addCallbackAndRun(() => update()) - this.osmConnection.isLoggedIn.addCallbackAndRun(() => update()) - } - + this.recentlyVisitedThemes = new OptionallySyncedHistory( + "theme", + this.osmConnection, + 10, + (a, b) => a === b, + ) + this.recentlyVisitedSearch = new OptionallySyncedHistory("places", + this.osmConnection, + 15, + (a, b) => a.osm_id === b.osm_id && a.osm_type === b.osm_type, + ) this.syncLanguage() + this.recentlyVisitedThemes.addDefferred(layout?.id) } private syncLanguage() { @@ -214,9 +287,9 @@ export default class UserRelatedState { } catch (e) { console.warn( "Removing theme " + - id + - " as it could not be parsed from the preferences; the content is:", - str + id + + " as it could not be parsed from the preferences; the content is:", + str, ) pref.setData(null) return undefined @@ -246,18 +319,31 @@ export default class UserRelatedState { title: layout.title.translations, shortDescription: layout.shortDescription.translations, definition: layout["definition"], - }) + }), ) } } - private InitInstalledUserThemes(): Store { + public static initInstalledUserThemes(osmConnection: OsmConnection): Store { const prefix = "mapcomplete-unofficial-theme-" - const postfix = "-combined-length" - return this.osmConnection.preferencesHandler.preferences.map((prefs) => + return osmConnection.preferencesHandler.allPreferences.map((prefs) => Object.keys(prefs) - .filter((k) => k.startsWith(prefix) && k.endsWith(postfix)) - .map((k) => k.substring(prefix.length, k.length - postfix.length)) + .filter((k) => k.startsWith(prefix)) + .map((k) => k.substring(prefix.length)), + ) + } + + /** + * List of all hidden themes that have been seen before + * @param osmConnection + */ + public static initDiscoveredHiddenThemes(osmConnection: OsmConnection): Store { + const prefix = "mapcomplete-hidden-theme-" + const userPreferences = osmConnection.preferencesHandler.allPreferences + return userPreferences.map((preferences) => + Object.keys(preferences) + .filter((key) => key.startsWith(prefix)) + .map((key) => key.substring(prefix.length, key.length - "-enabled".length)), ) } @@ -273,7 +359,7 @@ export default class UserRelatedState { return undefined } return [home.lon, home.lat] - }) + }), ).map((homeLonLat) => { if (homeLonLat === undefined) { return empty @@ -303,7 +389,7 @@ export default class UserRelatedState { * */ private initAmendedPrefs( layout?: LayoutConfig, - featureSwitches?: FeatureSwitchState + featureSwitches?: FeatureSwitchState, ): UIEventSource> { const amendedPrefs = new UIEventSource>({ _theme: layout?.id, @@ -324,23 +410,13 @@ export default class UserRelatedState { } const osmConnection = this.osmConnection - osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => { + osmConnection.preferencesHandler.allPreferences.addCallback((newPrefs) => { for (const k in newPrefs) { const v = newPrefs[k] - if (v === "undefined" || !v) { + if (v === "undefined" || v === "null" || !v) { continue } - if (k.endsWith("-combined-length")) { - const l = Number(v) - const key = k.substring(0, k.length - "length".length) - let combined = "" - for (let i = 0; i < l; i++) { - combined += (newPrefs[key + i]) - } - amendedPrefs.data[key.substring(0, key.length - "-combined-".length)] = combined - } else { - amendedPrefs.data[k] = newPrefs[k] - } + amendedPrefs.data[k] = newPrefs[k] ?? "" } amendedPrefs.ping() @@ -359,19 +435,19 @@ export default class UserRelatedState { const missingLayers = Utils.Dedup( untranslated .filter((k) => k.startsWith("layers:")) - .map((k) => k.slice("layers:".length).split(".")[0]) + .map((k) => k.slice("layers:".length).split(".")[0]), ) const zenLinks: { link: string; id: string }[] = Utils.NoNull([ hasMissingTheme ? { - id: "theme:" + layout.id, - link: LinkToWeblate.hrefToWeblateZen( - language, - "themes", - layout.id - ), - } + id: "theme:" + layout.id, + link: LinkToWeblate.hrefToWeblateZen( + language, + "themes", + layout.id, + ), + } : undefined, ...missingLayers.map((id) => ({ id: "layer:" + id, @@ -388,7 +464,7 @@ export default class UserRelatedState { } amendedPrefs.ping() }, - [this.translationMode] + [this.translationMode], ) this.mangroveIdentity.getKeyId().addCallbackAndRun((kid) => { @@ -407,7 +483,7 @@ export default class UserRelatedState { .makeHtml(userDetails.description) ?.replace(/>/g, ">") ?.replace(/</g, "<") - ?.replace(/\n/g, "") + ?.replace(/\n/g, ""), ) } @@ -418,7 +494,7 @@ export default class UserRelatedState { (c: { contributor: string; commits: number }) => { const replaced = c.contributor.toLowerCase().replace(/\s+/g, "") return replaced === simplifiedName - } + }, ) if (isTranslator) { amendedPrefs.data["_translation_contributions"] = "" + isTranslator.commits @@ -427,7 +503,7 @@ export default class UserRelatedState { (c: { contributor: string; commits: number }) => { const replaced = c.contributor.toLowerCase().replace(/\s+/g, "") return replaced === simplifiedName - } + }, ) if (isCodeContributor) { amendedPrefs.data["_code_contributions"] = "" + isCodeContributor.commits @@ -441,18 +517,14 @@ export default class UserRelatedState { // Language is managed separately continue } - if (tags[key + "-combined-0"]) { - // A combined value exists - if (tags[key].startsWith("undefined")) { - // Sometimes, a long string of 'undefined' will show up, we ignore them - continue - } - this.osmConnection.GetLongPreference(key, "").setData(tags[key]) - } else { - this.osmConnection - .GetPreference(key, undefined, { prefix: "" }) - .setData(tags[key]) + if(tags[key] === null){ + continue + } + let pref = this.osmConnection.preferencesHandler.getExistingPreference(key, undefined, "") + if (!pref) { + pref = this.osmConnection.GetPreference(key, undefined, {prefix: ""}) } + pref.set(tags[key]) } }) diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 9d2f8d2aa..a937ca907 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -67,7 +67,7 @@ import { LayerConfigJson } from "./ThemeConfig/Json/LayerConfigJson" import Hash from "../Logic/Web/Hash" import { GeoOperations } from "../Logic/GeoOperations" import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" -import { GeocodingUtils } from "../Logic/Geocoding/GeocodingProvider" +import { GeocodeResult, GeocodingUtils } from "../Logic/Geocoding/GeocodingProvider" import SearchState from "../Logic/State/SearchState" /** @@ -900,6 +900,20 @@ export default class ThemeViewState implements SpecialVisualizationState { } }) }) + + this.selectedElement.addCallbackD(selected => { + const [osm_type, osm_id] = selected.properties.id.split("/") + const [lon, lat] = GeoOperations.centerpointCoordinates(selected) + const layer = this.layout.getMatchingLayer(selected.properties) + const r = { + feature: selected, + display_name: selected.properties.name ?? selected.properties.alt_name ?? selected.properties.local_name ?? layer.title.GetRenderValue(selected.properties ?? {}).txt , + osm_id, osm_type, + lon, lat, + } + this.userRelatedState.recentlyVisitedSearch.add(r) + }) + new ThemeViewStateHashActor(this) new MetaTagging(this) new TitleHandler(this.selectedElement, this.featureProperties, this) diff --git a/src/UI/AllThemesGui.svelte b/src/UI/AllThemesGui.svelte index 0bdbdca4b..6f9c644bb 100644 --- a/src/UI/AllThemesGui.svelte +++ b/src/UI/AllThemesGui.svelte @@ -45,9 +45,9 @@ const officialThemes: MinimalLayoutInformation[] = MoreScreen.officialThemes.themes.filter(th => th.hideFromOverview === false) const hiddenThemes: MinimalLayoutInformation[] = MoreScreen.officialThemes.themes.filter(th => th.hideFromOverview === true) - let visitedHiddenThemes: Store = MoreScreen.knownHiddenThemes(state.osmConnection) + let visitedHiddenThemes: Store = UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection) .map((knownIds) => hiddenThemes.filter((theme) => - knownIds.has(theme.id) || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet" + knownIds.indexOf(theme.id) >= 0 || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet" )) diff --git a/src/UI/Base/DotMenu.svelte b/src/UI/Base/DotMenu.svelte index f3630600f..1ee5703c3 100644 --- a/src/UI/Base/DotMenu.svelte +++ b/src/UI/Base/DotMenu.svelte @@ -5,18 +5,19 @@ /** * A menu, opened by a dot */ - export let dotColor = "var(--background-interactive)" - export let placement: "left" | "right" | "top" | "bottom" = "left" + export let open = new UIEventSource(false) function toggle() { open.set(!open.data) } + +
@@ -31,6 +32,7 @@ :global(.dots-menu > path) { fill: var(--interactive-background); transition: fill 350ms linear; + cursor: pointer; } @@ -41,18 +43,21 @@ .collapsable { max-width: 100rem; max-height: 100rem; - transition: max-width 500ms ease-in-out, border 400ms linear; + transition: border 150ms linear, max-width 500ms linear, max-height 500ms linear; overflow: hidden; flex-wrap: nowrap; text-wrap: none; width: max-content; box-shadow: #ccc ; white-space: nowrap; + border: 1px solid var(--button-background); } .collapsed { max-width: 0; - border: 2px solid #00000000 + max-height: 0; + border: 2px solid #00000000; + pointer-events: none; } diff --git a/src/UI/BigComponents/MoreScreen.ts b/src/UI/BigComponents/MoreScreen.ts index 997be4c92..b3db72c84 100644 --- a/src/UI/BigComponents/MoreScreen.ts +++ b/src/UI/BigComponents/MoreScreen.ts @@ -188,18 +188,4 @@ export default class MoreScreen { return `${linkPrefix}` } - /** - * Gives all the IDs of the hidden themes which were previously visited - * @param osmConnection - */ - public static knownHiddenThemes(osmConnection: OsmConnection): Store> { - const prefix = "mapcomplete-hidden-theme-" - const userPreferences = osmConnection.preferencesHandler.preferences - return userPreferences.map((preferences) => - new Set( - Object.keys(preferences) - .filter((key) => key.startsWith(prefix)) - .map((key) => key.substring(prefix.length, key.length - "-enabled".length)) - )) - } } diff --git a/src/UI/Popup/AllTagsPanel.svelte b/src/UI/Popup/AllTagsPanel.svelte index 86238924f..519b06189 100644 --- a/src/UI/Popup/AllTagsPanel.svelte +++ b/src/UI/Popup/AllTagsPanel.svelte @@ -2,6 +2,8 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource" import SimpleMetaTaggers from "../../Logic/SimpleMetaTagger" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" + import Searchbar from "../Base/Searchbar.svelte" + import Translations from "../i18n/Translations" export let tags: UIEventSource> export let tagKeys = tags.map((tgs) => (tgs === undefined ? [] : Object.keys(tgs))) @@ -31,9 +33,11 @@ const metaKeys: string[] = [].concat(...SimpleMetaTaggers.metatags.map((k) => k.keys)) let allCalculatedTags = new Set([...calculatedTags, ...metaKeys]) + let search = new UIEventSource("")
+ @@ -43,7 +47,7 @@ {#each $tagKeys as key} - {#if !allCalculatedTags.has(key)} + {#if !allCalculatedTags.has(key) && ($search?.length === 0 || key.toLowerCase().indexOf($search.toLowerCase()) >= 0)} - @@ -121,7 +121,7 @@ - diff --git a/src/UI/SpecialVisualization.ts b/src/UI/SpecialVisualization.ts index 087320a0d..56244e4d8 100644 --- a/src/UI/SpecialVisualization.ts +++ b/src/UI/SpecialVisualization.ts @@ -1,11 +1,7 @@ import { Store, UIEventSource } from "../Logic/UIEventSource" import BaseUIElement from "./BaseUIElement" -import LayoutConfig, { MinimalLayoutInformation } from "../Models/ThemeConfig/LayoutConfig" -import { - FeatureSource, - IndexedFeatureSource, - WritableFeatureSource -} from "../Logic/FeatureSource/FeatureSource" +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource" import { OsmConnection } from "../Logic/Osm/OsmConnection" import { Changes } from "../Logic/Osm/Changes" import { ExportableMap, MapProperties } from "../Models/MapProperties" @@ -18,7 +14,6 @@ import LayerConfig from "../Models/ThemeConfig/LayerConfig" import FeatureSwitchState from "../Logic/State/FeatureSwitchState" import { MenuState } from "../Models/MenuState" import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader" -import { RasterLayerPolygon } from "../Models/RasterLayers" import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" import { OsmTags } from "../Models/OsmFeature" import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource" @@ -30,6 +25,8 @@ import { Map as MlMap } from "maplibre-gl" import ShowDataLayer from "./Map/ShowDataLayer" import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" import SearchState from "../Logic/State/SearchState" +import UserRelatedState, { OptionallySyncedHistory } from "../Logic/State/UserRelatedState" +import GeocodeResult from "./Search/GeocodeResult.svelte" /** * The state needed to render a special Visualisation. @@ -80,15 +77,7 @@ export interface SpecialVisualizationState { readonly fullNodeDatabase?: FullNodeDatabaseSource readonly perLayer: ReadonlyMap - readonly userRelatedState: { - readonly imageLicense: UIEventSource - readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> - readonly mangroveIdentity: MangroveIdentity - readonly showAllQuestionsAtOnce: UIEventSource - readonly preferencesAsTags: UIEventSource> - readonly language: UIEventSource - readonly recentlyVisitedThemes: Store - } + readonly userRelatedState: UserRelatedState readonly imageUploadManager: ImageUploadManager diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index a19040e7e..6a8cf9b77 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -2101,6 +2101,23 @@ export default class SpecialVisualizations { }) }, }, + { + funcName:"clear_all", + docs: "Clears all user preferences", + needsUrls: [], + args: [ + { + name: "text", + doc: "Text to show on the button" + } + ], + constr(state: SpecialVisualizationState, tagSource: UIEventSource>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement { + const text = argument[0] + return new SubtleButton(undefined, text).onClick(() => { + state.osmConnection.preferencesHandler.ClearPreferences() + }) + } + } ] specialVisualizations.push(new AutoApplyButton(specialVisualizations)) diff --git a/src/UI/Test.svelte b/src/UI/Test.svelte index 080c36b02..220a4ef6f 100644 --- a/src/UI/Test.svelte +++ b/src/UI/Test.svelte @@ -1,40 +1,3 @@ - -
- -
diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 651e793f9..f8399e9f5 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -60,7 +60,6 @@ let compass = Orientation.singleton.alpha let compassLoaded = Orientation.singleton.gotMeasurement let hash = Hash.hash - let previewedImage = state.previewedImage let addNewFeatureMode = state.userRelatedState.addNewFeatureMode let gpsAvailable = state.geolocation.geolocationState.gpsAvailable let gpsButtonAriaLabel = state.geolocation.geolocationState.gpsStateExplanation
KeyNormal tags
{key} diff --git a/src/UI/Search/GeocodeResult.svelte b/src/UI/Search/GeocodeResult.svelte index d7fe871b9..3ab188863 100644 --- a/src/UI/Search/GeocodeResult.svelte +++ b/src/UI/Search/GeocodeResult.svelte @@ -22,7 +22,7 @@ if (entry.feature?.properties?.id) { layer = state.layout.getMatchingLayer(entry.feature.properties) tags = state.featureProperties.getStore(entry.feature.properties.id) - descriptionTr = layer.tagRenderings.find(tr => tr.labels.indexOf("description") >= 0) + descriptionTr = layer?.tagRenderings?.find(tr => tr.labels.indexOf("description") >= 0) } let dispatch = createEventDispatcher<{ select }>() @@ -47,7 +47,7 @@ if (entry.feature?.properties?.id) { state.selectedElement.set(entry.feature) } - state.searchState.recentlySearched.addSelected(entry) + state.userRelatedState.recentlyVisitedSearch.add(entry) dispatch("select") } @@ -81,11 +81,11 @@
- {#if descriptionTr} + {#if descriptionTr && tags} {/if} - {#if descriptionTr && entry.description} + {#if descriptionTr && tags && entry.description} – {/if} {#if entry.description} diff --git a/src/UI/Search/SearchResults.svelte b/src/UI/Search/SearchResults.svelte index 61ce42067..6d4ec461f 100644 --- a/src/UI/Search/SearchResults.svelte +++ b/src/UI/Search/SearchResults.svelte @@ -22,8 +22,8 @@ export let state: ThemeViewState let activeFilters: Store = state.layerState.activeFilters.map(fs => fs.filter(f => Constants.priviliged_layers.indexOf(f.layer.id) < 0)) - let recentlySeen: UIEventSource = state.searchState.recentlySearched.seenThisSession - let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 6)) + let recentlySeen: Store = state.userRelatedState.recentlyVisitedSearch.value + let recentThemes = state.userRelatedState.recentlyVisitedThemes.value.map(themes => themes.filter(th => th.id !== state.layout.id).slice(0, 6)) let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview let searchTerm = state.searchState.searchTerm let results = state.searchState.suggestions @@ -97,7 +97,7 @@