mapcomplete/Logic/Osm/OsmPreferences.ts

259 lines
8.9 KiB
TypeScript

import { UIEventSource } from "../UIEventSource"
import UserDetails, { OsmConnection } from "./OsmConnection"
import { Utils } from "../../Utils"
import { DomEvent } from "leaflet"
import preventDefault = DomEvent.preventDefault
export class OsmPreferences {
public preferences = new UIEventSource<Record<string, string>>({}, "all-osm-preferences")
private readonly preferenceSources = new Map<string, UIEventSource<string>>()
private auth: any
private userDetails: UIEventSource<UserDetails>
private longPreferences = {}
constructor(auth, osmConnection: OsmConnection) {
this.auth = auth
this.userDetails = osmConnection.userDetails
const self = this
osmConnection.OnLoggedIn(() => self.UpdatePreferences())
}
/**
* OSM preferences can be at most 255 chars
* @param key
* @param prefix
* @constructor
*/
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
if (this.longPreferences[prefix + key] !== undefined) {
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"
// Gives the number of combined preferences
const length = this.GetPreference(allStartWith + "-length", "", "")
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"
)
}
const self = this
source.addCallback((str) => {
if (str === undefined || str === "") {
return
}
if (str === null) {
console.error("Deleting " + allStartWith)
let count = parseInt(length.data)
for (let i = 0; i < count; i++) {
// Delete all the preferences
self.GetPreference(allStartWith + "-" + i, "", "").setData("")
}
self.GetPreference(allStartWith + "-length", "", "").setData("")
return
}
let i = 0
while (str !== "") {
if (str === undefined || str === "undefined") {
throw "Long pref became undefined?"
}
if (i > 100) {
throw "This long preference is getting very long... "
}
self.GetPreference(allStartWith + "-" + i, "", "").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(self.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 (self.preferences.data[key] === undefined) {
console.warn(
"Detected a broken combined preference:",
key,
"is undefined",
self.preferences
)
}
str += self.preferences.data[key] ?? ""
}
source.setData(str)
}
length.addCallback((l) => {
updateData(Number(l))
})
this.preferences.addCallbackAndRun((_) => {
updateData(Number(length.data))
})
return source
}
public GetPreference(
key: string,
defaultValue: string = undefined,
prefix: string = "mapcomplete-"
): UIEventSource<string> {
if (key.startsWith(prefix) && prefix !== "") {
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, "")
if (key.length >= 255) {
throw "Preferences: key length to big"
}
const cached = this.preferenceSources.get(key)
if (cached !== undefined) {
return cached
}
if (this.userDetails.data.loggedIn && this.preferences.data[key] === undefined) {
this.UpdatePreferences()
}
const pref = new UIEventSource<string>(
this.preferences.data[key] ?? defaultValue,
"osm-preference:" + key
)
pref.addCallback((v) => {
this.UploadPreference(key, v)
})
this.preferenceSources.set(key, pref)
return pref
}
public ClearPreferences() {
let isRunning = false
const self = this
this.preferences.addCallback((prefs) => {
console.log("Cleaning preferences...")
if (Object.keys(prefs).length == 0) {
return
}
if (isRunning) {
return
}
isRunning = true
const prefixes = ["mapcomplete-"]
for (const key in prefs) {
const matches = prefixes.some((prefix) => key.startsWith(prefix))
if (matches) {
console.log("Clearing ", key)
self.GetPreference(key, "", "").setData("")
}
}
isRunning = false
return
})
}
private UpdatePreferences() {
const self = this
this.auth.xhr(
{
method: "GET",
path: "/api/0.6/user/preferences",
},
function (error, value: XMLDocument) {
if (error) {
console.log("Could not load preferences", error)
return
}
const prefs = value.getElementsByTagName("preference")
for (let i = 0; i < prefs.length; i++) {
const pref = prefs[i]
const k = pref.getAttribute("k")
const v = pref.getAttribute("v")
self.preferences.data[k] = v
}
// We merge all the preferences: new keys are uploaded
// For differing values, the server overrides local changes
self.preferenceSources.forEach((preference, key) => {
const osmValue = self.preferences.data[key]
if (osmValue === undefined && preference.data !== undefined) {
// OSM doesn't know this value yet
self.UploadPreference(key, preference.data)
} else {
// OSM does have a value - set it
preference.setData(osmValue)
}
})
self.preferences.ping()
}
)
}
private UploadPreference(k: string, v: string) {
if (!this.userDetails.data.loggedIn) {
console.debug(`Not saving preference ${k}: user not logged in`)
return
}
if (this.preferences.data[k] === v) {
return
}
console.debug("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15))
if (v === undefined || v === "") {
this.auth.xhr(
{
method: "DELETE",
path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
options: { header: { "Content-Type": "text/plain" } },
},
function (error) {
if (error) {
console.warn("Could not remove preference", error)
return
}
console.debug("Preference ", k, "removed!")
}
)
return
}
this.auth.xhr(
{
method: "PUT",
path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
options: { header: { "Content-Type": "text/plain" } },
content: v,
},
function (error) {
if (error) {
console.warn(`Could not set preference "${k}"'`, error)
return
}
console.debug(`Preference ${k} written!`)
}
)
}
}