2024-08-22 02:54:46 +02:00
|
|
|
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
|
|
|
import { Store } from "../../Logic/UIEventSource"
|
2023-06-14 20:39:36 +02:00
|
|
|
import { Utils } from "../../Utils"
|
2023-02-08 01:14:21 +01:00
|
|
|
import themeOverview from "../../assets/generated/theme_overview.json"
|
2022-04-24 01:32:19 +02:00
|
|
|
import Locale from "../i18n/Locale"
|
2024-08-22 02:54:46 +02:00
|
|
|
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
2020-07-29 15:48:21 +02:00
|
|
|
|
2024-09-05 02:25:03 +02:00
|
|
|
export type ThemeSearchScore = {
|
|
|
|
theme: MinimalLayoutInformation,
|
|
|
|
lowest: number,
|
|
|
|
perLayer?: Record<string, number>,
|
|
|
|
other: number
|
|
|
|
}
|
2023-12-13 02:16:53 +01:00
|
|
|
export default class MoreScreen {
|
2024-09-05 02:25:03 +02:00
|
|
|
public static readonly officialThemes: {
|
|
|
|
themes: MinimalLayoutInformation[],
|
|
|
|
layers: Record<string, Record<string, string[]>>
|
|
|
|
} = themeOverview
|
2024-08-22 02:54:46 +02:00
|
|
|
public static readonly officialThemesById: Map<string, MinimalLayoutInformation> = new Map<string, MinimalLayoutInformation>()
|
|
|
|
static {
|
2024-09-05 02:25:03 +02:00
|
|
|
for (const th of MoreScreen.officialThemes.themes) {
|
2024-08-22 02:54:46 +02:00
|
|
|
MoreScreen.officialThemesById.set(th.id, th)
|
|
|
|
}
|
|
|
|
}
|
2024-08-27 21:33:47 +02:00
|
|
|
|
2024-09-05 02:25:03 +02:00
|
|
|
/** Applies special search terms, such as 'studio', 'osmcha', ...
|
|
|
|
* Returns 'false' if nothing is matched.
|
|
|
|
* Doesn't return control flow if a match is found (navigates to another page in this case)
|
|
|
|
*/
|
|
|
|
public static applySearch(searchTerm: string, ) {
|
2023-12-13 02:16:53 +01:00
|
|
|
searchTerm = searchTerm.toLowerCase()
|
|
|
|
if (!searchTerm) {
|
2024-09-05 02:25:03 +02:00
|
|
|
return false
|
2023-12-13 02:16:53 +01:00
|
|
|
}
|
|
|
|
if (searchTerm === "personal") {
|
2024-09-05 02:25:03 +02:00
|
|
|
window.location.href = MoreScreen.createUrlFor({ id: "personal" })
|
2023-12-13 02:16:53 +01:00
|
|
|
}
|
|
|
|
if (searchTerm === "bugs" || searchTerm === "issues") {
|
|
|
|
window.location.href = "https://github.com/pietervdvn/MapComplete/issues"
|
|
|
|
}
|
|
|
|
if (searchTerm === "source") {
|
|
|
|
window.location.href = "https://github.com/pietervdvn/MapComplete"
|
|
|
|
}
|
|
|
|
if (searchTerm === "docs") {
|
|
|
|
window.location.href = "https://github.com/pietervdvn/MapComplete/tree/develop/Docs"
|
|
|
|
}
|
|
|
|
if (searchTerm === "osmcha" || searchTerm === "stats") {
|
|
|
|
window.location.href = Utils.OsmChaLinkFor(7)
|
|
|
|
}
|
2024-06-20 04:21:29 +02:00
|
|
|
if (searchTerm === "studio") {
|
2024-06-20 02:20:48 +02:00
|
|
|
window.location.href = "./studio.html"
|
|
|
|
}
|
2024-09-05 02:25:03 +02:00
|
|
|
return false
|
|
|
|
|
2021-06-10 01:36:20 +02:00
|
|
|
}
|
2022-04-28 02:04:25 +02:00
|
|
|
|
2024-09-05 02:25:03 +02:00
|
|
|
/**
|
|
|
|
* Searches for the smallest distance in words; will split both the query and the terms
|
|
|
|
*
|
|
|
|
* MoreScreen.scoreKeywords("drinking water", {"en": ["A layer with drinking water points"]}, "en") // => 0
|
|
|
|
* MoreScreen.scoreKeywords("waste", {"en": ["A layer with drinking water points"]}, "en") // => 2
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
public static scoreKeywords(query: string, keywords: Record<string, string[]> | string[], language?: string): number {
|
|
|
|
if(!keywords){
|
|
|
|
return Infinity
|
2023-04-23 13:22:57 +02:00
|
|
|
}
|
2024-09-05 02:25:03 +02:00
|
|
|
language ??= Locale.language.data
|
|
|
|
const queryParts = query.split(" ").map(q => Utils.simplifyStringForSearch(q))
|
|
|
|
let terms: string[]
|
|
|
|
if (Array.isArray(keywords)) {
|
|
|
|
terms = keywords
|
|
|
|
} else {
|
|
|
|
terms = (keywords[language] ?? []).concat(keywords["*"])
|
|
|
|
}
|
|
|
|
const termsAll = Utils.NoNullInplace(terms).flatMap(t => t.split(" "))
|
|
|
|
|
|
|
|
let distanceSummed = 0
|
|
|
|
for (let i = 0; i < queryParts.length; i++) {
|
|
|
|
const q = queryParts[i]
|
|
|
|
let minDistance: number = 99
|
|
|
|
for (const term of termsAll) {
|
|
|
|
const d = Utils.levenshteinDistance(q, Utils.simplifyStringForSearch(term))
|
|
|
|
if (d < minDistance) {
|
|
|
|
minDistance = d
|
|
|
|
}
|
|
|
|
}
|
|
|
|
distanceSummed += minDistance
|
2023-04-23 13:22:57 +02:00
|
|
|
}
|
2024-09-05 02:25:03 +02:00
|
|
|
return distanceSummed
|
|
|
|
}
|
|
|
|
|
|
|
|
public static scoreLayers(query: string): Record<string, number> {
|
|
|
|
const result: Record<string, number> = {}
|
|
|
|
for (const id in this.officialThemes.layers) {
|
|
|
|
const keywords = this.officialThemes.layers[id]
|
|
|
|
const distance = this.scoreKeywords(query, keywords)
|
|
|
|
result[id] = distance
|
2024-08-24 01:53:06 +02:00
|
|
|
}
|
2024-09-05 02:25:03 +02:00
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2024-08-27 21:33:47 +02:00
|
|
|
|
2024-09-05 02:25:03 +02:00
|
|
|
public static scoreThemes(query: string, themes: MinimalLayoutInformation[], ignoreLayers: string[] = []): Record<string, ThemeSearchScore> {
|
|
|
|
if (query?.length < 1) {
|
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
themes = Utils.NoNullInplace(themes)
|
|
|
|
const layerScores = this.scoreLayers(query)
|
|
|
|
for (const ignoreLayer of ignoreLayers) {
|
|
|
|
delete layerScores[ignoreLayer]
|
|
|
|
}
|
|
|
|
const results: Record<string, ThemeSearchScore> = {}
|
|
|
|
for (const layoutInfo of themes) {
|
|
|
|
const theme = layoutInfo.id
|
|
|
|
if (theme === "personal") {
|
2023-04-23 13:22:57 +02:00
|
|
|
continue
|
|
|
|
}
|
2024-09-05 02:25:03 +02:00
|
|
|
if (Utils.simplifyStringForSearch(theme) === query) {
|
|
|
|
results[theme] = {
|
|
|
|
theme: layoutInfo,
|
|
|
|
lowest: -1,
|
|
|
|
other: 0
|
2024-08-27 21:33:47 +02:00
|
|
|
}
|
2024-09-05 02:25:03 +02:00
|
|
|
continue
|
2024-08-27 21:33:47 +02:00
|
|
|
}
|
2024-09-05 02:25:03 +02:00
|
|
|
const perLayer = Utils.asRecord(
|
|
|
|
layoutInfo.layers ?? [], layer => layerScores[layer]
|
|
|
|
)
|
|
|
|
const language = Locale.language.data
|
|
|
|
|
|
|
|
const keywords =Utils.NoNullInplace( [layoutInfo.shortDescription, layoutInfo.title])
|
|
|
|
.map(item => typeof item === "string" ? item : (item[language] ?? item["*"]))
|
|
|
|
|
|
|
|
|
|
|
|
const other = Math.min(this.scoreKeywords(query, keywords), this.scoreKeywords(query, layoutInfo.keywords))
|
|
|
|
const lowest = Math.min(other, ...Object.values(perLayer))
|
|
|
|
results[theme] = {
|
|
|
|
theme:layoutInfo,
|
|
|
|
perLayer,
|
|
|
|
other,
|
|
|
|
lowest
|
2023-04-23 13:22:57 +02:00
|
|
|
}
|
|
|
|
}
|
2024-09-05 02:25:03 +02:00
|
|
|
return results
|
|
|
|
}
|
2023-04-23 13:22:57 +02:00
|
|
|
|
2024-09-05 02:25:03 +02:00
|
|
|
public static sortedByLowest(search: string, themes: MinimalLayoutInformation[], ignoreLayers: string[] = []){
|
|
|
|
const scored = Object.values(this.scoreThemes(search, themes, ignoreLayers ))
|
|
|
|
scored.sort((a,b) => a.lowest - b.lowest)
|
|
|
|
return scored
|
2023-04-23 13:22:57 +02:00
|
|
|
}
|
|
|
|
|
2023-12-13 02:16:53 +01:00
|
|
|
public static createUrlFor(
|
2024-08-22 02:54:46 +02:00
|
|
|
layout: { id: string },
|
2024-09-05 02:25:03 +02:00
|
|
|
state?: { layoutToUse?: { id } }
|
2024-08-22 02:54:46 +02:00
|
|
|
): string {
|
2021-11-07 16:34:51 +01:00
|
|
|
if (layout === undefined) {
|
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
if (layout.id === undefined) {
|
|
|
|
console.error("ID is undefined for layout", layout)
|
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
|
|
|
|
if (layout.id === state?.layoutToUse?.id) {
|
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
|
|
|
|
let path = window.location.pathname
|
|
|
|
// Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
|
|
|
|
path = path.substr(0, path.lastIndexOf("/"))
|
|
|
|
// Path will now contain '/dir/dir', or empty string in case of nothing
|
|
|
|
if (path === "") {
|
|
|
|
path = "."
|
|
|
|
}
|
|
|
|
|
|
|
|
let linkPrefix = `${path}/${layout.id.toLowerCase()}.html?`
|
|
|
|
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
|
2021-12-21 18:35:31 +01:00
|
|
|
linkPrefix = `${path}/theme.html?layout=${layout.id}&`
|
2021-11-07 16:34:51 +01:00
|
|
|
}
|
|
|
|
|
2024-09-05 02:25:03 +02:00
|
|
|
if (layout.id.startsWith("http://") || layout.id.startsWith("https://")) {
|
2021-12-21 18:35:31 +01:00
|
|
|
linkPrefix = `${path}/theme.html?userlayout=${layout.id}&`
|
2021-11-07 16:34:51 +01:00
|
|
|
}
|
|
|
|
|
2022-04-13 02:44:06 +02:00
|
|
|
|
2024-08-22 02:54:46 +02:00
|
|
|
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))
|
2024-09-05 02:25:03 +02:00
|
|
|
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
|
2024-08-22 02:54:46 +02:00
|
|
|
))
|
2022-04-30 02:10:57 +02:00
|
|
|
}
|
2020-07-29 15:48:21 +02:00
|
|
|
}
|