forked from MapComplete/MapComplete
Favourites: stabilize preferences and adding/removing favourites
This commit is contained in:
parent
f9827dd6ae
commit
3ce21f61cb
8 changed files with 122 additions and 47 deletions
|
@ -1,6 +1,6 @@
|
||||||
import StaticFeatureSource from "./StaticFeatureSource"
|
import StaticFeatureSource from "./StaticFeatureSource"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import { Store, UIEventSource } from "../../UIEventSource"
|
import { Store, Stores, UIEventSource } from "../../UIEventSource"
|
||||||
import { OsmConnection } from "../../Osm/OsmConnection"
|
import { OsmConnection } from "../../Osm/OsmConnection"
|
||||||
import { OsmId } from "../../../Models/OsmFeature"
|
import { OsmId } from "../../../Models/OsmFeature"
|
||||||
import { GeoOperations } from "../../GeoOperations"
|
import { GeoOperations } from "../../GeoOperations"
|
||||||
|
@ -14,6 +14,7 @@ import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
|
||||||
export default class FavouritesFeatureSource extends StaticFeatureSource {
|
export default class FavouritesFeatureSource extends StaticFeatureSource {
|
||||||
public static readonly prefix = "mapcomplete-favourite-"
|
public static readonly prefix = "mapcomplete-favourite-"
|
||||||
private readonly _osmConnection: OsmConnection
|
private readonly _osmConnection: OsmConnection
|
||||||
|
private readonly _detectedIds: Store<string[]>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
connection: OsmConnection,
|
connection: OsmConnection,
|
||||||
|
@ -21,62 +22,78 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
|
||||||
allFeatures: IndexedFeatureSource,
|
allFeatures: IndexedFeatureSource,
|
||||||
layout: LayoutConfig
|
layout: LayoutConfig
|
||||||
) {
|
) {
|
||||||
const detectedIds = new UIEventSource<Set<string>>(undefined)
|
const features: Store<Feature[]> = Stores.ListStabilized(
|
||||||
const features: Store<Feature[]> = connection.preferencesHandler.preferences.map(
|
connection.preferencesHandler.preferences.map((prefs) => {
|
||||||
(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) {
|
||||||
if (!key.startsWith(FavouritesFeatureSource.prefix)) {
|
if (!key.startsWith(FavouritesFeatureSource.prefix)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const id = key.substring(FavouritesFeatureSource.prefix.length)
|
|
||||||
const osmId = id.replace("-", "/")
|
try {
|
||||||
if (id.indexOf("-property-") > 0 || id.indexOf("-layer") > 0) {
|
const feat = FavouritesFeatureSource.ExtractFavourite(key, prefs)
|
||||||
continue
|
if (!feat) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
feats.push(feat)
|
||||||
|
allIds.add(feat.properties.id)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not create favourite from", key, "due to", e)
|
||||||
}
|
}
|
||||||
allIds.add(osmId)
|
|
||||||
const geometry = <[number, number]>JSON.parse(prefs[key])
|
|
||||||
const properties = FavouritesFeatureSource.getPropertiesFor(connection, id)
|
|
||||||
properties._orig_layer = prefs[FavouritesFeatureSource.prefix + id + "-layer"]
|
|
||||||
if (layout.layers.some((l) => l.id === properties._orig_layer)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
properties.id = osmId
|
|
||||||
properties._favourite = "yes"
|
|
||||||
feats.push({
|
|
||||||
type: "Feature",
|
|
||||||
properties,
|
|
||||||
geometry: {
|
|
||||||
type: "Point",
|
|
||||||
coordinates: geometry,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
console.log("Favouritess are", feats)
|
|
||||||
detectedIds.setData(allIds)
|
|
||||||
return feats
|
return feats
|
||||||
}
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
super(features)
|
const featuresWithoutAlreadyPresent = features.map((features) =>
|
||||||
|
features.filter(
|
||||||
|
(feat) => !layout.layers.some((l) => l.id === feat.properties._orig_layer)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
super(featuresWithoutAlreadyPresent)
|
||||||
|
|
||||||
this._osmConnection = connection
|
this._osmConnection = connection
|
||||||
detectedIds.addCallbackAndRunD((detected) =>
|
this._detectedIds = Stores.ListStabilized(
|
||||||
|
features.map((feats) => feats.map((f) => f.properties.id))
|
||||||
|
)
|
||||||
|
this._detectedIds.addCallbackAndRunD((detected) =>
|
||||||
this.markFeatures(detected, indexedSource, allFeatures)
|
this.markFeatures(detected, indexedSource, allFeatures)
|
||||||
)
|
)
|
||||||
// We use the indexedFeatureSource as signal to update
|
// We use the indexedFeatureSource as signal to update
|
||||||
allFeatures.features.map((_) =>
|
allFeatures.features.map((_) =>
|
||||||
this.markFeatures(detectedIds.data, indexedSource, allFeatures)
|
this.markFeatures(this._detectedIds.data, indexedSource, allFeatures)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ExtractFavourite(key: string, prefs: Record<string, string>): Feature {
|
||||||
|
const id = key.substring(FavouritesFeatureSource.prefix.length)
|
||||||
|
const osmId = id.replace("-", "/")
|
||||||
|
if (id.indexOf("-property-") > 0 || id.endsWith("-layer") || id.endsWith("-theme")) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const geometry = <[number, number]>JSON.parse(prefs[key])
|
||||||
|
const properties = FavouritesFeatureSource.getPropertiesFor(prefs, id)
|
||||||
|
properties._orig_layer = prefs[FavouritesFeatureSource.prefix + id + "-layer"]
|
||||||
|
|
||||||
|
properties.id = osmId
|
||||||
|
properties._favourite = "yes"
|
||||||
|
return {
|
||||||
|
type: "Feature",
|
||||||
|
properties,
|
||||||
|
geometry: {
|
||||||
|
type: "Point",
|
||||||
|
coordinates: geometry,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static getPropertiesFor(
|
private static getPropertiesFor(
|
||||||
osmConnection: OsmConnection,
|
prefs: Record<string, string>,
|
||||||
id: string
|
id: string
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
const properties: Record<string, string> = {}
|
const properties: Record<string, string> = {}
|
||||||
const prefs = osmConnection.preferencesHandler.preferences.data
|
|
||||||
const minLength = FavouritesFeatureSource.prefix.length + id.length + "-property-".length
|
const minLength = FavouritesFeatureSource.prefix.length + id.length + "-property-".length
|
||||||
for (const key in prefs) {
|
for (const key in prefs) {
|
||||||
if (key.length < minLength) {
|
if (key.length < minLength) {
|
||||||
|
@ -104,14 +121,18 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
|
||||||
if (isFavourite) {
|
if (isFavourite) {
|
||||||
const center = GeoOperations.centerpointCoordinates(feature)
|
const center = GeoOperations.centerpointCoordinates(feature)
|
||||||
pref.setData(JSON.stringify(center))
|
pref.setData(JSON.stringify(center))
|
||||||
|
|
||||||
this._osmConnection.GetPreference("favourite-" + id + "-layer").setData(layer)
|
this._osmConnection.GetPreference("favourite-" + id + "-layer").setData(layer)
|
||||||
this._osmConnection.GetPreference("favourite-" + id + "-theme").setData(theme)
|
this._osmConnection.GetPreference("favourite-" + id + "-theme").setData(theme)
|
||||||
|
|
||||||
for (const key in tags.data) {
|
for (const key in tags.data) {
|
||||||
const pref = this._osmConnection.GetPreference(
|
const pref = this._osmConnection.GetPreference(
|
||||||
"favourite-" + id + "-property-" + key.replaceAll(":", "__")
|
"favourite-" + id + "-property-" + key.replaceAll(":", "__")
|
||||||
)
|
)
|
||||||
pref.setData(tags.data[key])
|
const v = tags.data[key]
|
||||||
|
if (v === "" || !v) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pref.setData("" + v)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._osmConnection.preferencesHandler.removeAllWithPrefix(
|
this._osmConnection.preferencesHandler.removeAllWithPrefix(
|
||||||
|
@ -129,7 +150,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
private markFeatures(
|
private markFeatures(
|
||||||
detected: Set<string>,
|
detected: string[],
|
||||||
featureProperties: FeaturePropertiesStore,
|
featureProperties: FeaturePropertiesStore,
|
||||||
allFeatures: IndexedFeatureSource
|
allFeatures: IndexedFeatureSource
|
||||||
) {
|
) {
|
||||||
|
@ -141,7 +162,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
|
||||||
}
|
}
|
||||||
const store = featureProperties.getStore(id)
|
const store = featureProperties.getStore(id)
|
||||||
const origValue = store.data._favourite
|
const origValue = store.data._favourite
|
||||||
if (detected.has(id)) {
|
if (detected.indexOf(id) >= 0) {
|
||||||
if (origValue !== "yes") {
|
if (origValue !== "yes") {
|
||||||
store.data._favourite = "yes"
|
store.data._favourite = "yes"
|
||||||
store.ping()
|
store.ping()
|
||||||
|
|
|
@ -12,6 +12,10 @@ export class OsmPreferences {
|
||||||
"all-osm-preferences",
|
"all-osm-preferences",
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
/**
|
||||||
|
* A map containing the individual preference sources
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private readonly preferenceSources = new Map<string, UIEventSource<string>>()
|
private readonly preferenceSources = new Map<string, UIEventSource<string>>()
|
||||||
private auth: any
|
private auth: any
|
||||||
private userDetails: UIEventSource<UserDetails>
|
private userDetails: UIEventSource<UserDetails>
|
||||||
|
@ -21,7 +25,10 @@ export class OsmPreferences {
|
||||||
this.auth = auth
|
this.auth = auth
|
||||||
this.userDetails = osmConnection.userDetails
|
this.userDetails = osmConnection.userDetails
|
||||||
const self = this
|
const self = this
|
||||||
osmConnection.OnLoggedIn(() => self.UpdatePreferences())
|
osmConnection.OnLoggedIn(() => {
|
||||||
|
self.UpdatePreferences(true)
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -72,11 +79,19 @@ export class OsmPreferences {
|
||||||
let i = 0
|
let i = 0
|
||||||
while (str !== "") {
|
while (str !== "") {
|
||||||
if (str === undefined || str === "undefined") {
|
if (str === undefined || str === "undefined") {
|
||||||
|
source.setData(undefined)
|
||||||
throw (
|
throw (
|
||||||
"Got 'undefined' or a literal string containing 'undefined' for a long preference with name " +
|
"Got 'undefined' or a literal string containing 'undefined' for a long preference with name " +
|
||||||
key
|
key
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (str === "undefined") {
|
||||||
|
source.setData(undefined)
|
||||||
|
throw (
|
||||||
|
"Got a literal string containing 'undefined' for a long preference with name " +
|
||||||
|
key
|
||||||
|
)
|
||||||
|
}
|
||||||
if (i > 100) {
|
if (i > 100) {
|
||||||
throw "This long preference is getting very long... "
|
throw "This long preference is getting very long... "
|
||||||
}
|
}
|
||||||
|
@ -197,7 +212,7 @@ export class OsmPreferences {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private UpdatePreferences() {
|
private UpdatePreferences(forceUpdate?: boolean) {
|
||||||
const self = this
|
const self = this
|
||||||
this.auth.xhr(
|
this.auth.xhr(
|
||||||
{
|
{
|
||||||
|
@ -210,11 +225,22 @@ export class OsmPreferences {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const prefs = value.getElementsByTagName("preference")
|
const prefs = value.getElementsByTagName("preference")
|
||||||
|
const seenKeys = new Set<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")
|
||||||
const v = pref.getAttribute("v")
|
const v = pref.getAttribute("v")
|
||||||
self.preferences.data[k] = v
|
self.preferences.data[k] = v
|
||||||
|
seenKeys.add(k)
|
||||||
|
}
|
||||||
|
if (forceUpdate) {
|
||||||
|
for (let key in self.preferences.data) {
|
||||||
|
if (seenKeys.has(key)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
console.log("Deleting key", key, "as we didn't find it upstream")
|
||||||
|
delete self.preferences.data[key]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We merge all the preferences: new keys are uploaded
|
// We merge all the preferences: new keys are uploaded
|
||||||
|
@ -289,9 +315,10 @@ export class OsmPreferences {
|
||||||
removeAllWithPrefix(prefix: string) {
|
removeAllWithPrefix(prefix: string) {
|
||||||
for (const key in this.preferences.data) {
|
for (const key in this.preferences.data) {
|
||||||
if (key.startsWith(prefix)) {
|
if (key.startsWith(prefix)) {
|
||||||
this.GetPreference(key, undefined, { prefix: "" }).setData(undefined)
|
this.GetPreference(key, "", { prefix: "" }).setData(undefined)
|
||||||
console.log("Clearing preference", key)
|
console.log("Clearing preference", key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.preferences.ping()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -294,6 +294,9 @@ export default class UserRelatedState {
|
||||||
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
|
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
|
||||||
for (const k in newPrefs) {
|
for (const k in newPrefs) {
|
||||||
const v = newPrefs[k]
|
const v = newPrefs[k]
|
||||||
|
if (v === "undefined" || !v) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (k.endsWith("-combined-length")) {
|
if (k.endsWith("-combined-length")) {
|
||||||
const l = Number(v)
|
const l = Number(v)
|
||||||
const key = k.substring(0, k.length - "length".length)
|
const key = k.substring(0, k.length - "length".length)
|
||||||
|
@ -308,7 +311,6 @@ export default class UserRelatedState {
|
||||||
}
|
}
|
||||||
|
|
||||||
amendedPrefs.ping()
|
amendedPrefs.ping()
|
||||||
console.log("Amended prefs are:", amendedPrefs.data)
|
|
||||||
})
|
})
|
||||||
const translationMode = osmConnection.GetPreference("translation-mode")
|
const translationMode = osmConnection.GetPreference("translation-mode")
|
||||||
|
|
||||||
|
@ -395,6 +397,13 @@ export default class UserRelatedState {
|
||||||
}
|
}
|
||||||
if (tags[key + "-combined-0"]) {
|
if (tags[key + "-combined-0"]) {
|
||||||
// A combined value exists
|
// A combined value exists
|
||||||
|
console.log(
|
||||||
|
"Trying to get a long preference for ",
|
||||||
|
key,
|
||||||
|
"with length value",
|
||||||
|
tags[key],
|
||||||
|
"as -combined-0 exists"
|
||||||
|
)
|
||||||
this.osmConnection.GetLongPreference(key, "").setData(tags[key])
|
this.osmConnection.GetLongPreference(key, "").setData(tags[key])
|
||||||
} else {
|
} else {
|
||||||
this.osmConnection
|
this.osmConnection
|
||||||
|
|
|
@ -14,7 +14,7 @@ export class MangroveIdentity {
|
||||||
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined)
|
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined)
|
||||||
this.keypair = keypairEventSource
|
this.keypair = keypairEventSource
|
||||||
mangroveIdentity.addCallbackAndRunD(async (data) => {
|
mangroveIdentity.addCallbackAndRunD(async (data) => {
|
||||||
if (data === "") {
|
if (!data) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data))
|
const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data))
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
* Renders a 'marker', which consists of multiple 'icons'
|
* Renders a 'marker', which consists of multiple 'icons'
|
||||||
*/
|
*/
|
||||||
export let marker: IconConfig[] = config?.marker;
|
export let marker: IconConfig[] = config?.marker;
|
||||||
export let rotation: TagRenderingConfig;
|
export let rotation: TagRenderingConfig = undefined;
|
||||||
export let tags: Store<Record<string, string>>;
|
export let tags: Store<Record<string, string>>;
|
||||||
let _rotation = rotation ? tags.map(tags => rotation.GetRenderValue(tags).Subs(tags).txt) : new ImmutableStore(0);
|
let _rotation = rotation ? tags.map(tags => rotation.GetRenderValue(tags).Subs(tags).txt) : new ImmutableStore(0);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -32,8 +32,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export let icon: string | undefined;
|
export let icon: string | undefined;
|
||||||
export let color: string | undefined;
|
export let color: string | undefined = undefined
|
||||||
export let clss: string | undefined
|
export let clss: string | undefined = undefined
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if icon}
|
{#if icon}
|
||||||
|
|
|
@ -301,10 +301,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
if (str === undefined || str === null) {
|
if (str === undefined || str === null) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
if (typeof str !== "string") {
|
||||||
|
console.error("Not a string:", str)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
if (str.length <= l) {
|
if (str.length <= l) {
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
return str.substr(0, l - 3) + "..."
|
return str.substr(0, l - 1) + "…"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -125,7 +125,21 @@ describe("PrepareTheme", () => {
|
||||||
en: "Test layer - please ignore",
|
en: "Test layer - please ignore",
|
||||||
},
|
},
|
||||||
titleIcons: [],
|
titleIcons: [],
|
||||||
pointRendering: [{ location: ["point"], label: "xyz" }],
|
pointRendering: [
|
||||||
|
{
|
||||||
|
location: ["point"],
|
||||||
|
label: "xyz",
|
||||||
|
iconBadges: [
|
||||||
|
{
|
||||||
|
if: "_favourite=yes",
|
||||||
|
then: <any>{
|
||||||
|
id: "circlewhiteheartred",
|
||||||
|
render: "circle:white;heart:red",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
lineRendering: [{ width: 1 }],
|
lineRendering: [{ width: 1 }],
|
||||||
}
|
}
|
||||||
const sharedLayers = constructSharedLayers()
|
const sharedLayers = constructSharedLayers()
|
||||||
|
|
Loading…
Reference in a new issue