Rework preferences handling, improve search

This commit is contained in:
Pieter Vander Vennet 2024-09-10 02:19:55 +02:00
parent b45cfcaa18
commit 4085bbc1ac
19 changed files with 438 additions and 534 deletions

View file

@ -838,20 +838,20 @@
}, },
"mappings": [ "mappings": [
{ {
"if": "mapcomplete-theme-history=sync", "if": "mapcomplete-preference-theme-history=sync",
"alsoShowIf": "mapcomplete-theme-history=", "alsoShowIf": "mapcomplete-preference-theme-history=",
"then": { "then": {
"en": "Save the visited thematic maps and sync them via openstreetmap.org. OpenStreetMap and all apps you use can see this history" "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": { "then": {
"en": "Save the visited thematic maps on my device" "en": "Save the visited thematic maps on my device"
} }
}, },
{ {
"if": "mapcomplete-theme-history=no", "if": "mapcomplete-preference-theme-history=no",
"then": { "then": {
"en": "Don't save visited thematic maps" "en": "Don't save visited thematic maps"
} }
@ -868,20 +868,20 @@
}, },
"mappings": [ "mappings": [
{ {
"if": "mapcomplete-search-history=sync", "if": "mapcomplete-preference-search-history=sync",
"alsoShowIf": "mapcomplete-search-history=", "alsoShowIf": "mapcomplete-preference-search-history=",
"then": { "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" "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": { "then": {
"en": "Save the locations you search for and inspect on my device" "en": "Save the locations you search for and inspect on my device"
} }
}, },
{ {
"if": "mapcomplete-search-history=no", "if": "mapcomplete-preference-search-history=no",
"then": { "then": {
"en": "Don't save the locations you search for and inspect " "en": "Don't save the locations you search for and inspect "
} }

View file

@ -24,7 +24,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
constructor(state: SpecialVisualizationState) { constructor(state: SpecialVisualizationState) {
const features: Store<Feature[]> = Stores.ListStabilized( const features: Store<Feature[]> = Stores.ListStabilized(
state.osmConnection.preferencesHandler.preferences.map((prefs) => { state.osmConnection.preferencesHandler.allPreferences.map((prefs) => {
const feats: Feature[] = [] const feats: Feature[] = []
const allIds = new Set<string>() const allIds = new Set<string>()
for (const key in prefs) { for (const key in prefs) {

View file

@ -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<GeocodeResult[]>
constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) {
const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches")
this.seenThisSession = new UIEventSource<GeocodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
prefs.addCallbackAndRunD(pref => {
if (pref === "") {
return
}
try {
const simpleArr = <GeocodeResult[]>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 = <GeocodeResult>{
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])
}
}

View file

@ -1,13 +1,12 @@
import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider" import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider"
import * as themeOverview from "../../assets/generated/theme_overview.json"
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import { SpecialVisualizationState } from "../../UI/SpecialVisualization" import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
import MoreScreen from "../../UI/BigComponents/MoreScreen" import MoreScreen from "../../UI/BigComponents/MoreScreen"
import { ImmutableStore, Store } from "../UIEventSource" import { ImmutableStore, Store } from "../UIEventSource"
import UserRelatedState from "../State/UserRelatedState"
export default class ThemeSearch implements GeocodingProvider { export default class ThemeSearch implements GeocodingProvider {
private static allThemes: MinimalLayoutInformation[] = (themeOverview["default"] ?? themeOverview)
private readonly _state: SpecialVisualizationState private readonly _state: SpecialVisualizationState
private readonly _knownHiddenThemes: Store<Set<string>> private readonly _knownHiddenThemes: Store<Set<string>>
private readonly _suggestionLimit: number private readonly _suggestionLimit: number
@ -18,7 +17,7 @@ export default class ThemeSearch implements GeocodingProvider {
this._state = state this._state = state
this._layersToIgnore = state.layout.layers.map(l => l.id) this._layersToIgnore = state.layout.layers.map(l => l.id)
this._suggestionLimit = suggestionLimit 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 this._otherThemes = MoreScreen.officialThemes.themes
.filter(th => th.id !== state.layout.id) .filter(th => th.id !== state.layout.id)
} }
@ -45,7 +44,6 @@ export default class ThemeSearch implements GeocodingProvider {
return [] return []
} }
const sorted = MoreScreen.sortedByLowest(query, this._otherThemes, this._layersToIgnore) const sorted = MoreScreen.sortedByLowest(query, this._otherThemes, this._layersToIgnore)
console.log(">>>", sorted)
return sorted return sorted
.filter(sorted => sorted.lowest < 2) .filter(sorted => sorted.lowest < 2)
.map(th => th.theme) .map(th => th.theme)

View file

@ -137,7 +137,6 @@ export class OsmConnection {
this.preferencesHandler = new OsmPreferences(this.auth, this, this.fakeUser) this.preferencesHandler = new OsmPreferences(this.auth, this, this.fakeUser)
if (options.oauth_token?.data !== undefined) { if (options.oauth_token?.data !== undefined) {
console.log(options.oauth_token.data)
this.auth.bootstrapToken(options.oauth_token.data, (err, result) => { this.auth.bootstrapToken(options.oauth_token.data, (err, result) => {
console.log("Bootstrap token called back", err, result) console.log("Bootstrap token called back", err, result)
this.AttemptLogin() this.AttemptLogin()
@ -160,10 +159,10 @@ export class OsmConnection {
key: string, key: string,
defaultValue: string = undefined, defaultValue: string = undefined,
options?: { options?: {
documentation?: string
prefix?: string prefix?: string
} }
): UIEventSource<T | undefined> { ): UIEventSource<T | undefined> {
options ??= {prefix: "mapcomplete-"}
return <UIEventSource<T>>this.preferencesHandler.GetPreference(key, defaultValue, options) return <UIEventSource<T>>this.preferencesHandler.GetPreference(key, defaultValue, options)
} }
@ -183,7 +182,6 @@ export class OsmConnection {
this.userDetails.ping() this.userDetails.ping()
console.log("Logged out") console.log("Logged out")
this.loadingStatus.setData("not-attempted") this.loadingStatus.setData("not-attempted")
this.preferencesHandler.preferences.setData(undefined)
} }
/** /**

View file

@ -1,151 +1,109 @@
import { UIEventSource } from "../UIEventSource" import { Store, UIEventSource } from "../UIEventSource"
import UserDetails, { OsmConnection } from "./OsmConnection" import { OsmConnection } from "./OsmConnection"
import { Utils } from "../../Utils"
import { LocalStorageSource } from "../Web/LocalStorageSource" import { LocalStorageSource } from "../Web/LocalStorageSource"
import OSMAuthInstance = OSMAuth.osmAuth import OSMAuthInstance = OSMAuth.osmAuth
export class OsmPreferences { export class OsmPreferences {
/**
* A dictionary containing all the preferences. The 'preferenceSources' will be initialized from this private normalPreferences: Record<string, UIEventSource<string>> = {}
* 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 private longPreferences: Record<string, UIEventSource<string>> = {}
*/ private localStorageInited: Set<string> = new Set()
public preferences = LocalStorageSource.GetParsed<Record<string, string>>(
"all-osm-preferences", private readonly _allPreferences: UIEventSource<Record<string, string>> = new UIEventSource({})
{} public readonly allPreferences: Store<Readonly<Record<string, string>>> = this._allPreferences
)
/**
* A map containing the individual preference sources
* @private
*/
private readonly preferenceSources = new Map<string, UIEventSource<string>>()
private readonly auth: OSMAuthInstance
private userDetails: UIEventSource<UserDetails>
private longPreferences = {}
private readonly _fakeUser: boolean private readonly _fakeUser: boolean
private readonly auth: OSMAuthInstance
private readonly osmConnection: OsmConnection
constructor(auth: OSMAuthInstance, osmConnection: OsmConnection, fakeUser: boolean = false) { constructor(auth: OSMAuthInstance, osmConnection: OsmConnection, fakeUser: boolean = false) {
this.auth = auth this.auth = auth
this._fakeUser = fakeUser this._fakeUser = fakeUser
this.userDetails = osmConnection.userDetails this.osmConnection = osmConnection
osmConnection.OnLoggedIn(() => { osmConnection.OnLoggedIn(() => {
this.UpdatePreferences(true) this.loadBulkPreferences()
return true return true
}) })
} }
private getLongValue(allPrefs: Record<string, string>, key: string): string {
const count = Number(allPrefs[key + "-length"])
let str = ""
for (let i = 0; i < count; i++) {
str += allPrefs[key + i]
}
return str
}
private setPreferencesAll(key: string, value: string) {
if (this._allPreferences.data[key] !== value) {
this._allPreferences.data[key] = value
this._allPreferences.ping()
}
}
private initPreference(key: string, value: string = "", excludeFromAll: boolean = false): UIEventSource<string> {
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<string> {
if (this.longPreferences[key] !== undefined) {
return this.longPreferences[key]
}
const pref = this.longPreferences[key] = new UIEventSource<string>(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 (v.length > 0) {
this.UploadPreference(key + "-" + i, v.substring(0, maxLength))
i++
v = v.substring(maxLength)
}
this.setPreferencesAll(key, v)
})
return pref
}
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)
}
if (key.match(isCombined)) {
continue
}
this.initPreference(key, prefs[key])
}
}
/** /**
* OSM preferences can be at most 255 chars * OSM preferences can be at most 255 chars.
* @param key * This method chains multiple together.
* @param prefix * Values written into this key will be erased when the user logs in
* @constructor
*/ */
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
if (this.longPreferences[prefix + key] !== undefined) { return this.getPreferenceSeedFromlocal(key, true, undefined, { prefix })
return this.longPreferences[prefix + key]
}
const source = new UIEventSource<string>(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"
)
}
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
}
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)
i++
}
length.setData("" + i) // We use I, the number of preference fields used
})
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
}
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] ?? ""
}
source.setData(str)
}
length.addCallback((l) => {
updateData(Number(l))
})
this.preferences.addCallbackAndRun(() => {
updateData(Number(length.data))
})
return source
} }
public GetPreference( public GetPreference(
@ -154,145 +112,108 @@ export class OsmPreferences {
options?: { options?: {
documentation?: string documentation?: string
prefix?: 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<string> { ): UIEventSource<string> {
const prefix: string = options?.prefix ?? "mapcomplete-" if (options?.prefix) {
if (key.startsWith(prefix) && prefix !== "") { key = options.prefix + key
console.trace(
"A preference was requested which has a duplicate prefix in its key. This is probably a bug"
)
} }
key = prefix + key
key = key.replace(/[:/"' {}.%\\]/g, "") key = key.replace(/[:/"' {}.%\\]/g, "")
if (key.length >= 255) {
throw "Preferences: key length to big"
let pref : UIEventSource<string>
const localStorage = LocalStorageSource.Get(key)
if(localStorage.data === "null" || localStorage.data === "undefined"){
localStorage.set(undefined)
} }
const cached = this.preferenceSources.get(key) if(long){
if (cached !== undefined) { pref = this.initLongPreference(key, localStorage.data ?? defaultValue)
return cached }else{
} pref = this.initPreference(key, localStorage.data ?? defaultValue)
if (this.userDetails.data.loggedIn && this.preferences.data[key] === undefined) {
this.UpdatePreferences()
} }
const pref = new UIEventSource<string>( if (this.localStorageInited.has(key)) {
this.preferences.data[key] ?? defaultValue, return pref
"osm-preference:" + key }
)
pref.addCallback((v) => {
this.UploadPreference(key, v)
})
this.preferences.addCallbackD((allPrefs) => { if (options?.saveToLocalStorage ?? true) {
const v = allPrefs[key] pref.addCallback(v => localStorage.set(v)) // Keep a local copy
if (v === undefined) { }
return this.localStorageInited.add(key)
}
pref.setData(v)
})
this.preferenceSources.set(key, pref)
return pref return pref
} }
public ClearPreferences() { public ClearPreferences() {
let isRunning = false console.log("Starting to remove all preferences")
this.preferences.addCallback((prefs) => { this.removeAllWithPrefix("")
console.log("Cleaning preferences...") }
if (Object.keys(prefs).length == 0) {
return
} /**
if (isRunning) { * Bulk-downloads all preferences
return * @private
} */
isRunning = true private getPreferencesDict(): Promise<Record<string, string>> {
const prefixes = ["mapcomplete-"] return new Promise<Record<string, string>>((resolve, reject) => {
for (const key in prefs) { this.auth.xhr(
const matches = prefixes.some((prefix) => key.startsWith(prefix)) {
if (matches) { method: "GET",
console.log("Clearing ", key) path: "/api/0.6/user/preferences",
this.GetPreference(key, "", { prefix: "" }).setData("") },
} (error, value: XMLDocument) => {
} if (error) {
isRunning = false console.log("Could not load preferences", error)
return reject(error)
return
}
const prefs = value.getElementsByTagName("preference")
const dict: Record<string, string> = {}
for (let i = 0; i < prefs.length; i++) {
const pref = prefs[i]
const k = pref.getAttribute("k")
dict[k] = pref.getAttribute("v")
}
resolve(dict)
},
)
}) })
} }
removeAllWithPrefix(prefix: string) { /**
for (const key in this.preferences.data) { * UPloads the given k=v to the OSM-server
if (key.startsWith(prefix)) { * Deletes it if 'v' is undefined, null or empty
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<string>()
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]
}
}
// 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)
}
})
this.preferences.ping()
}
)
}
private UploadPreference(k: string, v: string) { 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`) console.debug(`Not saving preference ${k}: user not logged in`)
return return
} }
if (this.preferences.data[k] === v) {
return
}
console.debug("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15))
if (this._fakeUser) { if (this._fakeUser) {
return return
} }
if (v === undefined || v === "") { if (v === undefined || v === "" || v === null) {
this.auth.xhr( this.auth.xhr(
{ {
method: "DELETE", method: "DELETE",
@ -304,10 +225,8 @@ export class OsmPreferences {
console.warn("Could not remove preference", error) console.warn("Could not remove preference", error)
return return
} }
delete this.preferences.data[k]
this.preferences.ping()
console.debug("Preference ", k, "removed!") console.debug("Preference ", k, "removed!")
} },
) )
return return
} }
@ -319,15 +238,38 @@ export class OsmPreferences {
headers: { "Content-Type": "text/plain" }, headers: { "Content-Type": "text/plain" },
content: v, content: v,
}, },
(error)=> { (error) => {
if (error) { if (error) {
console.warn(`Could not set preference "${k}"'`, error) console.warn(`Could not set preference "${k}"'`, error)
return 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<string> {
if (prefix) {
key = prefix + key
}
key = key.replace(/[:/"' {}.%\\]/g, "")
if(this.normalPreferences[key]){
return this.normalPreferences[key]
}
return this.longPreferences[key]
}
} }

View file

@ -4,7 +4,6 @@ import GeocodingProvider, {
GeocodingUtils, GeocodingUtils,
type SearchResult type SearchResult
} from "../Geocoding/GeocodingProvider" } from "../Geocoding/GeocodingProvider"
import { RecentSearch } from "../Geocoding/RecentSearch"
import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource"
import CombinedSearcher from "../Geocoding/CombinedSearcher" import CombinedSearcher from "../Geocoding/CombinedSearcher"
import FilterSearch from "../Geocoding/FilterSearch" import FilterSearch from "../Geocoding/FilterSearch"
@ -25,7 +24,6 @@ import ShowDataLayer from "../../UI/Map/ShowDataLayer"
export default class SearchState { export default class SearchState {
public readonly isSearching = new UIEventSource(false) public readonly isSearching = new UIEventSource(false)
public readonly recentlySearched: RecentSearch
public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined) public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("") public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("")
public readonly searchIsFocused = new UIEventSource(false) public readonly searchIsFocused = new UIEventSource(false)
@ -48,7 +46,6 @@ export default class SearchState {
new PhotonSearch() // new NominatimGeocoding(), new PhotonSearch() // new NominatimGeocoding(),
] ]
this.recentlySearched = new RecentSearch(state)
const bounds = state.mapProperties.bounds const bounds = state.mapProperties.bounds
const suggestionsList = this.searchTerm.stabilized(250).mapD(search => { const suggestionsList = this.searchTerm.stabilized(250).mapD(search => {
if (search.length === 0) { if (search.length === 0) {

View file

@ -19,6 +19,104 @@ import { QueryParameters } from "../Web/QueryParameters"
import { ThemeMetaTagging } from "./UserSettingsMetaTagging" import { ThemeMetaTagging } from "./UserSettingsMetaTagging"
import { MapProperties } from "../../Models/MapProperties" import { MapProperties } from "../../Models/MapProperties"
import Showdown from "showdown" import Showdown from "showdown"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { GeocodeResult } from "../Geocoding/GeocodingProvider"
export class OptionallySyncedHistory<T> {
public readonly syncPreference: UIEventSource<"sync" | "local" | "no">
public readonly value: Store<T[]>
private readonly synced: UIEventSource<T[]>
private readonly local: UIEventSource<T[]>
private readonly thisSession: UIEventSource<T[]>
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<T[]>(osmconnection.GetLongPreference(key + "-history"), [])
const local = this.local = LocalStorageSource.GetParsed<T[]>(key + "-history", [])
const thisSession = this.thisSession = new UIEventSource<T[]>([], "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, * 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( public readonly gpsLocationHistoryRetentionTime = new UIEventSource(
7 * 24 * 60 * 60, 7 * 24 * 60 * 60,
"gps_location_retention" "gps_location_retention",
) )
public readonly addNewFeatureMode = new UIEventSource< public readonly addNewFeatureMode = new UIEventSource<
@ -80,87 +178,62 @@ export default class UserRelatedState {
public readonly preferencesAsTags: UIEventSource<Record<string, string>> public readonly preferencesAsTags: UIEventSource<Record<string, string>>
private readonly _mapProperties: MapProperties private readonly _mapProperties: MapProperties
public readonly recentlyVisitedThemes: UIEventSource<string[]> public readonly recentlyVisitedThemes: OptionallySyncedHistory<string>
public readonly recentlyVisitedSearch: OptionallySyncedHistory<GeocodeResult>
constructor( constructor(
osmConnection: OsmConnection, osmConnection: OsmConnection,
layout?: LayoutConfig, layout?: LayoutConfig,
featureSwitches?: FeatureSwitchState, featureSwitches?: FeatureSwitchState,
mapProperties?: MapProperties mapProperties?: MapProperties,
) { ) {
this.osmConnection = osmConnection this.osmConnection = osmConnection
this._mapProperties = mapProperties this._mapProperties = mapProperties
this.showAllQuestionsAtOnce = UIEventSource.asBoolean( this.showAllQuestionsAtOnce = UIEventSource.asBoolean(
this.osmConnection.GetPreference("show-all-questions", "false", { this.osmConnection.GetPreference("show-all-questions", "false"),
documentation:
"Either 'true' or 'false'. If set, all questions will be shown all at once",
})
) )
this.language = this.osmConnection.GetPreference("language") this.language = this.osmConnection.GetPreference("language")
this.showTags = this.osmConnection.GetPreference("show_tags") this.showTags = this.osmConnection.GetPreference("show_tags")
this.showCrosshair = this.osmConnection.GetPreference("show_crosshair") this.showCrosshair = this.osmConnection.GetPreference("show_crosshair")
this.fixateNorth = this.osmConnection.GetPreference("fixate-north") this.fixateNorth = this.osmConnection.GetPreference("fixate-north")
console.log("Fixate north is:", this.fixateNorth)
this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no") this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no")
this.a11y = this.osmConnection.GetPreference("a11y") this.a11y = this.osmConnection.GetPreference("a11y")
this.mangroveIdentity = new MangroveIdentity( this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.GetLongPreference("identity", "mangrove"), this.osmConnection.GetLongPreference("identity", "mangrove"),
this.osmConnection.GetPreference("identity-creation-date", "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.preferredBackgroundLayer = this.osmConnection.GetPreference("preferred-background-layer")
this.addNewFeatureMode = this.osmConnection.GetPreference( this.addNewFeatureMode = this.osmConnection.GetPreference(
"preferences-add-new-mode", "preferences-add-new-mode",
"button_click_right", "button_click_right",
{
documentation: "How adding a new feature is done",
}
) )
this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", { this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0")
documentation: "The license under which new images are uploaded", this.installedUserThemes = UserRelatedState.initInstalledUserThemes(osmConnection)
})
this.installedUserThemes = this.InitInstalledUserThemes()
this.translationMode = this.initTranslationMode() this.translationMode = this.initTranslationMode()
this.homeLocation = this.initHomeLocation() this.homeLocation = this.initHomeLocation()
this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches) this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches)
const prefs = this.osmConnection this.recentlyVisitedThemes = new OptionallySyncedHistory<string>(
this.recentlyVisitedThemes = UIEventSource.asObject(prefs.GetLongPreference("recently-visited-themes"), []) "theme",
if (layout) { this.osmConnection,
const osmConn = this.osmConnection 10,
const recentlyVisited = this.recentlyVisitedThemes (a, b) => a === b,
)
function update() { this.recentlyVisitedSearch = new OptionallySyncedHistory<GeocodeResult>("places",
if (!osmConn.isLoggedIn.data) { this.osmConnection,
return 15,
} (a, b) => a.osm_id === b.osm_id && a.osm_type === b.osm_type,
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.syncLanguage() this.syncLanguage()
this.recentlyVisitedThemes.addDefferred(layout?.id)
} }
private syncLanguage() { private syncLanguage() {
@ -214,9 +287,9 @@ export default class UserRelatedState {
} catch (e) { } catch (e) {
console.warn( console.warn(
"Removing theme " + "Removing theme " +
id + id +
" as it could not be parsed from the preferences; the content is:", " as it could not be parsed from the preferences; the content is:",
str str,
) )
pref.setData(null) pref.setData(null)
return undefined return undefined
@ -246,18 +319,31 @@ export default class UserRelatedState {
title: layout.title.translations, title: layout.title.translations,
shortDescription: layout.shortDescription.translations, shortDescription: layout.shortDescription.translations,
definition: layout["definition"], definition: layout["definition"],
}) }),
) )
} }
} }
private InitInstalledUserThemes(): Store<string[]> { public static initInstalledUserThemes(osmConnection: OsmConnection): Store<string[]> {
const prefix = "mapcomplete-unofficial-theme-" const prefix = "mapcomplete-unofficial-theme-"
const postfix = "-combined-length" return osmConnection.preferencesHandler.allPreferences.map((prefs) =>
return this.osmConnection.preferencesHandler.preferences.map((prefs) =>
Object.keys(prefs) Object.keys(prefs)
.filter((k) => k.startsWith(prefix) && k.endsWith(postfix)) .filter((k) => k.startsWith(prefix))
.map((k) => k.substring(prefix.length, k.length - postfix.length)) .map((k) => k.substring(prefix.length)),
)
}
/**
* List of all hidden themes that have been seen before
* @param osmConnection
*/
public static initDiscoveredHiddenThemes(osmConnection: OsmConnection): Store<string[]> {
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 undefined
} }
return [home.lon, home.lat] return [home.lon, home.lat]
}) }),
).map((homeLonLat) => { ).map((homeLonLat) => {
if (homeLonLat === undefined) { if (homeLonLat === undefined) {
return empty return empty
@ -303,7 +389,7 @@ export default class UserRelatedState {
* */ * */
private initAmendedPrefs( private initAmendedPrefs(
layout?: LayoutConfig, layout?: LayoutConfig,
featureSwitches?: FeatureSwitchState featureSwitches?: FeatureSwitchState,
): UIEventSource<Record<string, string>> { ): UIEventSource<Record<string, string>> {
const amendedPrefs = new UIEventSource<Record<string, string>>({ const amendedPrefs = new UIEventSource<Record<string, string>>({
_theme: layout?.id, _theme: layout?.id,
@ -324,23 +410,13 @@ export default class UserRelatedState {
} }
const osmConnection = this.osmConnection const osmConnection = this.osmConnection
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => { osmConnection.preferencesHandler.allPreferences.addCallback((newPrefs) => {
for (const k in newPrefs) { for (const k in newPrefs) {
const v = newPrefs[k] const v = newPrefs[k]
if (v === "undefined" || !v) { if (v === "undefined" || v === "null" || !v) {
continue continue
} }
if (k.endsWith("-combined-length")) { amendedPrefs.data[k] = newPrefs[k] ?? ""
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.ping() amendedPrefs.ping()
@ -359,19 +435,19 @@ export default class UserRelatedState {
const missingLayers = Utils.Dedup( const missingLayers = Utils.Dedup(
untranslated untranslated
.filter((k) => k.startsWith("layers:")) .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([ const zenLinks: { link: string; id: string }[] = Utils.NoNull([
hasMissingTheme hasMissingTheme
? { ? {
id: "theme:" + layout.id, id: "theme:" + layout.id,
link: LinkToWeblate.hrefToWeblateZen( link: LinkToWeblate.hrefToWeblateZen(
language, language,
"themes", "themes",
layout.id layout.id,
), ),
} }
: undefined, : undefined,
...missingLayers.map((id) => ({ ...missingLayers.map((id) => ({
id: "layer:" + id, id: "layer:" + id,
@ -388,7 +464,7 @@ export default class UserRelatedState {
} }
amendedPrefs.ping() amendedPrefs.ping()
}, },
[this.translationMode] [this.translationMode],
) )
this.mangroveIdentity.getKeyId().addCallbackAndRun((kid) => { this.mangroveIdentity.getKeyId().addCallbackAndRun((kid) => {
@ -407,7 +483,7 @@ export default class UserRelatedState {
.makeHtml(userDetails.description) .makeHtml(userDetails.description)
?.replace(/&gt;/g, ">") ?.replace(/&gt;/g, ">")
?.replace(/&lt;/g, "<") ?.replace(/&lt;/g, "<")
?.replace(/\n/g, "") ?.replace(/\n/g, ""),
) )
} }
@ -418,7 +494,7 @@ export default class UserRelatedState {
(c: { contributor: string; commits: number }) => { (c: { contributor: string; commits: number }) => {
const replaced = c.contributor.toLowerCase().replace(/\s+/g, "") const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
return replaced === simplifiedName return replaced === simplifiedName
} },
) )
if (isTranslator) { if (isTranslator) {
amendedPrefs.data["_translation_contributions"] = "" + isTranslator.commits amendedPrefs.data["_translation_contributions"] = "" + isTranslator.commits
@ -427,7 +503,7 @@ export default class UserRelatedState {
(c: { contributor: string; commits: number }) => { (c: { contributor: string; commits: number }) => {
const replaced = c.contributor.toLowerCase().replace(/\s+/g, "") const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
return replaced === simplifiedName return replaced === simplifiedName
} },
) )
if (isCodeContributor) { if (isCodeContributor) {
amendedPrefs.data["_code_contributions"] = "" + isCodeContributor.commits amendedPrefs.data["_code_contributions"] = "" + isCodeContributor.commits
@ -441,18 +517,14 @@ export default class UserRelatedState {
// Language is managed separately // Language is managed separately
continue continue
} }
if (tags[key + "-combined-0"]) { if(tags[key] === null){
// A combined value exists continue
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])
} }
let pref = this.osmConnection.preferencesHandler.getExistingPreference(key, undefined, "")
if (!pref) {
pref = this.osmConnection.GetPreference(key, undefined, {prefix: ""})
}
pref.set(tags[key])
} }
}) })

View file

@ -67,7 +67,7 @@ import { LayerConfigJson } from "./ThemeConfig/Json/LayerConfigJson"
import Hash from "../Logic/Web/Hash" import Hash from "../Logic/Web/Hash"
import { GeoOperations } from "../Logic/GeoOperations" import { GeoOperations } from "../Logic/GeoOperations"
import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" 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" 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 = <GeocodeResult> {
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 ThemeViewStateHashActor(this)
new MetaTagging(this) new MetaTagging(this)
new TitleHandler(this.selectedElement, this.featureProperties, this) new TitleHandler(this.selectedElement, this.featureProperties, this)

View file

@ -45,9 +45,9 @@
const officialThemes: MinimalLayoutInformation[] = MoreScreen.officialThemes.themes.filter(th => th.hideFromOverview === false) const officialThemes: MinimalLayoutInformation[] = MoreScreen.officialThemes.themes.filter(th => th.hideFromOverview === false)
const hiddenThemes: MinimalLayoutInformation[] = MoreScreen.officialThemes.themes.filter(th => th.hideFromOverview === true) const hiddenThemes: MinimalLayoutInformation[] = MoreScreen.officialThemes.themes.filter(th => th.hideFromOverview === true)
let visitedHiddenThemes: Store<MinimalLayoutInformation[]> = MoreScreen.knownHiddenThemes(state.osmConnection) let visitedHiddenThemes: Store<MinimalLayoutInformation[]> = UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection)
.map((knownIds) => hiddenThemes.filter((theme) => .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"
)) ))

View file

@ -5,18 +5,19 @@
/** /**
* A menu, opened by a dot * 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) export let open = new UIEventSource(false)
function toggle() { function toggle() {
open.set(!open.data) open.set(!open.data)
} }
</script> </script>
<div class="relative" style="z-index: 50"> <div class="relative" style="z-index: 50">
<div <div
class="sidebar-unit absolute right-0 top-0 collapsable normal-background button-unstyled border-2 border-gray-300" class="sidebar-unit absolute right-0 top-0 collapsable normal-background button-unstyled"
class:collapsed={!$open}> class:collapsed={!$open}>
<slot /> <slot />
</div> </div>
@ -31,6 +32,7 @@
:global(.dots-menu > path) { :global(.dots-menu > path) {
fill: var(--interactive-background); fill: var(--interactive-background);
transition: fill 350ms linear; transition: fill 350ms linear;
cursor: pointer;
} }
@ -41,18 +43,21 @@
.collapsable { .collapsable {
max-width: 100rem; max-width: 100rem;
max-height: 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; overflow: hidden;
flex-wrap: nowrap; flex-wrap: nowrap;
text-wrap: none; text-wrap: none;
width: max-content; width: max-content;
box-shadow: #ccc ; box-shadow: #ccc ;
white-space: nowrap; white-space: nowrap;
border: 1px solid var(--button-background);
} }
.collapsed { .collapsed {
max-width: 0; max-width: 0;
border: 2px solid #00000000 max-height: 0;
border: 2px solid #00000000;
pointer-events: none;
} }
</style> </style>

View file

@ -188,18 +188,4 @@ export default class MoreScreen {
return `${linkPrefix}` return `${linkPrefix}`
} }
/**
* Gives all the IDs of the hidden themes which were previously visited
* @param osmConnection
*/
public static knownHiddenThemes(osmConnection: OsmConnection): Store<Set<string>> {
const prefix = "mapcomplete-hidden-theme-"
const userPreferences = osmConnection.preferencesHandler.preferences
return userPreferences.map((preferences) =>
new Set<string>(
Object.keys(preferences)
.filter((key) => key.startsWith(prefix))
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
))
}
} }

View file

@ -2,6 +2,8 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { Store, UIEventSource } from "../../Logic/UIEventSource"
import SimpleMetaTaggers from "../../Logic/SimpleMetaTagger" import SimpleMetaTaggers from "../../Logic/SimpleMetaTagger"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Searchbar from "../Base/Searchbar.svelte"
import Translations from "../i18n/Translations"
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)))
@ -31,9 +33,11 @@
const metaKeys: string[] = [].concat(...SimpleMetaTaggers.metatags.map((k) => k.keys)) const metaKeys: string[] = [].concat(...SimpleMetaTaggers.metatags.map((k) => k.keys))
let allCalculatedTags = new Set<string>([...calculatedTags, ...metaKeys]) let allCalculatedTags = new Set<string>([...calculatedTags, ...metaKeys])
let search = new UIEventSource<string>("")
</script> </script>
<section> <section>
<Searchbar value={search} placeholder={Translations.T("Search a key")}></Searchbar>
<table class="zebra-table break-all"> <table class="zebra-table break-all">
<tr> <tr>
<th>Key</th> <th>Key</th>
@ -43,7 +47,7 @@
<th colspan="2">Normal tags</th> <th colspan="2">Normal tags</th>
</tr> </tr>
{#each $tagKeys as key} {#each $tagKeys as key}
{#if !allCalculatedTags.has(key)} {#if !allCalculatedTags.has(key) && ($search?.length === 0 || key.toLowerCase().indexOf($search.toLowerCase()) >= 0)}
<tr> <tr>
<td>{key}</td> <td>{key}</td>
<td style="width: 75%"> <td style="width: 75%">

View file

@ -22,7 +22,7 @@
if (entry.feature?.properties?.id) { if (entry.feature?.properties?.id) {
layer = state.layout.getMatchingLayer(entry.feature.properties) layer = state.layout.getMatchingLayer(entry.feature.properties)
tags = state.featureProperties.getStore(entry.feature.properties.id) 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 }>() let dispatch = createEventDispatcher<{ select }>()
@ -47,7 +47,7 @@
if (entry.feature?.properties?.id) { if (entry.feature?.properties?.id) {
state.selectedElement.set(entry.feature) state.selectedElement.set(entry.feature)
} }
state.searchState.recentlySearched.addSelected(entry) state.userRelatedState.recentlyVisitedSearch.add(entry)
dispatch("select") dispatch("select")
} }
</script> </script>
@ -81,11 +81,11 @@
</div> </div>
<div class="flex flex-wrap gap-x-2"> <div class="flex flex-wrap gap-x-2">
{#if descriptionTr} {#if descriptionTr && tags}
<TagRenderingAnswer defaultSize="subtle" noIcons={true} config={descriptionTr} {tags} {state} <TagRenderingAnswer defaultSize="subtle" noIcons={true} config={descriptionTr} {tags} {state}
selectedElement={entry.feature} {layer} /> selectedElement={entry.feature} {layer} />
{/if} {/if}
{#if descriptionTr && entry.description} {#if descriptionTr && tags && entry.description}
{/if} {/if}
{#if entry.description} {#if entry.description}

View file

@ -22,8 +22,8 @@
export let state: ThemeViewState export let state: ThemeViewState
let activeFilters: Store<ActiveFilter[]> = state.layerState.activeFilters.map(fs => fs.filter(f => Constants.priviliged_layers.indexOf(<any>f.layer.id) < 0)) let activeFilters: Store<ActiveFilter[]> = state.layerState.activeFilters.map(fs => fs.filter(f => Constants.priviliged_layers.indexOf(<any>f.layer.id) < 0))
let recentlySeen: UIEventSource<GeocodeResult[]> = state.searchState.recentlySearched.seenThisSession let recentlySeen: Store<GeocodeResult[]> = state.userRelatedState.recentlyVisitedSearch.value
let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 6)) 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 allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview
let searchTerm = state.searchState.searchTerm let searchTerm = state.searchState.searchTerm
let results = state.searchState.suggestions let results = state.searchState.suggestions
@ -97,7 +97,7 @@
<Tr t={Translations.t.general.search.recents} /> <Tr t={Translations.t.general.search.recents} />
</h3> </h3>
<DotMenu> <DotMenu>
<button on:click={() => {state.searchState.recentlySearched.seenThisSession.set([])}}> <button on:click={() => {state.userRelatedState.recentlyVisitedSearch.clear()}}>
<TrashIcon /> <TrashIcon />
Delete search history Delete search history
</button> </button>
@ -121,7 +121,7 @@
<Tr t={Translations.t.general.search.recentThemes} /> <Tr t={Translations.t.general.search.recentThemes} />
</h3> </h3>
<DotMenu> <DotMenu>
<button on:click={() => {state.userRelatedState.recentlyVisitedThemes.set([])}}> <button on:click={() => {state.userRelatedState.recentlyVisitedThemes.clear()}}>
<TrashIcon /> <TrashIcon />
Delete earlier visited themes Delete earlier visited themes
</button> </button>

View file

@ -1,11 +1,7 @@
import { Store, UIEventSource } from "../Logic/UIEventSource" import { Store, UIEventSource } from "../Logic/UIEventSource"
import BaseUIElement from "./BaseUIElement" import BaseUIElement from "./BaseUIElement"
import LayoutConfig, { MinimalLayoutInformation } from "../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
import { import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
FeatureSource,
IndexedFeatureSource,
WritableFeatureSource
} from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection" import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { Changes } from "../Logic/Osm/Changes" import { Changes } from "../Logic/Osm/Changes"
import { ExportableMap, MapProperties } from "../Models/MapProperties" import { ExportableMap, MapProperties } from "../Models/MapProperties"
@ -18,7 +14,6 @@ import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState" import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import { MenuState } from "../Models/MenuState" import { MenuState } from "../Models/MenuState"
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader" import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
import { RasterLayerPolygon } from "../Models/RasterLayers"
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
import { OsmTags } from "../Models/OsmFeature" import { OsmTags } from "../Models/OsmFeature"
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource" import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
@ -30,6 +25,8 @@ import { Map as MlMap } from "maplibre-gl"
import ShowDataLayer from "./Map/ShowDataLayer" import ShowDataLayer from "./Map/ShowDataLayer"
import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
import SearchState from "../Logic/State/SearchState" 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. * The state needed to render a special Visualisation.
@ -80,15 +77,7 @@ export interface SpecialVisualizationState {
readonly fullNodeDatabase?: FullNodeDatabaseSource readonly fullNodeDatabase?: FullNodeDatabaseSource
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
readonly userRelatedState: { readonly userRelatedState: UserRelatedState
readonly imageLicense: UIEventSource<string>
readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full">
readonly mangroveIdentity: MangroveIdentity
readonly showAllQuestionsAtOnce: UIEventSource<boolean>
readonly preferencesAsTags: UIEventSource<Record<string, string>>
readonly language: UIEventSource<string>
readonly recentlyVisitedThemes: Store<string[]>
}
readonly imageUploadManager: ImageUploadManager readonly imageUploadManager: ImageUploadManager

View file

@ -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<Record<string, string>>, 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)) specialVisualizations.push(new AutoApplyButton(specialVisualizations))

View file

@ -1,40 +1,3 @@
<script lang="ts"> <script lang="ts">
import OHTable from "./InputElement/Helpers/OpeningHours/OHTable.svelte"
import { UIEventSource } from "../Logic/UIEventSource"
import type { OpeningHour } from "./OpeningHours/OpeningHours"
export let value: UIEventSource<OpeningHour[]> = new UIEventSource<OpeningHour[]>([
{
weekday: 3,
startMinutes: 0,
endMinutes: 0,
startHour: 12,
endHour: 16
},
{
weekday: 0,
startMinutes: 0,
endMinutes: 0,
startHour: 0,
endHour: 24
},
{
weekday: 1,
startMinutes: 0,
endMinutes: 0,
startHour: 1,
endHour: 24
},
{
weekday: 2,
startMinutes: 0,
endMinutes: 0,
startHour: 12,
endHour: 24
}
])
</script> </script>
<main >
<OHTable {value}/>
</main>

View file

@ -60,7 +60,6 @@
let compass = Orientation.singleton.alpha let compass = Orientation.singleton.alpha
let compassLoaded = Orientation.singleton.gotMeasurement let compassLoaded = Orientation.singleton.gotMeasurement
let hash = Hash.hash let hash = Hash.hash
let previewedImage = state.previewedImage
let addNewFeatureMode = state.userRelatedState.addNewFeatureMode let addNewFeatureMode = state.userRelatedState.addNewFeatureMode
let gpsAvailable = state.geolocation.geolocationState.gpsAvailable let gpsAvailable = state.geolocation.geolocationState.gpsAvailable
let gpsButtonAriaLabel = state.geolocation.geolocationState.gpsStateExplanation let gpsButtonAriaLabel = state.geolocation.geolocationState.gpsStateExplanation