Add themes to search functionality, including quickswitch between recent themes

This commit is contained in:
Pieter Vander Vennet 2024-08-22 02:54:46 +02:00
parent b4866cdbac
commit 329865a15e
22 changed files with 679 additions and 431 deletions

View file

@ -1,12 +1,13 @@
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
import { Utils } from "../../Utils"
export default class CombinedSearcher implements GeocodingProvider {
private _providers: ReadonlyArray<GeocodingProvider>
private _providersWithSuggest: ReadonlyArray<GeocodingProvider>
constructor(...providers: ReadonlyArray<GeocodingProvider>) {
this._providers = providers
this._providersWithSuggest = providers.filter(pr => pr.suggest !== undefined)
this._providers = Utils.NoNull(providers)
this._providersWithSuggest = this._providers.filter(pr => pr.suggest !== undefined)
}
/**

View file

@ -24,7 +24,7 @@ export type GeoCodeResult = {
osm_type?: "node" | "way" | "relation"
osm_id?: string,
category?: GeocodingCategory,
importance?: number
payload?: object
}
export interface GeocodingOptions {

View file

@ -7,21 +7,20 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
export class RecentSearch {
private readonly _recentSearches: UIEventSource<string[]>
public readonly recentSearches: Store<string[]>
private readonly _seenThisSession: UIEventSource<GeoCodeResult[]> = new UIEventSource<GeoCodeResult[]>([])
public readonly seenThisSession: Store<GeoCodeResult[]> = this._seenThisSession
private readonly _seenThisSession: UIEventSource<GeoCodeResult[]>
public readonly seenThisSession: Store<GeoCodeResult[]>
constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) {
const longPref = state.osmConnection.preferencesHandler.GetLongPreference("recent-searches")
this._recentSearches = longPref.sync(str => !str ? [] : <string[]>JSON.parse(str), [], strs => JSON.stringify(strs))
this.recentSearches = this._recentSearches
// const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches")
this._seenThisSession = new UIEventSource<GeoCodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
this.seenThisSession = this._seenThisSession
state.selectedElement.addCallbackAndRunD(selected => {
const [osm_type, osm_id] = selected.properties.id.split("/")
const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
const entry = <GeoCodeResult> {
const entry = <GeoCodeResult>{
feature: selected,
osm_id, osm_type,
description: "Viewed recently",
@ -33,7 +32,7 @@ export class RecentSearch {
}
addSelected(entry: GeoCodeResult) {
const arr = [...this.seenThisSession.data.slice(0, 20), entry]
const arr = [...(this.seenThisSession.data ?? []).slice(0, 20), entry]
const seenIds = new Set<string>()
for (let i = arr.length - 1; i >= 0; i--) {

View file

@ -0,0 +1,43 @@
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
import * as themeOverview from "../../assets/generated/theme_overview.json"
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
import { Utils } from "../../Utils"
import MoreScreen from "../../UI/BigComponents/MoreScreen"
import { Store } from "../UIEventSource"
export default class ThemeSearch implements GeocodingProvider {
private static allThemes: MinimalLayoutInformation[] = (themeOverview["default"] ?? themeOverview)
private readonly _state: SpecialVisualizationState
private readonly _knownHiddenThemes: Store<Set<string>>
constructor(state: SpecialVisualizationState) {
this._state = state
this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection)
}
search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
return this.suggest(query, options)
}
async suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
if(query.length < 1){
return []
}
const limit = options?.limit ?? 4
query = Utils.simplifyStringForSearch(query)
const withMatch = ThemeSearch.allThemes
.filter(th => !th.hideFromOverview )
.filter(th => th.id !== this._state.layout.id)
.filter(th => MoreScreen.MatchesLayout(th, query))
.slice(0, limit + 1)
return withMatch.map(match => (<GeoCodeResult> {
payload: match,
osm_id: match.id
}))
}
}

View file

@ -71,7 +71,7 @@ export class OsmPreferences {
}
if (str === null) {
console.error("Deleting " + allStartWith)
let count = parseInt(length.data)
const count = parseInt(length.data)
for (let i = 0; i < count; i++) {
// Delete all the preferences
self.GetPreference(allStartWith + "-" + i, "", subOptions).setData("")

View file

@ -78,6 +78,10 @@ export default class UserRelatedState {
public readonly preferencesAsTags: UIEventSource<Record<string, string>>
private readonly _mapProperties: MapProperties
private readonly _recentlyVisitedThemes: UIEventSource<string[]>
public readonly recentlyVisitedThemes: Store<string[]>
constructor(
osmConnection: OsmConnection,
layout?: LayoutConfig,
@ -109,7 +113,7 @@ export default class UserRelatedState {
this.showAllQuestionsAtOnce = UIEventSource.asBoolean(
this.osmConnection.GetPreference("show-all-questions", "false", {
documentation:
"Either 'true' or 'false'. If set, all questions will be shown all at once",
"Either 'true' or 'false'. If set, all questions will be shown all at once"
})
)
this.language = this.osmConnection.GetPreference("language")
@ -129,7 +133,7 @@ export default class UserRelatedState {
undefined,
{
documentation:
"The ID of a layer or layer category that MapComplete uses by default",
"The ID of a layer or layer category that MapComplete uses by default"
}
)
@ -137,12 +141,12 @@ export default class UserRelatedState {
"preferences-add-new-mode",
"button_click_right",
{
documentation: "How adding a new feature is done",
documentation: "How adding a new feature is done"
}
)
this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", {
documentation: "The license under which new images are uploaded",
documentation: "The license under which new images are uploaded"
})
this.installedUserThemes = this.InitInstalledUserThemes()
@ -150,6 +154,30 @@ export default class UserRelatedState {
this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches)
const prefs = this.osmConnection
this._recentlyVisitedThemes = UIEventSource.asObject(prefs.GetLongPreference("recently-visited-themes"), [])
this.recentlyVisitedThemes = this._recentlyVisitedThemes
if (layout) {
const osmConn =this.osmConnection
const recentlyVisited = this._recentlyVisitedThemes
function update() {
if (!osmConn.isLoggedIn.data) {
return
}
const previously = recentlyVisited.data
if (previously[0] === layout.id) {
return true
}
const newThemes = Utils.Dedup([layout.id, ...previously]).slice(0, 30)
recentlyVisited.set(newThemes)
return true
}
this._recentlyVisitedThemes.addCallbackAndRun(() => update())
this.osmConnection.isLoggedIn.addCallbackAndRun(() => update())
}
this.syncLanguage()
}
@ -171,13 +199,13 @@ export default class UserRelatedState {
public GetUnofficialTheme(id: string):
| {
id: string
icon: string
title: any
shortDescription: any
definition?: any
isOfficial: boolean
}
id: string
icon: string
title: any
shortDescription: any
definition?: any
isOfficial: boolean
}
| undefined {
console.log("GETTING UNOFFICIAL THEME")
const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id)
@ -202,8 +230,8 @@ export default class UserRelatedState {
} catch (e) {
console.warn(
"Removing theme " +
id +
" as it could not be parsed from the preferences; the content is:",
id +
" as it could not be parsed from the preferences; the content is:",
str
)
pref.setData(null)
@ -233,7 +261,7 @@ export default class UserRelatedState {
icon: layout.icon,
title: layout.title.translations,
shortDescription: layout.shortDescription.translations,
definition: layout["definition"],
definition: layout["definition"]
})
)
}
@ -273,13 +301,13 @@ export default class UserRelatedState {
id: "home",
"user:home": "yes",
_lon: homeLonLat[0],
_lat: homeLonLat[1],
_lat: homeLonLat[1]
},
geometry: {
type: "Point",
coordinates: homeLonLat,
},
},
coordinates: homeLonLat
}
}
]
})
return new StaticFeatureSource(feature)
@ -300,7 +328,7 @@ export default class UserRelatedState {
_applicationOpened: new Date().toISOString(),
_supports_sharing:
typeof window === "undefined" ? "no" : window.navigator.share ? "yes" : "no",
_iframe: Utils.isIframe ? "yes" : "no",
_iframe: Utils.isIframe ? "yes" : "no"
})
for (const key in Constants.userJourney) {
@ -355,18 +383,18 @@ export default class UserRelatedState {
const zenLinks: { link: string; id: string }[] = Utils.NoNull([
hasMissingTheme
? {
id: "theme:" + layout.id,
link: LinkToWeblate.hrefToWeblateZen(
language,
"themes",
layout.id
),
}
id: "theme:" + layout.id,
link: LinkToWeblate.hrefToWeblateZen(
language,
"themes",
layout.id
)
}
: undefined,
...missingLayers.map((id) => ({
id: "layer:" + id,
link: LinkToWeblate.hrefToWeblateZen(language, "layers", id),
})),
link: LinkToWeblate.hrefToWeblateZen(language, "layers", id)
}))
])
const untranslated_count = untranslated.length
amendedPrefs.data["_translation_total"] = "" + total
@ -391,8 +419,8 @@ export default class UserRelatedState {
for (const k in userDetails) {
amendedPrefs.data["_" + k] = "" + userDetails[k]
}
if(userDetails.description){
amendedPrefs.data["_description_html"] = Utils.purify(new Showdown.Converter()
if (userDetails.description) {
amendedPrefs.data["_description_html"] = Utils.purify(new Showdown.Converter()
.makeHtml(userDetails.description)
?.replace(/&gt;/g, ">")
?.replace(/&lt;/g, "<")

View file

@ -104,7 +104,9 @@ export abstract class Store<T> implements Readable<T> {
extraStoresToWatch: Store<any>[],
callbackDestroyFunction: (f: () => void) => void
): Store<J>
M
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraStoresToWatch?: Store<any>[],
@ -246,6 +248,7 @@ export abstract class Store<T> implements Readable<T> {
return f(<Exclude<T, undefined | null>>t)
})
}
public stabilized(millisToStabilize): Store<T> {
if (Utils.runningFromConsole) {
return this
@ -311,12 +314,14 @@ export class ImmutableStore<T> extends Store<T> {
public readonly data: T
static FALSE = new ImmutableStore<boolean>(false)
static TRUE = new ImmutableStore<boolean>(true)
constructor(data: T) {
super()
this.data = data
}
private static readonly pass: () => void = () => {}
private static readonly pass: () => void = () => {
}
addCallback(_: (data: T) => void): () => void {
// pass: data will never change
@ -718,6 +723,27 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
)
}
static asObject<T extends object>(stringUIEventSource: UIEventSource<string>, defaultV: T): UIEventSource<T> {
return stringUIEventSource.sync(
(str) => {
if (str === undefined || str === null || str === "") {
return defaultV
}
try {
return <T> JSON.parse(str)
} catch (e) {
console.error("Could not parse value", str,"due to",e)
return defaultV
}
},
[],
(b) => {
console.log("Stringifying", b)
return JSON.stringify(b) ?? ""
}
)
}
/**
* Create a new UIEVentSource. Whenever 'source' changes, the returned UIEventSource will get this value as well.
* However, this value can be overriden without affecting source
@ -863,7 +889,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
const newSource = new UIEventSource<J>(f(this.data), "map(" + this.tag + ")@" + callee)
const update = function () {
const update = function() {
newSource.setData(f(self.data))
return allowUnregister && newSource._callbacks.length() === 0
}