forked from MapComplete/MapComplete
Add themes to search functionality, including quickswitch between recent themes
This commit is contained in:
parent
b4866cdbac
commit
329865a15e
22 changed files with 679 additions and 431 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export type GeoCodeResult = {
|
|||
osm_type?: "node" | "way" | "relation"
|
||||
osm_id?: string,
|
||||
category?: GeocodingCategory,
|
||||
importance?: number
|
||||
payload?: object
|
||||
}
|
||||
|
||||
export interface GeocodingOptions {
|
||||
|
|
|
|||
|
|
@ -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--) {
|
||||
|
|
|
|||
43
src/Logic/Geocoding/ThemeSearch.ts
Normal file
43
src/Logic/Geocoding/ThemeSearch.ts
Normal 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
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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("")
|
||||
|
|
|
|||
|
|
@ -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(/>/g, ">")
|
||||
?.replace(/</g, "<")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue