forked from MapComplete/MapComplete
Rework preferences handling, improve search
This commit is contained in:
parent
b45cfcaa18
commit
4085bbc1ac
19 changed files with 438 additions and 534 deletions
|
@ -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 "
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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])
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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,85 +112,69 @@ 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) => {
|
|
||||||
const v = allPrefs[key]
|
|
||||||
if (v === undefined) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
pref.setData(v)
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
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) {
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
* Bulk-downloads all preferences
|
||||||
return
|
* @private
|
||||||
}
|
*/
|
||||||
|
private getPreferencesDict(): Promise<Record<string, string>> {
|
||||||
|
return new Promise<Record<string, string>>((resolve, reject) => {
|
||||||
this.auth.xhr(
|
this.auth.xhr(
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@ -241,58 +183,37 @@ export class OsmPreferences {
|
||||||
(error, value: XMLDocument) => {
|
(error, value: XMLDocument) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log("Could not load preferences", error)
|
console.log("Could not load preferences", error)
|
||||||
|
reject(error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const prefs = value.getElementsByTagName("preference")
|
const prefs = value.getElementsByTagName("preference")
|
||||||
const seenKeys = new Set<string>()
|
const dict: Record<string, string> = {}
|
||||||
for (let i = 0; i < prefs.length; i++) {
|
for (let i = 0; i < prefs.length; i++) {
|
||||||
const pref = prefs[i]
|
const pref = prefs[i]
|
||||||
const k = pref.getAttribute("k")
|
const k = pref.getAttribute("k")
|
||||||
this.preferences.data[k] = pref.getAttribute("v")
|
dict[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)
|
|
||||||
}
|
}
|
||||||
|
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) {
|
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]
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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() {
|
||||||
|
@ -216,7 +289,7 @@ export default class UserRelatedState {
|
||||||
"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,7 +435,7 @@ 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([
|
||||||
|
@ -369,7 +445,7 @@ export default class UserRelatedState {
|
||||||
link: LinkToWeblate.hrefToWeblateZen(
|
link: LinkToWeblate.hrefToWeblateZen(
|
||||||
language,
|
language,
|
||||||
"themes",
|
"themes",
|
||||||
layout.id
|
layout.id,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
@ -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(/>/g, ">")
|
?.replace(/>/g, ">")
|
||||||
?.replace(/</g, "<")
|
?.replace(/</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
|
|
||||||
if (tags[key].startsWith("undefined")) {
|
|
||||||
// Sometimes, a long string of 'undefined' will show up, we ignore them
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
this.osmConnection.GetLongPreference(key, "").setData(tags[key])
|
let pref = this.osmConnection.preferencesHandler.getExistingPreference(key, undefined, "")
|
||||||
} else {
|
if (!pref) {
|
||||||
this.osmConnection
|
pref = this.osmConnection.GetPreference(key, undefined, {prefix: ""})
|
||||||
.GetPreference(key, undefined, { prefix: "" })
|
|
||||||
.setData(tags[key])
|
|
||||||
}
|
}
|
||||||
|
pref.set(tags[key])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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%">
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue